diff --git a/packages/dynamic-address-resolution/README.md b/packages/dynamic-address-resolution/README.md index 2f4fe25d7..3c42829a5 100644 --- a/packages/dynamic-address-resolution/README.md +++ b/packages/dynamic-address-resolution/README.md @@ -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 @@ -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 +const address = await resolveInstructionAccountAddress({ + accountsInput: { authority }, + dataInput: { amount: 1_000_000_000n }, + path: [root, root.program, instruction, vaultAccount], +}); ``` -Writes `-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({ - 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`
(PDA, programId, etc.) | `{ system?: Address }` | Auto-resolved to `defaultValue` if omitted | -| Optional account (`isOptional: true`)
without `defaultValue` | `{ system: Address \| null }` | Resolved via `optionalAccountStrategy`,
if provided as `null` | -| Optional account (`isOptional: true`)
with `defaultValue` | `{ system?: Address \| null }` | - `null` resolves via `optionalAccountStrategy`
- `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`. +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 ``` -### `createDefaultValueEncoderVisitor(codec)` +Writes `-address-resolution-types.ts` to the output directory. -Returns a `Visitor` 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({ + 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. diff --git a/packages/dynamic-address-resolution/src/cli/commands/generate-types/register-command.ts b/packages/dynamic-address-resolution/src/cli/commands/generate-types/register-command.ts index 4eb8f2f87..5ec6d1b72 100644 --- a/packages/dynamic-address-resolution/src/cli/commands/generate-types/register-command.ts +++ b/packages/dynamic-address-resolution/src/cli/commands/generate-types/register-command.ts @@ -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('', 'Path to a Codama IDL JSON file (e.g., ./idl/codama.json)') .argument('', 'Path to the output directory for the generated .ts file, e.g., ./generated') diff --git a/packages/dynamic-address-resolution/src/codegen/codama-type-to-ts.ts b/packages/dynamic-address-resolution/src/codegen/codama-type-to-ts.ts index 8728166a3..576f18ef1 100644 --- a/packages/dynamic-address-resolution/src/codegen/codama-type-to-ts.ts +++ b/packages/dynamic-address-resolution/src/codegen/codama-type-to-ts.ts @@ -1,14 +1,22 @@ 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': @@ -16,56 +24,39 @@ export function codamaTypeToTS(type: TypeNode | undefined, definedTypes: Defined 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': @@ -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`; - } + case 'mapTypeNode': + return `Record`; 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; diff --git a/packages/dynamic-address-resolution/src/codegen/collect-resolver-names.ts b/packages/dynamic-address-resolution/src/codegen/collect-resolver-names.ts deleted file mode 100644 index 1c5074a8b..000000000 --- a/packages/dynamic-address-resolution/src/codegen/collect-resolver-names.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { InstructionInputValueNode, InstructionNode } from 'codama'; - -/** - * Collects all unique resolverValueNode names from an instruction's accounts and arguments. - */ -export function collectResolverNames(ix: InstructionNode): Set { - const names = new Set(); - - 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): 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); - } -} diff --git a/packages/dynamic-address-resolution/src/codegen/generate-pda-types.ts b/packages/dynamic-address-resolution/src/codegen/generate-pda-types.ts index 6f3f66d50..c00391bb8 100644 --- a/packages/dynamic-address-resolution/src/codegen/generate-pda-types.ts +++ b/packages/dynamic-address-resolution/src/codegen/generate-pda-types.ts @@ -4,7 +4,7 @@ import { codamaTypeToTS } from './codama-type-to-ts'; import { collectPdaNodesFromIdl } from './collect-pda-nodes'; /** - * Generate `${Pda}PdaSeeds` types and the aggregate `${Program}Pdas` map type. + * Generate `${Pda}Seeds` 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. */ @@ -23,7 +23,7 @@ export function generatePdaTypes(idl: RootNode): { mapTypeName: string | null; t const variableSeeds = getVariableSeedNodes(pdaNode); if (variableSeeds.length === 0) continue; const typeName = pascalCase(pdaName); - output += `export type ${typeName}PdaSeeds = {\n`; + output += `export type ${typeName}Seeds = {\n`; for (const seed of variableSeeds) { const tsType = seed.type ? codamaTypeToTS(seed.type, definedTypes) @@ -39,7 +39,7 @@ export function generatePdaTypes(idl: RootNode): { mapTypeName: string | null; t for (const [pdaName, pdaNode] of pdaMap) { const typeName = pascalCase(pdaName); const seedsParam = - getVariableSeedNodes(pdaNode).length > 0 ? `seeds: ${typeName}PdaSeeds` : `seeds?: Record`; + getVariableSeedNodes(pdaNode).length > 0 ? `seeds: ${typeName}Seeds` : `seeds?: Record`; output += ` ${pdaName}: (${seedsParam}) => Promise;\n`; } output += '};\n\n'; diff --git a/packages/dynamic-address-resolution/src/codegen/generate-resolution-input-types.ts b/packages/dynamic-address-resolution/src/codegen/generate-resolution-input-types.ts index 2ae6bebeb..a69ea7bb9 100644 --- a/packages/dynamic-address-resolution/src/codegen/generate-resolution-input-types.ts +++ b/packages/dynamic-address-resolution/src/codegen/generate-resolution-input-types.ts @@ -1,72 +1,38 @@ import { type DefinedTypeNode, type InstructionNode, type RootNode } from 'codama'; -import { OPTIONAL_NODE_KINDS } from '../shared/nodes'; import { codamaTypeToTS } from './codama-type-to-ts'; -import { collectResolverNames } from './collect-resolver-names'; import { getResolutionRefs } from './get-resolution-refs'; import { isAccountAutoResolvable } from './is-account-auto-resolvable'; /** - * Local non-exported declaration of `ResolverFn` emitted into generated files - * that contain at least one `${Name}Resolvers` block. The structural shape - * must remain identical to the runtime `ResolverFn` exported from - * `@codama/dynamic-address-resolution`; a type-level test in this package - * locks the two together. - * - * @internal Exported only for a drift test that pins this string to the - * runtime `ResolverFn` type. Do not consume from downstream codegen — call - * `generateResolutionInputTypes` instead, which inlines this declaration - * into the emitted output. - */ -export const RESOLVER_FN_DECLARATION = - 'type ResolverFn = (argumentsInput: TArgumentsInput, accountsInput: TAccountsInput) => Promise;\n\n'; - -/** - * Emits the input types required for address resolution of each instruction — - * `${Name}Args`, `${Name}Accounts`, `${Name}Resolvers`. - * - * These types live in this package because address resolution - * (PDAs, defaults, resolver functions) operates on them. + * Emits the input types required for address resolution of each instruction: + * `${Name}InstructionDataArgs` and `${Name}Accounts`, whose remaining accounts + * are named account lists. */ export function generateResolutionInputTypes(idl: RootNode): string { const definedTypes = idl.program.definedTypes ?? []; - const hasAnyResolvers = (idl.program.instructions ?? []).some(ix => getResolutionRefs(ix).hasResolvers); - let output = hasAnyResolvers ? RESOLVER_FN_DECLARATION : ''; - for (const ix of idl.program.instructions ?? []) { - output += generateTypeBlockForInstruction(ix, definedTypes); - } - return output; + return (idl.program.instructions ?? []).map(ix => generateTypeBlockForInstruction(ix, definedTypes)).join(''); } function generateTypeBlockForInstruction(ix: InstructionNode, definedTypes: DefinedTypeNode[]): string { - const refs = getResolutionRefs(ix); + const refs = getResolutionRefs(ix, definedTypes); let output = ''; - if (refs.argsRef) { - const args = (ix.arguments ?? []).filter(arg => arg.defaultValueStrategy !== 'omitted'); - const remainingAccountArgs = (ix.remainingAccounts ?? []).filter(ra => ra.value.kind === 'argumentValueNode'); - output += `export type ${refs.argsRef} = {\n`; - for (const arg of args) { - const tsType = codamaTypeToTS(arg.type, definedTypes); - const isOptional = OPTIONAL_NODE_KINDS.includes(arg.type.kind); - const sep = isOptional ? '?:' : ':'; - output += ` ${arg.identifier}${sep} ${tsType};\n`; - } - for (const ra of remainingAccountArgs) { - const sep = ra.isOptional ? '?:' : ':'; - output += ` ${ra.value.name}${sep} Address[];\n`; - } - output += '};\n\n'; + if (refs.dataRef) { + output += `export type ${refs.dataRef} = ${codamaTypeToTS(ix.data, definedTypes)};\n\n`; } - if ((ix.accounts ?? []).length > 0) { - output += `export type ${refs.accountsRef} = {\n`; - for (const acc of ix.accounts ?? []) { - const omittable = isAccountAutoResolvable(acc) ? '?' : ''; - const type = acc.isOptional ? 'Address | null' : 'Address'; - output += ` ${acc.identifier}${omittable}: ${type};\n`; - } - output += '};\n\n'; + const accounts = [ + ...(ix.accounts ?? []).map(account => { + const omittable = isAccountAutoResolvable(account, ix) ? '?' : ''; + return ` ${account.identifier}${omittable}: ${account.isOptional ? 'Address | null' : 'Address'};\n`; + }), + ...(ix.remainingAccounts ?? []).map( + remaining => ` ${remaining.identifier}${remaining.isOptional ? '?' : ''}: Address[];\n`, + ), + ]; + if (accounts.length > 0) { + output += `export type ${refs.accountsRef} = {\n${accounts.join('')}};\n\n`; output += `export type ${refs.accountsWithDataRef} = ${refs.accountsRef} & Record;\n\n`; } else { // No IDL-declared accounts: emit the strict and loose forms independently. @@ -74,14 +40,5 @@ function generateTypeBlockForInstruction(ix: InstructionNode, definedTypes: Defi output += `export type ${refs.accountsWithDataRef} = Record;\n\n`; } - if (refs.resolversRef) { - const resolverArgsRef = refs.argsRef ?? 'Record'; - output += `export type ${refs.resolversRef} = {\n`; - for (const name of collectResolverNames(ix)) { - output += ` ${name}: ResolverFn<${resolverArgsRef}, ${refs.accountsRef}>;\n`; - } - output += '};\n\n'; - } - return output; } diff --git a/packages/dynamic-address-resolution/src/codegen/generate-types.ts b/packages/dynamic-address-resolution/src/codegen/generate-types.ts index 8a53e7acc..a96f6061e 100644 --- a/packages/dynamic-address-resolution/src/codegen/generate-types.ts +++ b/packages/dynamic-address-resolution/src/codegen/generate-types.ts @@ -6,7 +6,7 @@ import { generateResolutionInputTypes } from './generate-resolution-input-types' /** * Generate a self-contained TypeScript file with all address-resolution types for a Codama IDL: * - PDA seed types, `${Program}Pdas` map, - * - Per-instruction `${Name}Args`, `${Name}Accounts`, `${Name}Resolvers`. + * - Per-instruction `${Name}InstructionDataArgs` and `${Name}Accounts`. */ export function generateTypes(idl: RootNode): string { const { typeBlock: pdaBlock, mapTypeName } = generatePdaTypes(idl); diff --git a/packages/dynamic-address-resolution/src/codegen/get-resolution-refs.ts b/packages/dynamic-address-resolution/src/codegen/get-resolution-refs.ts index 25aac1b0e..7622c8351 100644 --- a/packages/dynamic-address-resolution/src/codegen/get-resolution-refs.ts +++ b/packages/dynamic-address-resolution/src/codegen/get-resolution-refs.ts @@ -1,7 +1,6 @@ -import { type InstructionNode, pascalCase } from 'codama'; +import { type DefinedTypeNode, type InstructionNode, isNode, pascalCase, type TypeNode } from 'codama'; import { OPTIONAL_NODE_KINDS } from '../shared/nodes'; -import { collectResolverNames } from './collect-resolver-names'; /** * Symbol registry for the resolvable surface of an instruction. @@ -10,8 +9,6 @@ import { collectResolverNames } from './collect-resolver-names'; * (and downstream consumers) should reference when emitting types tied * to address resolution. `null` means "this symbol is not emitted for * this instruction" — callers pick their own fallback at the use site. - * - * Lives here because resolution rules decide which of these symbols exist. */ export type ResolutionRefs = { /** Identifier of the emitted `${Name}Accounts` type (strict, IDL-named keys only). */ @@ -21,39 +18,37 @@ export type ResolutionRefs = { * Widened with `Record`. */ accountsWithDataRef: string; - /** Identifier of the emitted `${Name}Args` type, or `null` if no args type is emitted. */ - argsRef: string | null; - /** `true` when the instruction has any arguments (mirrors `argsRef !== null`). */ - hasArgs: boolean; - /** `true` when at least one argument is non-optional. */ - hasRequiredArgs: boolean; - /** `true` when at least one remaining-account argument is non-optional. */ - hasRequiredRemainingAccounts: boolean; - /** `true` when the instruction has any custom resolvers (mirrors `resolversRef !== null`). */ - hasResolvers: boolean; - /** Identifier of the emitted `${Name}Resolvers` type, or `null` if no resolvers type is emitted. */ - resolversRef: string | null; + /** Identifier of the emitted `${Name}InstructionDataArgs` type, or `null` if there is no data to provide. */ + dataRef: string | null; + /** `true` when the instruction has data to provide (mirrors `dataRef !== null`). */ + hasData: boolean; + /** `true` when some of the instruction data must be provided. */ + hasRequiredData: boolean; }; -export function getResolutionRefs(ix: InstructionNode): ResolutionRefs { +export function getResolutionRefs(ix: InstructionNode, definedTypes: DefinedTypeNode[] = []): ResolutionRefs { const typeName = pascalCase(ix.identifier); - - const args = (ix.arguments ?? []).filter(arg => arg.defaultValueStrategy !== 'omitted'); - const remainingAccountArgs = (ix.remainingAccounts ?? []).filter(ra => ra.value.kind === 'argumentValueNode'); - const hasArgs = args.length > 0 || remainingAccountArgs.length > 0; - const hasRequiredArgs = args.some(arg => !OPTIONAL_NODE_KINDS.includes(arg.type.kind)); - const hasRequiredRemainingAccounts = remainingAccountArgs.some(ra => !ra.isOptional); - - const hasResolvers = collectResolverNames(ix).size > 0; + const data = resolveType(ix.data, definedTypes); + // Omitted fields always encode their default value, so they are never provided. + const fields = isNode(data, 'structTypeNode') + ? (data.fields ?? []).filter(field => field.defaultValueStrategy !== 'omitted' || !field.defaultValue) + : undefined; + const hasData = data !== undefined && (fields === undefined || fields.length > 0); + const hasRequiredData = fields + ? fields.some(field => field.defaultValue === undefined && !OPTIONAL_NODE_KINDS.includes(field.type.kind)) + : data !== undefined && !OPTIONAL_NODE_KINDS.includes(data.kind); return { accountsRef: `${typeName}Accounts`, accountsWithDataRef: `${typeName}AccountsWithData`, - argsRef: hasArgs ? `${typeName}Args` : null, - hasArgs, - hasRequiredArgs, - hasRequiredRemainingAccounts, - hasResolvers, - resolversRef: hasResolvers ? `${typeName}Resolvers` : null, + dataRef: hasData ? `${typeName}InstructionDataArgs` : null, + hasData, + hasRequiredData, }; } + +/** Resolve a link to its defined type when found, keeping unresolved links as data of unknown shape. */ +function resolveType(type: TypeNode | undefined, definedTypes: DefinedTypeNode[]): TypeNode | undefined { + if (!isNode(type, 'definedTypeLinkNode')) return type; + return definedTypes.find(definedType => definedType.identifier === type.identifier)?.type ?? type; +} diff --git a/packages/dynamic-address-resolution/src/codegen/index.ts b/packages/dynamic-address-resolution/src/codegen/index.ts index ef8ce7a12..67ea1639d 100644 --- a/packages/dynamic-address-resolution/src/codegen/index.ts +++ b/packages/dynamic-address-resolution/src/codegen/index.ts @@ -1,6 +1,5 @@ export { codamaTypeToTS } from './codama-type-to-ts'; export { collectPdaNodesFromIdl } from './collect-pda-nodes'; -export { collectResolverNames } from './collect-resolver-names'; export { generatePdaTypes } from './generate-pda-types'; export { generateResolutionInputTypes } from './generate-resolution-input-types'; export { generateTypes } from './generate-types'; diff --git a/packages/dynamic-address-resolution/src/codegen/is-account-auto-resolvable.ts b/packages/dynamic-address-resolution/src/codegen/is-account-auto-resolvable.ts index 644f109a3..4ce1e39c0 100644 --- a/packages/dynamic-address-resolution/src/codegen/is-account-auto-resolvable.ts +++ b/packages/dynamic-address-resolution/src/codegen/is-account-auto-resolvable.ts @@ -1,12 +1,27 @@ -import type { InstructionAccountNode } from 'codama'; +import { + INSTRUCTION_INPUT_VALUE_NODE_KINDS, + type InstructionAccountNode, + type InstructionInputValueNode, + type InstructionNode, + ProvidedScope, +} from 'codama'; -// Accounts with these default value nodes always require user input. -const nonResolvableValueNodes = ['payerValueNode', 'identityValueNode']; +// Accounts with these default values cannot be resolved without user input or fetching accounts. +const NON_RESOLVABLE_VALUE_NODES: InstructionInputValueNode['kind'][] = [ + 'accountBumpValueNode', + 'accountDataValueNode', + 'identityValueNode', + 'payerValueNode', +]; /** - * Determines if an account has an auto-resolvable default value. + * Whether an account can be omitted from the accounts input because its + * default value resolves its address. Injected default values are resolved + * from the `provides` of the given instruction, if any. */ -export function isAccountAutoResolvable(acc: InstructionAccountNode): boolean { - if (acc.defaultValue == null) return false; - return !nonResolvableValueNodes.includes(acc.defaultValue.kind); +export function isAccountAutoResolvable(acc: InstructionAccountNode, instruction?: InstructionNode): boolean { + if (acc.defaultValue === undefined) return false; + const scope = new ProvidedScope(instruction?.provides ?? []); + const defaultValue = scope.resolve(acc.defaultValue, { kinds: INSTRUCTION_INPUT_VALUE_NODE_KINDS }); + return defaultValue !== undefined && !NON_RESOLVABLE_VALUE_NODES.includes(defaultValue.kind); } diff --git a/packages/dynamic-address-resolution/src/index.ts b/packages/dynamic-address-resolution/src/index.ts index dd2d3e21e..639cf05ee 100644 --- a/packages/dynamic-address-resolution/src/index.ts +++ b/packages/dynamic-address-resolution/src/index.ts @@ -1,17 +1,11 @@ // Resolvers export { resolveInstructionAccountAddress, resolveStandalonePda } from './resolvers'; - -// Visitors -export { - createCodecInputTransformer, - createDefaultValueEncoderVisitor, - DEFAULT_VALUE_ENCODER_SUPPORTED_NODE_KINDS, -} from './visitors'; +export type { ResolveInstructionAccountAddressInput, ResolveStandalonePdaInput } from './resolvers'; // Helpers export { isPublicKeyLike, isAddressConvertible, toAddress } from './shared/address'; export { OPTIONAL_NODE_KINDS } from './shared/nodes'; // Types -export type { AccountsInput, ArgumentsInput, ResolverFn, ResolversInput, ResolverFnInput } from './shared/types'; +export type { AccountsInput, DataInput } from './shared/types'; export type { AddressInput, PublicKeyLike } from './shared/address'; diff --git a/packages/dynamic-address-resolution/src/resolvers/context.ts b/packages/dynamic-address-resolution/src/resolvers/context.ts new file mode 100644 index 000000000..b038986cf --- /dev/null +++ b/packages/dynamic-address-resolution/src/resolvers/context.ts @@ -0,0 +1,161 @@ +import { getNodeValueCodecVisitor, getValueNodeVisitor } from '@codama/dynamic-codecs'; +import { + CODAMA_ERROR__CANNOT_RESOLVE_PATH, + CODAMA_ERROR__DYNAMIC_CLIENT__DATA_MISSING, + CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, + CodamaError, +} from '@codama/errors'; +import type { Address } from '@solana/addresses'; +import type { ReadonlyUint8Array } from '@solana/codecs'; +import { + findProgramNodeFromPath, + getLastNodeFromPath, + getRecordLinkablesVisitor, + type InstructionNode, + isNode, + LinkableDictionary, + type Node, + type NodePath, + NodeStack, + parsePath, + type PathSegment, + type PathString, + pathString, + ProvidedScope, + resolveTypePath, + type TypeNode, + type ValueNode, + visit, +} from 'codama'; + +import { toAddress } from '../shared/address'; +import type { AccountsInput, DataInput } from '../shared/types'; +import type { ResolutionContext } from './types'; + +const linkablesCache = new WeakMap(); + +/** Record, once per root node, the linkable nodes of the tree containing the given path. */ +export function getLinkables(path: NodePath): LinkableDictionary { + const root = path[0]; + let linkables = linkablesCache.get(root); + if (!linkables) { + linkables = new LinkableDictionary(); + visit(root, getRecordLinkablesVisitor(linkables)); + linkablesCache.set(root, linkables); + } + return linkables; +} + +/** Open a frame for every instruction in the path, parents first, so their `provides` resolve injected values. */ +export function getProvidedScope(path: NodePath): ProvidedScope { + return new ProvidedScope(...path.flatMap(node => (isNode(node, 'instructionNode') ? [node.provides ?? []] : []))); +} + +/** Create the resolution context of the instruction at the end of the given path. */ +export function createResolutionContext( + instructionPath: NodePath, + inputs: { accountsInput?: TAccounts; dataInput?: TData }, +): ResolutionContext { + return { + accountsInput: inputs.accountsInput, + dataInput: inputs.dataInput, + instructionPath, + linkables: getLinkables(instructionPath), + resolutionPath: [], + scope: getProvidedScope(instructionPath), + }; +} + +export function getInstruction(ctx: ResolutionContext): InstructionNode { + return getLastNodeFromPath(ctx.instructionPath); +} + +/** The address of the program defining the node at the end of the given path. */ +export function getProgramAddress(path: NodePath): Address { + const program = findProgramNodeFromPath(path); + if (!program) { + throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, { + message: `Cannot find the program of [${path[path.length - 1]?.kind}]: its path must include its program.`, + }); + } + return toAddress(program.publicKey); +} + +/** + * Get the value at the given path within the instruction data, as its codec + * would encode it. + * + * Each field along the path uses its default value the same way its codec + * encodes it: always when its `defaultValueStrategy` is `omitted`, and when + * missing from the input otherwise. For instance, `config.owner` is read from + * the default value of `config` when `config` is missing. Returns `undefined` + * when the value is missing and has no default value. + */ +export function getDataValue(ctx: ResolutionContext, path: PathString): unknown { + const instruction = getInstruction(ctx); + const segments = parsePath(path); + // Resolving the path against the data type first ensures it exists. + if (!instruction.data) { + throw new CodamaError(CODAMA_ERROR__CANNOT_RESOLVE_PATH, { + nodePath: ctx.instructionPath, + path, + segment: segments[0] ? formatPathSegment(segments[0]) : path, + }); + } + const source = [...ctx.instructionPath, instruction.data] as const; + let prefix = ''; + return segments.reduce((current, segment) => { + prefix += segment.kind === 'field' && prefix !== '' ? `.${segment.identifier}` : formatPathSegment(segment); + const typePath = resolveTypePath(source, pathString(prefix), ctx.linkables); + const key = segment.kind === 'field' ? segment.identifier : segment.index; + const value = + current === undefined || current === null ? undefined : (current as Record)[key]; + const node = getLastNodeFromPath(typePath); + if (!isNode(node, 'structFieldTypeNode') || node.defaultValue === undefined) return value; + const useDefault = node.defaultValueStrategy === 'omitted' || value === undefined; + return useDefault ? getValue(ctx, typePath.slice(0, -1), node.defaultValue) : value; + }, ctx.dataInput); +} + +function formatPathSegment(segment: PathSegment): string { + return segment.kind === 'field' ? segment.identifier : `[${segment.index}]`; +} + +/** Same as {@link getDataValue} but throws when the value is missing. */ +export function getRequiredDataValue(ctx: ResolutionContext, path: PathString): unknown { + const value = getDataValue(ctx, path); + if (value === undefined || value === null) { + throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__DATA_MISSING, { + instructionName: getInstruction(ctx).identifier, + path, + }); + } + return value; +} + +/** Get the raw value of a value node, e.g. `integerValueNode('42')` returns `42n`, resolving injections from the scope. */ +export function getValue( + ctx: Pick, + parentPath: NodePath, + node: ValueNode, +): unknown { + const valueVisitor = getValueNodeVisitor(ctx.linkables, { + scope: ctx.scope.clone(), + stack: new NodeStack(parentPath), + }); + return visit(node, valueVisitor); +} + +/** Encode a value using the codec of the given type, whose links resolve from its parent path. */ +export function encodeValue( + ctx: Pick, + parentPath: NodePath, + type: TypeNode, + value: unknown, +): ReadonlyUint8Array { + const codecVisitor = getNodeValueCodecVisitor(ctx.linkables, { + scope: ctx.scope.clone(), + stack: new NodeStack(parentPath), + }); + return visit(type, codecVisitor).encode(value); +} diff --git a/packages/dynamic-address-resolution/src/resolvers/index.ts b/packages/dynamic-address-resolution/src/resolvers/index.ts index a58b2b01e..99b98c5e6 100644 --- a/packages/dynamic-address-resolution/src/resolvers/index.ts +++ b/packages/dynamic-address-resolution/src/resolvers/index.ts @@ -3,6 +3,7 @@ export { resolveInstructionAccountAddress } from './resolve-instruction-account- export type { ResolveInstructionAccountAddressInput } from './resolve-instruction-account-address'; export { resolveAccountValueNodeAddress } from './resolve-account-value-node-address'; export { resolveConditionalValueNodeCondition } from './resolve-conditional'; -export { resolvePDAAddress } from './resolve-pda-address'; -export type { BaseResolutionContext, ResolutionPath } from './types'; +export { resolvePdaAddress } from './resolve-pda-address'; +export type { ResolutionContext, ResolutionPath } from './types'; export { resolveStandalonePda } from './resolve-standalone-pda'; +export type { ResolveStandalonePdaInput } from './resolve-standalone-pda'; diff --git a/packages/dynamic-address-resolution/src/resolvers/resolve-account-address.ts b/packages/dynamic-address-resolution/src/resolvers/resolve-account-address.ts index 779a5daa0..bad2685bd 100644 --- a/packages/dynamic-address-resolution/src/resolvers/resolve-account-address.ts +++ b/packages/dynamic-address-resolution/src/resolvers/resolve-account-address.ts @@ -1,112 +1,63 @@ import { CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING, - CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_OPTIONAL_ACCOUNT_STRATEGY, - CODAMA_ERROR__UNEXPECTED_NODE_KIND, CodamaError, } from '@codama/errors'; import type { Address } from '@solana/addresses'; -import type { InstructionAccountNode, InstructionNode, RootNode } from 'codama'; +import type { InstructionAccountNode } from 'codama'; import { visitOrElse } from 'codama'; -import { toAddress } from '../shared/address'; -import type { AccountsInput, ArgumentsInput, ResolverFnInput, ResolversInput } from '../shared/types'; import { safeStringify } from '../shared/util'; -import { - ACCOUNT_DEFAULT_VALUE_SUPPORTED_NODE_KINDS, - createAccountDefaultValueVisitor, -} from '../visitors/account-default-value'; -import type { BaseResolutionContext } from './types'; - -type ResolveAccountAddressContext< - TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, -> = BaseResolutionContext & { - ixAccountNode: InstructionAccountNode; -}; +import { createAccountDefaultValueVisitor, unexpectedAccountDefaultValueNode } from '../visitors/account-default-value'; +import { getInstruction, getProgramAddress } from './context'; +import type { ResolutionContext } from './types'; /** - * Resolves the address of an instruction account node via either defaultValue or optionalAccountStrategy. + * Resolve the address of an instruction account that is not provided, + * using its `defaultValue` or the `optionalAccountStrategy` of its instruction. */ -export async function resolveAccountAddress< - TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, ->({ - root, - ixNode, - ixAccountNode, - argumentsInput, - accountsInput, - resolutionPath, - resolversInput, -}: ResolveAccountAddressContext): Promise
{ - const accountAddressInput = accountsInput?.[ixAccountNode.identifier]; - // Optional accounts explicitly provided as null should be resolved based on optionalAccountStrategy - if (accountAddressInput === null && ixAccountNode.isOptional) { - return resolveOptionalAccountWithStrategy(root, ixNode, ixAccountNode); +export async function resolveAccountAddress( + ixAccountNode: InstructionAccountNode, + ctx: ResolutionContext, +): Promise
{ + // Optional accounts explicitly provided as null resolve using the optional account strategy. + if (ctx.accountsInput?.[ixAccountNode.identifier] === null && ixAccountNode.isOptional) { + return resolveOptionalAccountWithStrategy(ixAccountNode, ctx); } if (ixAccountNode.defaultValue) { - const visitor = createAccountDefaultValueVisitor({ - accountsInput, - argumentsInput, - ixAccountNode, - ixNode, - resolutionPath, - resolversInput, - root, - }); - - const addressValue = await visitOrElse(ixAccountNode.defaultValue, visitor, node => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...ACCOUNT_DEFAULT_VALUE_SUPPORTED_NODE_KINDS], - kind: node.kind, - node, - }); - }); + const visitor = createAccountDefaultValueVisitor(ixAccountNode, ctx); + const address = await visitOrElse(ixAccountNode.defaultValue, visitor, unexpectedAccountDefaultValueNode); - // conditionalValueNode with ifFalse branch returns null. - // This should be resolved via optionalAccountStrategy for optional accounts. - if (addressValue === null && ixAccountNode.isOptional) { - return resolveOptionalAccountWithStrategy(root, ixNode, ixAccountNode); + // A conditional default without a matching branch resolves using the optional account strategy. + if (address === null && ixAccountNode.isOptional) { + return resolveOptionalAccountWithStrategy(ixAccountNode, ctx); } - - return addressValue; + return address; } throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING, { accountName: ixAccountNode.identifier, - instructionName: ixNode.identifier, + instructionName: getInstruction(ctx).identifier, }); } /** - * Optional account resolution via instruction strategy. - * With "programId" strategy, optional accounts are resolved to programId. - * With "omitted" strategy, optional accounts must be excluded from accounts list. + * With the "programId" strategy, optional accounts resolve to the program address. + * With the "omitted" strategy, they are excluded from the account list. */ -function resolveOptionalAccountWithStrategy( - root: RootNode, - ixNode: InstructionNode, - ixAccountNode: InstructionAccountNode, -) { - if (!ixAccountNode.isOptional) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, { - message: `resolveOptionalAccountWithStrategy called for non-optional account: ${ixAccountNode.identifier}`, - }); - } - switch (ixNode.optionalAccountStrategy) { +function resolveOptionalAccountWithStrategy(ixAccountNode: InstructionAccountNode, ctx: ResolutionContext) { + const instruction = getInstruction(ctx); + switch (instruction.optionalAccountStrategy ?? 'programId') { case 'omitted': return null; case 'programId': - return toAddress(root.program.publicKey); + return getProgramAddress(ctx.instructionPath); default: throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_OPTIONAL_ACCOUNT_STRATEGY, { accountName: ixAccountNode.identifier, - instructionName: ixNode.identifier, - strategy: safeStringify(ixNode.optionalAccountStrategy), + instructionName: instruction.identifier, + strategy: safeStringify(instruction.optionalAccountStrategy), }); } } diff --git a/packages/dynamic-address-resolution/src/resolvers/resolve-account-value-node-address.ts b/packages/dynamic-address-resolution/src/resolvers/resolve-account-value-node-address.ts index 9edf2ebfb..4de863513 100644 --- a/packages/dynamic-address-resolution/src/resolvers/resolve-account-value-node-address.ts +++ b/packages/dynamic-address-resolution/src/resolvers/resolve-account-value-node-address.ts @@ -7,52 +7,34 @@ import type { Address } from '@solana/addresses'; import type { AccountValueNode } from 'codama'; import { toAddress } from '../shared/address'; -import type { AccountsInput, ArgumentsInput, ResolverFnInput, ResolversInput } from '../shared/types'; +import { getInstruction } from './context'; import { resolveAccountAddress } from './resolve-account-address'; -import type { BaseResolutionContext, ResolutionPath } from './types'; +import type { ResolutionContext, ResolutionPath } from './types'; /** - * Resolves an AccountValueNode reference to an Address. - * - * Shared logic for resolving account references across visitors: - * Checks if the user provided the account address in accountsInput. - * Finds the referenced InstructionAccountNode. - * Delegates to resolveAccountAddress for default value resolution. + * Resolve an `accountValueNode` to the address of the account it refers to: + * the provided address if any, otherwise the resolved address of that account. */ -export async function resolveAccountValueNodeAddress< - TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, ->(node: AccountValueNode, ctx: BaseResolutionContext): Promise
{ - const { accountsInput, ixNode, resolutionPath } = ctx; - - // Check if user provided the account address. - const providedAddress = accountsInput?.[node.identifier]; +export async function resolveAccountValueNodeAddress( + node: AccountValueNode, + ctx: ResolutionContext, +): Promise
{ + const providedAddress = ctx.accountsInput?.[node.identifier]; if (providedAddress !== undefined && providedAddress !== null) { return toAddress(providedAddress); } - // Find the referenced account in the instruction. - const referencedIxAccountNode = (ixNode.accounts ?? []).find(acc => acc.identifier === node.identifier); - if (!referencedIxAccountNode) { + const instruction = getInstruction(ctx); + const account = (instruction.accounts ?? []).find(account => account.identifier === node.identifier); + if (!account) { throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__NODE_REFERENCE_NOT_FOUND, { - instructionName: ixNode.identifier, + instructionName: instruction.identifier, referencedName: node.identifier, }); } - // Detect circular dependencies before recursing. - detectCircularDependency(node.identifier, resolutionPath); - - return await resolveAccountAddress({ - accountsInput: ctx.accountsInput, - argumentsInput: ctx.argumentsInput, - ixAccountNode: referencedIxAccountNode, - ixNode, - resolutionPath: [...resolutionPath, node.identifier], - resolversInput: ctx.resolversInput, - root: ctx.root, - }); + detectCircularDependency(node.identifier, ctx.resolutionPath); + return await resolveAccountAddress(account, { ...ctx, resolutionPath: [...ctx.resolutionPath, node.identifier] }); } export function detectCircularDependency(nodeName: string, resolutionPath: ResolutionPath) { diff --git a/packages/dynamic-address-resolution/src/resolvers/resolve-conditional.ts b/packages/dynamic-address-resolution/src/resolvers/resolve-conditional.ts index 5769c5329..cca192cd7 100644 --- a/packages/dynamic-address-resolution/src/resolvers/resolve-conditional.ts +++ b/packages/dynamic-address-resolution/src/resolvers/resolve-conditional.ts @@ -1,95 +1,35 @@ -import { - CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, - CODAMA_ERROR__UNEXPECTED_NODE_KIND, - CodamaError, -} from '@codama/errors'; +import { CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, CodamaError } from '@codama/errors'; import type { ConditionalValueNode, InstructionAccountNode, InstructionInputValueNode } from 'codama'; -import { isNode, visitOrElse } from 'codama'; +import { visitOrElse } from 'codama'; -import type { AccountsInput, ArgumentsInput, ResolverFnInput, ResolversInput } from '../shared/types'; -import { getMaybeNodeKind } from '../shared/util'; -import { CONDITION_NODE_SUPPORTED_NODE_KINDS, createConditionNodeValueVisitor } from '../visitors/condition-node-value'; -import { createValueNodeVisitor, VALUE_NODE_SUPPORTED_NODE_KINDS } from '../visitors/value-node-value'; -import type { BaseResolutionContext } from './types'; - -export type ResolveConditionalContext< - TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, -> = BaseResolutionContext & { - conditionalValueNode: ConditionalValueNode; - ixAccountNode: InstructionAccountNode; -}; +import { isValueEqual } from '../shared/util'; +import { createConditionNodeValueVisitor, unexpectedConditionNode } from '../visitors/condition-node-value'; +import { getInstruction, getValue } from './context'; +import type { ResolutionContext } from './types'; /** - * Evaluates a ConditionalValueNode's condition. - * Returns the matching branch (ifTrue or ifFalse) as an InstructionInputValueNode or undefined if no branch matches. + * Evaluate the condition of a `conditionalValueNode` and return the matching + * branch, or `undefined` when that branch is not defined. + * + * With a `value`, the condition passes when it equals that value, e.g. `2` + * equals `integerValueNode('2')`. Without a `value`, it passes when the + * referenced account or data exists. */ -export async function resolveConditionalValueNodeCondition< - TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, ->({ - root, - ixNode, - ixAccountNode, - conditionalValueNode, - argumentsInput, - accountsInput, - resolutionPath, - resolversInput, -}: ResolveConditionalContext): Promise { - if (!isNode(conditionalValueNode, 'conditionalValueNode')) { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: ['conditionalValueNode'], - kind: getMaybeNodeKind(conditionalValueNode), - node: conditionalValueNode, - }); - } - const { condition, value: expectedValueNode, ifTrue, ifFalse } = conditionalValueNode; - - if (!expectedValueNode && !ifTrue && !ifFalse) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, { - message: `Invalid conditionalValueNode: missing value and branches for account ${ixAccountNode.identifier} in ${ixNode.identifier}`, - }); - } - - // Resolve the condition value of ConditionalValueNode. - const conditionVisitor = createConditionNodeValueVisitor({ - accountsInput, - argumentsInput, - ixNode, - resolutionPath, - resolversInput, - root, - }); - const actualProvidedValue = await visitOrElse(condition, conditionVisitor, condNode => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...CONDITION_NODE_SUPPORTED_NODE_KINDS], - kind: condNode.kind, - node: condNode, - }); - }); - - if (!expectedValueNode) { - return actualProvidedValue ? ifTrue : ifFalse; - } - - // If expectedValueNode exists, the condition must be equal to expected value. - const valueVisitor = createValueNodeVisitor(); - const expectedValue = visitOrElse(expectedValueNode, valueVisitor, valueNode => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...VALUE_NODE_SUPPORTED_NODE_KINDS], - kind: valueNode.kind, - node: valueNode, - }); - }); - - if (typeof expectedValue.value === 'object' || typeof actualProvidedValue === 'object') { +export async function resolveConditionalValueNodeCondition( + conditionalValueNode: ConditionalValueNode, + ixAccountNode: InstructionAccountNode, + ctx: ResolutionContext, +): Promise { + const { condition, value, ifTrue, ifFalse } = conditionalValueNode; + if (!value && !ifTrue && !ifFalse) { throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, { - message: 'Deep equality comparison not yet supported for conditional value', + message: `Invalid conditionalValueNode: missing value and branches for account ${ixAccountNode.identifier} in ${getInstruction(ctx).identifier}`, }); } - return actualProvidedValue === expectedValue.value ? ifTrue : ifFalse; + const actual = await visitOrElse(condition, createConditionNodeValueVisitor(ctx), unexpectedConditionNode); + const passes = value + ? isValueEqual(actual, getValue(ctx, ctx.instructionPath, value)) + : actual !== undefined && actual !== null; + return passes ? ifTrue : ifFalse; } diff --git a/packages/dynamic-address-resolution/src/resolvers/resolve-constant-pda-seed-value.ts b/packages/dynamic-address-resolution/src/resolvers/resolve-constant-pda-seed-value.ts deleted file mode 100644 index 79d1269d1..000000000 --- a/packages/dynamic-address-resolution/src/resolvers/resolve-constant-pda-seed-value.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { ReadonlyUint8Array } from '@solana/codecs'; -import type { ProgramIdValueNode, ValueNode } from 'codama'; -import { visitOrElse } from 'codama'; - -import { - type ConstantPdaSeedValueVisitorContext, - createConstantPdaSeedValueVisitor, - unexpectedConstantPdaSeedNodeFallback, -} from '../visitors'; - -/** - * Resolves a constant PDA seed value to raw bytes. Facade over `createConstantPdaSeedValueVisitor`. - */ -export async function resolveConstantPdaSeedValue( - node: ProgramIdValueNode | ValueNode, - ctx: ConstantPdaSeedValueVisitorContext, -): Promise { - const visitor = createConstantPdaSeedValueVisitor(ctx); - return await visitOrElse(node, visitor, unexpectedConstantPdaSeedNodeFallback); -} diff --git a/packages/dynamic-address-resolution/src/resolvers/resolve-instruction-account-address.ts b/packages/dynamic-address-resolution/src/resolvers/resolve-instruction-account-address.ts index 99b9bae85..0b4081fc6 100644 --- a/packages/dynamic-address-resolution/src/resolvers/resolve-instruction-account-address.ts +++ b/packages/dynamic-address-resolution/src/resolvers/resolve-instruction-account-address.ts @@ -1,69 +1,75 @@ -import { CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING, CodamaError } from '@codama/errors'; +import { + CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING, + CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, + CodamaError, +} from '@codama/errors'; import type { Address } from '@solana/addresses'; -import type { InstructionAccountNode, InstructionNode, RootNode } from 'codama'; +import { getLastNodeFromPath, getNodePathUntilLastNode, type InstructionAccountNode, type NodePath } from 'codama'; import { toAddress } from '../shared/address'; -import type { AccountsInput, ArgumentsInput, ResolverFnInput, ResolversInput } from '../shared/types'; +import type { AccountsInput, DataInput } from '../shared/types'; +import { createResolutionContext } from './context'; import { resolveAccountAddress } from './resolve-account-address'; export type ResolveInstructionAccountAddressInput< TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, + TData extends DataInput = DataInput, > = { accountsInput?: TAccounts; - argumentsInput?: TArgs; - ixAccountNode: InstructionAccountNode; - ixNode: InstructionNode; - resolversInput?: TResolvers; - root: RootNode; + dataInput?: TData; + /** The path of the instruction account from the root node, e.g. `[root, program, instruction, account]`. */ + path: NodePath; }; +/** + * Resolve the address of an instruction account: the provided address if + * any, otherwise its default value or its instruction's optional account + * strategy. + * + * | Account | Input | Resolves to | + * | --- | --- | --- | + * | Without default | `undefined` or `null` | throws, unless optional and `null` | + * | With default | `undefined` | its default value | + * | Optional | `null` | its `optionalAccountStrategy` | + * + * @example + * ```ts + * const address = await resolveInstructionAccountAddress({ + * accountsInput: { authority }, + * dataInput: { amount: 42n }, + * path: [root, root.program, instruction, vaultAccount], + * }); + * ``` + */ export async function resolveInstructionAccountAddress< TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, + TData extends DataInput = DataInput, >({ accountsInput, - argumentsInput, - ixAccountNode, - ixNode, - resolversInput, - root, -}: ResolveInstructionAccountAddressInput): Promise
{ - const accountAddressInput = accountsInput?.[ixAccountNode.identifier]; - const isAccountProvided = accountAddressInput !== undefined && accountAddressInput !== null; + dataInput, + path, +}: ResolveInstructionAccountAddressInput): Promise
{ + const ixAccountNode = getLastNodeFromPath(path); + const instructionPath = getNodePathUntilLastNode(path, 'instructionNode'); + if (!instructionPath) { + throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, { + message: `The path of account [${ixAccountNode.identifier}] must include its instruction.`, + }); + } - // Accounts values (with default or with optionalAccountStrategy) can be omitted, as they are auto-resolved. - // [AccountStatus + HasDefaultValue + UserInput]: - // - Required + no default + undefined: throws. - // - Required + no default + null: throws. - // - Required + default + undefined: resolves from default value. - // - Required + default + null: resolves from default value. - // - Optional + no default + undefined: throws. - // - Optional + no default + null: resolves from optionalAccountStrategy (programId or omitted). - // - Optional + default + undefined: resolves from default value. - // - Optional + default + null: resolves from optionalAccountStrategy (programId or omitted). - const canAutoResolve = !!ixAccountNode.defaultValue || (ixAccountNode.isOptional && accountAddressInput === null); + const addressInput = accountsInput?.[ixAccountNode.identifier]; + if (addressInput !== undefined && addressInput !== null) { + return toAddress(addressInput); + } - if (!isAccountProvided && !canAutoResolve) { + const canAutoResolve = !!ixAccountNode.defaultValue || (ixAccountNode.isOptional && addressInput === null); + if (!canAutoResolve) { throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING, { accountName: ixAccountNode.identifier, - instructionName: ixNode.identifier, + instructionName: getLastNodeFromPath(instructionPath).identifier, }); } - if (isAccountProvided) { - return toAddress(accountAddressInput); - } - - return await resolveAccountAddress({ - accountsInput, - argumentsInput, - ixAccountNode, - ixNode, - resolutionPath: [], - resolversInput, - root, - }); + const ctx = createResolutionContext(instructionPath, { accountsInput, dataInput }); + return await resolveAccountAddress(ixAccountNode, ctx); } diff --git a/packages/dynamic-address-resolution/src/resolvers/resolve-pda-address.ts b/packages/dynamic-address-resolution/src/resolvers/resolve-pda-address.ts index 91777aaa0..2e3637603 100644 --- a/packages/dynamic-address-resolution/src/resolvers/resolve-pda-address.ts +++ b/packages/dynamic-address-resolution/src/resolvers/resolve-pda-address.ts @@ -1,239 +1,119 @@ import { - CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, + CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_DERIVE_PDA, CODAMA_ERROR__DYNAMIC_CLIENT__NODE_REFERENCE_NOT_FOUND, - CODAMA_ERROR__LINKED_NODE_NOT_FOUND, - CODAMA_ERROR__UNEXPECTED_NODE_KIND, - CODAMA_ERROR__UNRECOGNIZED_NODE_KIND, + CODAMA_ERROR__DYNAMIC_CLIENT__PDA_SEED_MISSING, CodamaError, } from '@codama/errors'; import type { Address, ProgramDerivedAddress } from '@solana/addresses'; -import { address, getProgramDerivedAddress } from '@solana/addresses'; +import { getProgramDerivedAddress } from '@solana/addresses'; import type { ReadonlyUint8Array } from '@solana/codecs'; -import type { Node, PdaNode, PdaSeedValueNode, PdaValueNode, RegisteredPdaSeedNode, VariablePdaSeedNode } from 'codama'; -import { isNode, visitOrElse } from 'codama'; - -import type { AccountsInput, ArgumentsInput, ResolverFnInput, ResolversInput } from '../shared/types'; -import { getMaybeNodeKind } from '../shared/util'; -import { createPdaSeedValueVisitor, PDA_SEED_VALUE_SUPPORTED_NODE_KINDS } from '../visitors/pda-seed-value'; -import type { BaseResolutionContext } from './types'; - -export type ResolvePDAAddressContext< - TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, -> = BaseResolutionContext & { - pdaValueNode: PdaValueNode; -}; +import { + getLastNodeFromPath, + type IdentifierString, + isNode, + type NodePath, + type PdaNode, + type PdaValueNode, + type VariablePdaSeedNode, +} from 'codama'; + +import { toAddress, toAddressOrThrow } from '../shared/address'; +import { OPTIONAL_NODE_KINDS } from '../shared/nodes'; +import { resolvePdaSeedValue } from '../visitors/pda-seed-value'; +import { encodeValue, getInstruction, getProgramAddress, getRequiredDataValue, getValue } from './context'; +import { resolveAccountValueNodeAddress } from './resolve-account-value-node-address'; +import type { ResolutionContext } from './types'; /** - * Derives a PDA from a PdaValueNode. - * Encodes each seed (ConstantPdaSeedNode and VariablePdaSeedNode) into bytes and computes the address. + * Derive the PDA of a `pdaValueNode`, using the program address it provides, + * or the one of the PDA, or the address of the program defining the PDA. */ -export async function resolvePDAAddress< - TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, ->({ - root, - ixNode, - argumentsInput, - accountsInput, - pdaValueNode, - resolutionPath, - resolversInput, -}: ResolvePDAAddressContext): Promise { - if (!isNode(pdaValueNode, 'pdaValueNode')) { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: ['pdaValueNode'], - kind: getMaybeNodeKind(pdaValueNode), - node: pdaValueNode, - }); - } - - const pdaNode = resolvePdaNode(pdaValueNode, root.program.pdas ?? []); - const programId = address(pdaNode.programId || root.program.publicKey); - - const seedValues = await Promise.all( - (pdaNode.seeds ?? []).map(async seedNode => { - if (seedNode.kind === 'constantPdaSeedNode') { - return await resolveConstantPdaSeed({ - accountsInput, - argumentsInput, - ixNode, - programId, - resolutionPath, - resolversInput, - root, - seedNode, - }); - } - - if (seedNode.kind === 'variablePdaSeedNode') { - const variableSeedValueNodes = pdaValueNode.seeds ?? []; - const seedName = seedNode.identifier; - const variableSeedValueNode = variableSeedValueNodes.find(node => node.identifier === seedName); - - if (!variableSeedValueNode) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__NODE_REFERENCE_NOT_FOUND, { - instructionName: ixNode.identifier, - referencedName: seedName, - }); - } - - return await resolveVariablePdaSeed({ - accountsInput, - argumentsInput, - ixNode, - programId, - resolutionPath, - resolversInput, - root, - seedNode, - variableSeedValueNode, - }); - } - - throw new CodamaError(CODAMA_ERROR__UNRECOGNIZED_NODE_KIND, { - kind: getMaybeNodeKind(seedNode) ?? 'unknown', +export async function resolvePdaAddress( + node: PdaValueNode, + ctx: ResolutionContext, + accountName: IdentifierString, +): Promise { + const pdaPath = getPdaPath(node, ctx); + const programAddress = node.programId + ? await resolvePdaProgramAddress(node.programId, ctx, accountName) + : getPdaProgramAddress(pdaPath); + + return await derivePda(pdaPath, programAddress, ctx, async seedNode => { + const seedValueNode = (node.seeds ?? []).find(seed => seed.identifier === seedNode.identifier); + if (!seedValueNode) { + throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__NODE_REFERENCE_NOT_FOUND, { + instructionName: getInstruction(ctx).identifier, + referencedName: seedNode.identifier, }); - }), - ); - - return await getProgramDerivedAddress({ - programAddress: programId, - seeds: seedValues, + } + return await resolvePdaSeedValue(seedValueNode.value, ctx); }); } -function resolvePdaNode(pdaDefaultValue: PdaValueNode, pdas: PdaNode[]): PdaNode { - if (isNode(pdaDefaultValue.pda, 'pdaLinkNode')) { - const linkedPda = pdas.find(p => p.identifier === pdaDefaultValue.pda.identifier); - if (!linkedPda) { - throw new CodamaError(CODAMA_ERROR__LINKED_NODE_NOT_FOUND, { - kind: 'pdaLinkNode', - linkNode: pdaDefaultValue.pda, - name: pdaDefaultValue.pda.identifier, - path: [], - }); - } - return linkedPda; - } - - if (isNode(pdaDefaultValue.pda, 'pdaNode')) { - return pdaDefaultValue.pda; +/** The path of the PDA of a `pdaValueNode`, following links. Inline PDAs belong to the program of the instruction. */ +function getPdaPath(node: PdaValueNode, ctx: ResolutionContext): NodePath { + if (isNode(node.pda, 'pdaLinkNode')) { + return ctx.linkables.getPathOrThrow([...ctx.instructionPath, node, node.pda]); } + return [...ctx.instructionPath, node, node.pda]; +} - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: ['pdaLinkNode', 'pdaNode'], - kind: getMaybeNodeKind(pdaDefaultValue.pda), - node: pdaDefaultValue.pda, - }); +/** The program address used to derive a PDA when its value does not provide one. */ +export function getPdaProgramAddress(pdaPath: NodePath): Address { + const pdaNode = getLastNodeFromPath(pdaPath); + return pdaNode.programId ? toAddress(pdaNode.programId) : getProgramAddress(pdaPath); } -type ResolvePdaSeedContext< - TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, -> = BaseResolutionContext & { - programId: Address; - seedNode: VariablePdaSeedNode; - variableSeedValueNode: PdaSeedValueNode; -}; -function resolveVariablePdaSeed< - TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, ->({ - accountsInput, - argumentsInput, - ixNode, - programId, - resolutionPath, - resolversInput, - root, - seedNode, - variableSeedValueNode, -}: ResolvePdaSeedContext): Promise { - if (!isNode(variableSeedValueNode, 'pdaSeedValueNode')) { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: ['pdaSeedValueNode'], - kind: getMaybeNodeKind(variableSeedValueNode), - node: variableSeedValueNode as Node, - }); +async function resolvePdaProgramAddress( + programId: NonNullable, + ctx: ResolutionContext, + accountName: IdentifierString, +): Promise
{ + if (isNode(programId, 'dataValueNode')) { + return toAddressOrThrow(getRequiredDataValue(ctx, programId.path), accountName); } - - if (seedNode.identifier !== variableSeedValueNode.identifier) { - // Sanity check: this should not happen. - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, { - message: `Mismatched PDA seed names: expected [${seedNode.identifier}], got [${variableSeedValueNode.identifier}]`, + const address = await resolveAccountValueNodeAddress(programId, ctx); + if (address === null) { + throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_DERIVE_PDA, { + accountName: programId.identifier, }); } - - const visitor = createPdaSeedValueVisitor({ - accountsInput, - argumentsInput, - ixNode, - programId, - resolutionPath, - resolversInput, - root, - seedTypeNode: seedNode.type, - }); - - return visitOrElse(variableSeedValueNode.value, visitor, node => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...PDA_SEED_VALUE_SUPPORTED_NODE_KINDS], - kind: node.kind, - node, - }); - }); + return address; } -type ResolveConstantPdaSeedContext< - TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, -> = BaseResolutionContext & { - programId: Address; - seedNode: RegisteredPdaSeedNode; -}; -function resolveConstantPdaSeed< - TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, ->({ - accountsInput, - argumentsInput, - ixNode, - programId, - resolutionPath, - resolversInput, - root, - seedNode, -}: ResolveConstantPdaSeedContext): Promise { - if (!isNode(seedNode, 'constantPdaSeedNode')) { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: ['constantPdaSeedNode'], - kind: seedNode.kind, - node: seedNode, - }); - } - - const visitor = createPdaSeedValueVisitor({ - accountsInput, - argumentsInput, - ixNode, - programId, - resolutionPath, - resolversInput, - root, - seedTypeNode: seedNode.type, - }); - return visitOrElse(seedNode.value, visitor, node => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...PDA_SEED_VALUE_SUPPORTED_NODE_KINDS], - kind: node.kind, - node, - }); - }); +/** + * Derive a PDA by encoding each of its seeds using its declared type. + * Constant seeds use their value, and variable seeds use the value returned by `getVariableSeedValue`. + */ +export async function derivePda( + pdaPath: NodePath, + programAddress: Address, + ctx: Pick, + getVariableSeedValue: (seedNode: VariablePdaSeedNode) => Promise, +): Promise { + const pdaNode = getLastNodeFromPath(pdaPath); + const seeds = await Promise.all( + (pdaNode.seeds ?? []).map(async (seedNode): Promise => { + const seedPath = [...pdaPath, seedNode]; + if (isNode(seedNode, 'constantPdaSeedNode')) { + const value = isNode(seedNode.value, 'programIdValueNode') + ? programAddress + : getValue(ctx, seedPath, seedNode.value); + return encodeValue(ctx, seedPath, seedNode.type, value); + } + const value = await getVariableSeedValue(seedNode); + if (value === undefined || value === null) { + // Missing option seeds encode as `None`, e.g. to zero bytes for remainder options. + if (OPTIONAL_NODE_KINDS.includes(seedNode.type.kind)) { + return encodeValue(ctx, seedPath, seedNode.type, null); + } + throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__PDA_SEED_MISSING, { + pdaName: pdaNode.identifier, + seedName: seedNode.identifier, + }); + } + return encodeValue(ctx, seedPath, seedNode.type, value); + }), + ); + return await getProgramDerivedAddress({ programAddress, seeds }); } diff --git a/packages/dynamic-address-resolution/src/resolvers/resolve-standalone-pda.ts b/packages/dynamic-address-resolution/src/resolvers/resolve-standalone-pda.ts index fc7c9601c..28d57fe4d 100644 --- a/packages/dynamic-address-resolution/src/resolvers/resolve-standalone-pda.ts +++ b/packages/dynamic-address-resolution/src/resolvers/resolve-standalone-pda.ts @@ -1,109 +1,34 @@ -import { getNodeCodec } from '@codama/dynamic-codecs'; -import { - CODAMA_ERROR__DYNAMIC_CLIENT__ARGUMENT_MISSING, - CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE, - CODAMA_ERROR__UNEXPECTED_NODE_KIND, - CODAMA_ERROR__UNRECOGNIZED_NODE_KIND, - CodamaError, -} from '@codama/errors'; -import type { Address, ProgramDerivedAddress } from '@solana/addresses'; -import { getProgramDerivedAddress } from '@solana/addresses'; -import type { ReadonlyUint8Array } from '@solana/codecs'; -import type { PdaNode, RegisteredPdaSeedNode, RootNode, VariablePdaSeedNode } from 'codama'; -import { camelCase, isNode } from 'codama'; +import type { ProgramDerivedAddress } from '@solana/addresses'; +import type { NodePath, PdaNode } from 'codama'; +import { ProvidedScope } from 'codama'; -import { toAddress } from '../shared/address'; -import { getMemoizedUtf8Encoder } from '../shared/codecs'; -import { formatValueType, getMaybeNodeKind } from '../shared/util'; -import { createCodecInputTransformer } from '../visitors/codec-input-transformer'; -import { resolveConstantPdaSeedValue } from './resolve-constant-pda-seed-value'; +import { getLinkables } from './context'; +import { derivePda, getPdaProgramAddress } from './resolve-pda-address'; + +export type ResolveStandalonePdaInput = { + /** The path of the PDA from the root node, e.g. `[root, program, pda]`. */ + path: NodePath; + /** The values of the variable seeds of the PDA, keyed by seed identifier. */ + seedsInput?: Record; +}; /** - * Derives a PDA from a standalone `PdaNode` and user-supplied seed values, - * without requiring an instruction context. + * Derive a PDA from its seed values, outside of any instruction. + * + * @example + * ```ts + * const [address, bump] = await resolveStandalonePda({ + * path: [root, root.program, metadataPda], + * seedsInput: { authority, seed: 'idl' }, + * }); + * ``` */ -export async function resolveStandalonePda( - root: RootNode, - pdaNode: PdaNode, - seedInputs: Record = {}, -): Promise { - const programAddress = toAddress(pdaNode.programId || root.program.publicKey); - const seedValues = await Promise.all( - (pdaNode.seeds ?? []).map(async (seedNode): Promise => { - if (seedNode.kind === 'constantPdaSeedNode') { - return await resolveStandaloneConstantSeed(programAddress, seedNode); - } - if (seedNode.kind === 'variablePdaSeedNode') { - return await resolveStandaloneVariableSeed(root, seedNode, seedInputs); - } - throw new CodamaError(CODAMA_ERROR__UNRECOGNIZED_NODE_KIND, { - kind: getMaybeNodeKind(seedNode) ?? 'unknown', - }); - }), +export async function resolveStandalonePda({ + path, + seedsInput = {}, +}: ResolveStandalonePdaInput): Promise { + const ctx = { linkables: getLinkables(path), scope: new ProvidedScope() }; + return await derivePda(path, getPdaProgramAddress(path), ctx, seedNode => + Promise.resolve(seedsInput[seedNode.identifier]), ); - - return await getProgramDerivedAddress({ programAddress, seeds: seedValues }); -} - -function resolveStandaloneConstantSeed( - programAddress: Address, - seedNode: RegisteredPdaSeedNode, -): Promise { - if (!isNode(seedNode, 'constantPdaSeedNode')) { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: ['constantPdaSeedNode'], - kind: seedNode.kind, - node: seedNode, - }); - } - return resolveConstantPdaSeedValue(seedNode.value, { programId: programAddress }); -} - -function resolveStandaloneVariableSeed( - root: RootNode, - seedNode: VariablePdaSeedNode, - seedInputs: Record, -): Promise { - const input = seedInputs[seedNode.identifier]; - const typeNode = seedNode.type; - - // remainderOptionTypeNode seeds are optional — null means zero bytes. - if (input === undefined || input === null) { - if (isNode(typeNode, 'remainderOptionTypeNode')) { - return Promise.resolve(new Uint8Array(0)); - } - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__ARGUMENT_MISSING, { - argumentName: seedNode.identifier, - instructionName: camelCase('standaloneSeedNode'), - }); - } - - // For simple string seeds encode directly with UTF-8 (no length prefix) - if (isNode(typeNode, 'stringTypeNode')) { - if (typeof input !== 'string') { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE, { - actualType: formatValueType(input), - expectedType: 'string', - nodeKind: 'stringTypeNode', - }); - } - return Promise.resolve(getMemoizedUtf8Encoder().encode(input)); - } - - // Create a synthetic instructionArgumentNode so getNodeCodec can resolve the type. - // The seed's declared type is used directly (no size-prefix wrapper). - const syntheticArgNode = createSyntheticArgNode(seedNode); - const codec = getNodeCodec([root, root.program, syntheticArgNode]); - const transformer = createCodecInputTransformer(typeNode, root, { bytesEncoding: 'base16' }); - const transformedInput = transformer(input); - return Promise.resolve(codec.encode(transformedInput)); -} - -function createSyntheticArgNode(seedNode: VariablePdaSeedNode) { - return { - docs: [] as string[], - kind: 'instructionArgumentNode' as const, - identifier: seedNode.identifier, - type: seedNode.type, - }; } diff --git a/packages/dynamic-address-resolution/src/resolvers/types.ts b/packages/dynamic-address-resolution/src/resolvers/types.ts index 9593ed287..395702386 100644 --- a/packages/dynamic-address-resolution/src/resolvers/types.ts +++ b/packages/dynamic-address-resolution/src/resolvers/types.ts @@ -1,23 +1,22 @@ -import type { InstructionNode, RootNode } from 'codama'; +import type { InstructionNode, LinkableDictionary, NodePath, ProvidedScope } from 'codama'; -import type { AccountsInput, ArgumentsInput, ResolverFnInput, ResolversInput } from '../shared/types'; +import type { AccountsInput, DataInput } from '../shared/types'; -// Array of node names being resolved to detect circular dependencies. +/** The identifiers of the accounts being resolved, used to detect circular dependencies. */ export type ResolutionPath = readonly string[]; /** - * Shared context threaded through the account/PDA resolution pipeline. - * Individual resolvers/visitors extend this with domain-specific fields. + * Shared context threaded through the account and PDA resolution pipeline. + * Individual resolvers and visitors extend it with domain-specific fields. */ -export type BaseResolutionContext< - TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, -> = { +export type ResolutionContext = { accountsInput: TAccounts | undefined; - argumentsInput: TArgs | undefined; - ixNode: InstructionNode; + dataInput: TData | undefined; + /** The path of the instruction whose accounts are resolved, from the root node. */ + instructionPath: NodePath; + /** Used to follow links, e.g. to PDAs, defined types or programs. */ + linkables: LinkableDictionary; resolutionPath: ResolutionPath; - resolversInput: TResolvers | undefined; - root: RootNode; + /** The values provided by the instruction and its parents, used to resolve injected values. */ + scope: ProvidedScope; }; diff --git a/packages/dynamic-address-resolution/src/shared/address.ts b/packages/dynamic-address-resolution/src/shared/address.ts index 7905d60ef..12af93862 100644 --- a/packages/dynamic-address-resolution/src/shared/address.ts +++ b/packages/dynamic-address-resolution/src/shared/address.ts @@ -1,8 +1,13 @@ -import { CODAMA_ERROR__DYNAMIC_CLIENT__CANNOT_CONVERT_TO_ADDRESS, CodamaError } from '@codama/errors'; +import { + CODAMA_ERROR__DYNAMIC_CLIENT__CANNOT_CONVERT_TO_ADDRESS, + CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ADDRESS_TYPE, + CodamaError, +} from '@codama/errors'; import type { Address } from '@solana/addresses'; import { address, isAddress } from '@solana/addresses'; +import type { IdentifierString } from 'codama'; -import { safeStringify } from './util'; +import { formatValueType, safeStringify } from './util'; /** * Accept both modern Address strings and legacy PublicKey-like objects. @@ -26,6 +31,21 @@ export function toAddress(input: AddressInput): Address { }); } +/** + * Convert a value to the address of the given account, throwing + * `UNEXPECTED_ADDRESS_TYPE` when it is not address convertible. + */ +export function toAddressOrThrow(value: unknown, accountName: IdentifierString): Address { + if (!isAddressConvertible(value)) { + throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ADDRESS_TYPE, { + accountName, + actualType: formatValueType(value), + expectedType: 'Address | PublicKey', + }); + } + return toAddress(value); +} + export function isAddressConvertible(value: unknown): value is AddressInput { if (value == null) return false; return isPublicKeyLike(value) || (typeof value === 'string' && isAddress(value)); diff --git a/packages/dynamic-address-resolution/src/shared/bytes-encoding.ts b/packages/dynamic-address-resolution/src/shared/bytes-encoding.ts deleted file mode 100644 index 81f4ebaf3..000000000 --- a/packages/dynamic-address-resolution/src/shared/bytes-encoding.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { CODAMA_ERROR__UNRECOGNIZED_BYTES_ENCODING, CodamaError } from '@codama/errors'; -import type { BytesEncoding } from 'codama'; - -import { getMemoizedBase16Codec, getMemoizedBase58Codec, getMemoizedBase64Codec, getMemoizedUtf8Codec } from './codecs'; -import { safeStringify } from './util'; - -/** - * Converts Uint8Array to encoded string based on encoding type. - * Uses @solana/codecs encoders internally for consistent encoding/decoding. - */ -export function uint8ArrayToEncodedString(bytes: Uint8Array, encoding: BytesEncoding): string { - const codec = getCodecFromBytesEncoding(encoding); - return codec.decode(bytes); -} - -/** - * Gets the appropriate codec for a given bytes encoding format. - */ -export function getCodecFromBytesEncoding(encoding: BytesEncoding) { - switch (encoding) { - case 'base16': - return getMemoizedBase16Codec(); - case 'base58': - return getMemoizedBase58Codec(); - case 'base64': - return getMemoizedBase64Codec(); - case 'utf8': - return getMemoizedUtf8Codec(); - default: - throw new CodamaError(CODAMA_ERROR__UNRECOGNIZED_BYTES_ENCODING, { - encoding: safeStringify(encoding as unknown), - }); - } -} -/** - * Type guard to check if a value is a Uint8Array. - */ -export function isUint8Array(value: unknown): value is Uint8Array { - return value instanceof Uint8Array; -} diff --git a/packages/dynamic-address-resolution/src/shared/codecs.ts b/packages/dynamic-address-resolution/src/shared/codecs.ts deleted file mode 100644 index 39423f8b2..000000000 --- a/packages/dynamic-address-resolution/src/shared/codecs.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { getAddressEncoder } from '@solana/addresses'; -import type { Encoder } from '@solana/codecs'; -import { - getBase16Codec, - getBase58Codec, - getBase64Codec, - getBooleanEncoder, - getUtf8Codec, - getUtf8Encoder, -} from '@solana/codecs'; - -// Memoized encoders and codecs to avoid unnecessary re-instantiation. - -let addressEncoder: ReturnType | undefined; -export function getMemoizedAddressEncoder() { - if (!addressEncoder) addressEncoder = getAddressEncoder(); - return addressEncoder; -} - -let utf8Encoder: ReturnType | undefined; -export function getMemoizedUtf8Encoder() { - if (!utf8Encoder) utf8Encoder = getUtf8Encoder(); - return utf8Encoder; -} - -let booleanEncoder: Encoder | undefined; -export function getMemoizedBooleanEncoder() { - if (!booleanEncoder) booleanEncoder = getBooleanEncoder(); - return booleanEncoder; -} - -let utf8Codec: ReturnType | undefined; -export function getMemoizedUtf8Codec() { - if (!utf8Codec) utf8Codec = getUtf8Codec(); - return utf8Codec; -} - -let base16Codec: ReturnType | undefined; -export function getMemoizedBase16Codec() { - if (!base16Codec) base16Codec = getBase16Codec(); - return base16Codec; -} - -let base58Codec: ReturnType | undefined; -export function getMemoizedBase58Codec() { - if (!base58Codec) base58Codec = getBase58Codec(); - return base58Codec; -} - -let base64Codec: ReturnType | undefined; -export function getMemoizedBase64Codec() { - if (!base64Codec) base64Codec = getBase64Codec(); - return base64Codec; -} diff --git a/packages/dynamic-address-resolution/src/shared/types.ts b/packages/dynamic-address-resolution/src/shared/types.ts index 0b136e88a..5272e5765 100644 --- a/packages/dynamic-address-resolution/src/shared/types.ts +++ b/packages/dynamic-address-resolution/src/shared/types.ts @@ -1,20 +1,10 @@ import type { AddressInput } from './address'; +/** The addresses provided for the accounts of an instruction, keyed by account identifier. */ export type AccountsInput = Partial>; -export type ArgumentsInput = Partial>; - -// Custom Resolvers for ResolverValueNode -export type ResolverFn = ( - argumentsInput: TArgs, - accountsInput: TAccounts, -) => Promise; -export type ResolversInput = Record; /** - * Generic constraint for resolver-record type parameters across the address-resolution pipeline. - * Uses `ResolverFn` to bypass the contravariance — a narrowed - * `ResolverFn` would not satisfy `Record>`, - * but it does satisfy `Record>`. + * The data provided for an instruction, as accepted by the codec of its `data` + * (see `getNodeValueCodec` in `@codama/dynamic-codecs`), e.g. `{ amount: 42n }`. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- see comment above -export type ResolverFnInput = Record>; +export type DataInput = unknown; diff --git a/packages/dynamic-address-resolution/src/shared/util.ts b/packages/dynamic-address-resolution/src/shared/util.ts index 981383931..85c2837f4 100644 --- a/packages/dynamic-address-resolution/src/shared/util.ts +++ b/packages/dynamic-address-resolution/src/shared/util.ts @@ -1,4 +1,12 @@ -import type { NodeKind } from 'codama'; +import { + type Encoder, + getBase16Encoder, + getBase58Encoder, + getBase64Encoder, + getUtf8Encoder, + type ReadonlyUint8Array, +} from '@solana/codecs'; +import type { BytesEncoding, NodeKind } from 'codama'; /** * Checks if a value is a plain object record (struct-like). @@ -33,3 +41,55 @@ export function safeStringify(value: unknown): string { return `non-serializable ${formatValueType(value)}`; } } + +/** + * Whether a user-provided value equals a value decoded from a value node, + * e.g. to evaluate conditions. Integers compare by value whether they are + * numbers or bigints, enum variants compare by `__kind` and `data` whether + * they are objects or identifiers, and arrays and objects compare deeply. + */ +export function isValueEqual(actual: unknown, expected: unknown): boolean { + if (isInteger(actual) && isInteger(expected)) return BigInt(actual) === BigInt(expected); + // Bytes compare by content, whether they are raw bytes or `[encoding, data]` tuples. + const actualBytes = getBytes(actual); + const expectedBytes = getBytes(expected); + if (actualBytes && expectedBytes) { + return actualBytes.length === expectedBytes.length && actualBytes.every((byte, i) => byte === expectedBytes[i]); + } + if (typeof actual === 'string' && isEnumVariantValue(expected)) { + return expected.data === undefined && actual === expected.__kind; + } + if (Array.isArray(actual) && Array.isArray(expected)) { + return actual.length === expected.length && actual.every((item, index) => isValueEqual(item, expected[index])); + } + if (isObjectRecord(actual) && isObjectRecord(expected)) { + const keys = new Set([...Object.keys(actual), ...Object.keys(expected)]); + // The enum discriminator is derived from `__kind`, so it may be omitted from inputs. + keys.delete('__discriminator'); + return [...keys].every(key => isValueEqual(actual[key], expected[key])); + } + return actual === expected; +} + +function isInteger(value: unknown): value is bigint | number { + return typeof value === 'bigint' || (typeof value === 'number' && Number.isInteger(value)); +} + +function isEnumVariantValue(value: unknown): value is { __kind: string; data?: unknown } { + return isObjectRecord(value) && typeof value.__kind === 'string'; +} + +const BYTES_ENCODERS: Record Encoder> = { + base16: getBase16Encoder, + base58: getBase58Encoder, + base64: getBase64Encoder, + utf8: getUtf8Encoder, +}; + +/** The content of bytes given as raw bytes or `[encoding, data]` tuples, e.g. `['base16', '0102']`. */ +function getBytes(value: unknown): ReadonlyUint8Array | undefined { + if (value instanceof Uint8Array) return value; + if (!Array.isArray(value) || value.length !== 2 || typeof value[1] !== 'string') return undefined; + const encoder = BYTES_ENCODERS[value[0] as BytesEncoding] as (() => Encoder) | undefined; + return encoder?.().encode(value[1]); +} diff --git a/packages/dynamic-address-resolution/src/visitors/account-default-value.ts b/packages/dynamic-address-resolution/src/visitors/account-default-value.ts index 8b6f98527..12afeab73 100644 --- a/packages/dynamic-address-resolution/src/visitors/account-default-value.ts +++ b/packages/dynamic-address-resolution/src/visitors/account-default-value.ts @@ -1,227 +1,111 @@ import { CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING, - CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_RESOLVER_MISSING, - CODAMA_ERROR__DYNAMIC_CLIENT__ARGUMENT_MISSING, - CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_DERIVE_PDA, - CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_EXECUTE_RESOLVER, - CODAMA_ERROR__DYNAMIC_CLIENT__INVALID_ACCOUNT_ADDRESS, - CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ADDRESS_TYPE, CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_NODE, CODAMA_ERROR__UNEXPECTED_NODE_KIND, CodamaError, } from '@codama/errors'; import type { Address } from '@solana/addresses'; -import { address } from '@solana/addresses'; -import type { Visitor } from 'codama'; -import type { - AccountBumpValueNode, - AccountValueNode, - ArgumentValueNode, - ConditionalValueNode, - IdentityValueNode, - InstructionAccountNode, - PayerValueNode, - PdaValueNode, - ProgramIdValueNode, - PublicKeyValueNode, - ResolverValueNode, +import { + INSTRUCTION_INPUT_VALUE_NODE_KINDS, + type InstructionAccountNode, + type Node, + type Visitor, + visitOrElse, } from 'codama'; -import { visitOrElse } from 'codama'; +import { getInstruction, getProgramAddress, getRequiredDataValue } from '../resolvers/context'; import { resolveAccountValueNodeAddress } from '../resolvers/resolve-account-value-node-address'; import { resolveConditionalValueNodeCondition } from '../resolvers/resolve-conditional'; -import { resolvePDAAddress } from '../resolvers/resolve-pda-address'; -import type { BaseResolutionContext } from '../resolvers/types'; -import { isAddressConvertible, toAddress } from '../shared/address'; -import type { AccountsInput, ArgumentsInput, ResolverFnInput, ResolversInput } from '../shared/types'; -import { formatValueType, safeStringify } from '../shared/util'; - -type AccountDefaultValueVisitorContext< - TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, -> = BaseResolutionContext & { - ixAccountNode: InstructionAccountNode; -}; +import { resolvePdaAddress } from '../resolvers/resolve-pda-address'; +import type { ResolutionContext } from '../resolvers/types'; +import { toAddress, toAddressOrThrow } from '../shared/address'; export const ACCOUNT_DEFAULT_VALUE_SUPPORTED_NODE_KINDS = [ 'accountBumpValueNode', + 'accountDataValueNode', 'accountValueNode', - 'argumentValueNode', 'conditionalValueNode', + 'dataValueNode', 'identityValueNode', + 'injectedValueNode', 'payerValueNode', 'pdaValueNode', 'programIdValueNode', + 'programLinkNode', 'publicKeyValueNode', - 'resolverValueNode', ] as const; type AccountDefaultValueSupportedNodeKind = (typeof ACCOUNT_DEFAULT_VALUE_SUPPORTED_NODE_KINDS)[number]; -/** - * Visitor for resolving InstructionInputValueNode types to Address values for account resolution. - */ -export function createAccountDefaultValueVisitor< - TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, ->( - ctx: AccountDefaultValueVisitorContext, -): Visitor, AccountDefaultValueSupportedNodeKind> { - const { root, ixNode, ixAccountNode, argumentsInput, accountsInput, resolversInput, resolutionPath } = ctx; - const accountAddressInput = accountsInput?.[ixAccountNode.identifier]; - - return { - visitAccountBumpValue: async (_node: AccountBumpValueNode) => { - return await Promise.reject( - new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_NODE, { - nodeKind: 'accountBumpValueNode', - }), - ); - }, +export function unexpectedAccountDefaultValueNode(node: Node): never { + throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { + expectedKinds: [...ACCOUNT_DEFAULT_VALUE_SUPPORTED_NODE_KINDS], + kind: node.kind, + node, + }); +} - visitAccountValue: async (node: AccountValueNode) => { - return await resolveAccountValueNodeAddress(node, { - accountsInput, - argumentsInput, - ixNode, - resolutionPath, - resolversInput, - root, +/** Visitor resolving the default value of an instruction account to its address. */ +export function createAccountDefaultValueVisitor( + ixAccountNode: InstructionAccountNode, + ctx: ResolutionContext, +): Visitor, AccountDefaultValueSupportedNodeKind> { + const accountAddressInput = ctx.accountsInput?.[ixAccountNode.identifier]; + const requireProvidedAccount = () => { + if (accountAddressInput === undefined || accountAddressInput === null) { + throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING, { + accountName: ixAccountNode.identifier, + instructionName: getInstruction(ctx).identifier, }); - }, - - visitArgumentValue: async (node: ArgumentValueNode) => { - const argValue = argumentsInput?.[node.name]; - if (argValue === undefined || argValue === null) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__ARGUMENT_MISSING, { - argumentName: node.name, - instructionName: ixNode.identifier, - }); - } + } + return Promise.resolve(toAddress(accountAddressInput)); + }; + const unsupported = (nodeKind: AccountDefaultValueSupportedNodeKind) => + Promise.reject(new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_NODE, { nodeKind })); - if (!isAddressConvertible(argValue)) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ADDRESS_TYPE, { - accountName: ixAccountNode.identifier, - actualType: formatValueType(argValue), - expectedType: 'Address | PublicKey', - }); - } + const visitor: Visitor, AccountDefaultValueSupportedNodeKind> = { + visitAccountBumpValue: () => unsupported('accountBumpValueNode'), - return await Promise.resolve(toAddress(argValue)); - }, + // Resolving account data would require fetching the account. + visitAccountDataValue: () => unsupported('accountDataValueNode'), - visitConditionalValue: async (conditionalValueNode: ConditionalValueNode) => { - // ifTrue or ifFalse branch of ConditionalValueNode. - const resolvedInputValueNode = await resolveConditionalValueNodeCondition({ - accountsInput, - argumentsInput, - conditionalValueNode, - ixAccountNode, - ixNode, - resolutionPath, - resolversInput, - root, - }); + visitAccountValue: async node => await resolveAccountValueNodeAddress(node, ctx), - if (resolvedInputValueNode === undefined) { - // No matching branch (e.g. conditional with no ifFalse and falsy condition). - // Return null to signal "unresolved" to apply optionalAccountStrategy. - if (ixAccountNode.isOptional) { - return null; - } + visitConditionalValue: async node => { + const branch = await resolveConditionalValueNodeCondition(node, ixAccountNode, ctx); + if (branch === undefined) { + // No matching branch: optional accounts resolve using the optional account strategy. + if (ixAccountNode.isOptional) return null; throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING, { accountName: ixAccountNode.identifier, - instructionName: ixNode.identifier, + instructionName: getInstruction(ctx).identifier, }); } - // Recursively resolve the chosen branch. - const visitor = createAccountDefaultValueVisitor(ctx); - const addressValue = await visitOrElse(resolvedInputValueNode, visitor, innerNode => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...ACCOUNT_DEFAULT_VALUE_SUPPORTED_NODE_KINDS], - kind: innerNode.kind, - node: innerNode, - }); - }); - return addressValue; + return await visitOrElse(branch, visitor, unexpectedAccountDefaultValueNode); }, - visitIdentityValue: async (_node: IdentityValueNode) => { - if (accountAddressInput === undefined || accountAddressInput === null) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING, { - accountName: ixAccountNode.identifier, - instructionName: ixNode.identifier, - }); - } - return await Promise.resolve(toAddress(accountAddressInput)); - }, + visitDataValue: node => + Promise.resolve(toAddressOrThrow(getRequiredDataValue(ctx, node.path), ixAccountNode.identifier)), - visitPayerValue: async (_node: PayerValueNode) => { - if (accountAddressInput === undefined || accountAddressInput === null) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING, { - accountName: ixAccountNode.identifier, - instructionName: ixNode.identifier, - }); - } - return await Promise.resolve(toAddress(accountAddressInput)); - }, + visitIdentityValue: requireProvidedAccount, - visitPdaValue: async (node: PdaValueNode) => { - const pda = await resolvePDAAddress({ - accountsInput, - argumentsInput, - ixNode, - pdaValueNode: node, - resolutionPath, - resolversInput, - root, - }); - if (pda === null) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_DERIVE_PDA, { - accountName: ixAccountNode.identifier, - }); - } - return pda[0]; + visitInjectedValue: async node => { + const resolved = ctx.scope.resolveOrThrow(node, { kinds: INSTRUCTION_INPUT_VALUE_NODE_KINDS }); + return await visitOrElse(resolved, visitor, unexpectedAccountDefaultValueNode); }, - visitProgramIdValue: async (_node: ProgramIdValueNode) => { - return await Promise.resolve(address(root.program.publicKey)); - }, + visitPayerValue: requireProvidedAccount, - visitPublicKeyValue: async (node: PublicKeyValueNode) => { - return await Promise.resolve(address(node.publicKey)); - }, + visitPdaValue: async node => (await resolvePdaAddress(node, ctx, ixAccountNode.identifier))[0], - visitResolverValue: async (node: ResolverValueNode) => { - const resolverFn = resolversInput?.[node.name]; - if (!resolverFn) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_RESOLVER_MISSING, { - accountName: ixAccountNode.identifier, - resolverName: node.name, - }); - } - let result: unknown; - try { - result = await resolverFn(argumentsInput ?? {}, accountsInput ?? {}); - } catch (error) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_EXECUTE_RESOLVER, { - cause: error, - resolverName: node.name, - targetKind: 'instructionAccountNode', - targetName: ixAccountNode.identifier, - }); - } + visitProgramIdValue: () => Promise.resolve(getProgramAddress(ctx.instructionPath)), - if (!isAddressConvertible(result)) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__INVALID_ACCOUNT_ADDRESS, { - accountName: ixAccountNode.identifier, - value: safeStringify(result), - }); - } - - return toAddress(result); + visitProgramLink: node => { + const program = ctx.linkables.getOrThrow([...ctx.instructionPath, ixAccountNode, node]); + return Promise.resolve(toAddress(program.publicKey)); }, + + visitPublicKeyValue: node => Promise.resolve(toAddress(node.publicKey)), }; + return visitor; } diff --git a/packages/dynamic-address-resolution/src/visitors/codec-input-transformer.ts b/packages/dynamic-address-resolution/src/visitors/codec-input-transformer.ts deleted file mode 100644 index 109dc2050..000000000 --- a/packages/dynamic-address-resolution/src/visitors/codec-input-transformer.ts +++ /dev/null @@ -1,384 +0,0 @@ -import { - CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE, - CODAMA_ERROR__LINKED_NODE_NOT_FOUND, - CODAMA_ERROR__UNEXPECTED_NODE_KIND, - CodamaError, -} from '@codama/errors'; -import type { BytesEncoding, Node, RootNode, TypeNode, Visitor } from 'codama'; -import { isNode, pascalCase, visitOrElse } from 'codama'; - -import { isUint8Array, uint8ArrayToEncodedString } from '../shared/bytes-encoding'; -import { formatValueType, isObjectRecord } from '../shared/util'; - -/** - * Type nodes that the input value transformer can process. - * Includes all StandaloneTypeNode kinds plus definedTypeLinkNode. - */ -export const CODEC_INPUT_TRANSFORMER_SUPPORTED_NODE_KINDS = [ - 'amountTypeNode', - 'arrayTypeNode', - 'booleanTypeNode', - 'bytesTypeNode', - 'dateTimeTypeNode', - 'definedTypeLinkNode', - 'enumTypeNode', - 'fixedSizeTypeNode', - 'hiddenPrefixTypeNode', - 'hiddenSuffixTypeNode', - 'mapTypeNode', - 'numberTypeNode', - 'optionTypeNode', - 'postOffsetTypeNode', - 'preOffsetTypeNode', - 'publicKeyTypeNode', - 'remainderOptionTypeNode', - 'sentinelTypeNode', - 'setTypeNode', - 'sizePrefixTypeNode', - 'solAmountTypeNode', - 'stringTypeNode', - 'structFieldTypeNode', - 'structTypeNode', - 'tupleTypeNode', - 'zeroableOptionTypeNode', -] as const; - -export type TransformableTypeNodeKind = (typeof CODEC_INPUT_TRANSFORMER_SUPPORTED_NODE_KINDS)[number]; - -export type CodecInputTransformerOptions = { - bytesEncoding?: BytesEncoding; -}; - -/** - * A transformer function that converts user input to Codama codec-compatible format. - */ -export type CodecInputTransformer = (input: unknown) => unknown; - -function unexpectedNodeFallback(node: Node): never { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...CODEC_INPUT_TRANSFORMER_SUPPORTED_NODE_KINDS], - kind: node.kind, - node, - }); -} - -export function createCodecInputTransformerVisitor( - root: RootNode, - options: CodecInputTransformerOptions = {}, -): Visitor { - const bytesEncoding = options.bytesEncoding ?? 'base16'; - - const visitor: Visitor = { - visitAmountType(node) { - return visitOrElse(node.number, visitor, unexpectedNodeFallback); - }, - - visitArrayType(node) { - const itemTransform = visitOrElse(node.item, visitor, unexpectedNodeFallback); - return (input: unknown) => { - if (!Array.isArray(input)) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE, { - actualType: formatValueType(input), - expectedType: 'array', - nodeKind: 'arrayTypeNode', - }); - } - return input.map(itemTransform); - }; - }, - - visitBooleanType() { - return (input: unknown) => input; - }, - - visitBytesType() { - return (input: unknown) => { - if (isUint8Array(input)) { - return [bytesEncoding, uint8ArrayToEncodedString(input, bytesEncoding)]; - } - // Accept number[] by coercing to Uint8Array. - if (Array.isArray(input) && input.every(item => typeof item === 'number')) { - return [bytesEncoding, uint8ArrayToEncodedString(new Uint8Array(input), bytesEncoding)]; - } - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE, { - actualType: formatValueType(input), - expectedType: 'Uint8Array | number[]', - nodeKind: 'bytesTypeNode', - }); - }; - }, - - visitDateTimeType(node) { - return visitOrElse(node.number, visitor, unexpectedNodeFallback); - }, - - visitDefinedTypeLink(node) { - const definedType = (root.program.definedTypes ?? []).find(dt => dt.identifier === node.identifier); - if (!definedType) { - throw new CodamaError(CODAMA_ERROR__LINKED_NODE_NOT_FOUND, { - kind: 'definedTypeLinkNode', - linkNode: node, - name: node.identifier, - path: [], - }); - } - return visitOrElse(definedType.type, visitor, unexpectedNodeFallback); - }, - - visitEnumType(node) { - // Enums encode as discriminated unions with a PascalCase `__kind`, - // matching how @codama/dynamic-codecs builds the codec. Bare - // empty-variant names and indices are also accepted and resolved - // to that shape for ergonomics. - return (input: unknown) => { - if (typeof input === 'number' || typeof input === 'string') { - const variantNode = - typeof input === 'number' - ? (node.variants ?? [])[input] - : (node.variants ?? []).find(v => pascalCase(v.identifier) === pascalCase(input)); - if (variantNode && isNode(variantNode, 'enumEmptyVariantTypeNode')) { - return { __kind: pascalCase(variantNode.identifier) }; - } - return input; - } - - if (!isObjectRecord(input)) { - return input; - } - - if (!('__kind' in input)) { - return input; - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { __discriminator: _d, __kind, ...rest } = input as Record & { __kind: unknown }; - const kindObj = { __kind: pascalCase(String(__kind)) }; - const variantNode = (node.variants ?? []).find( - v => pascalCase(v.identifier) === pascalCase(String(__kind)), - ); - - if (!variantNode) { - const availableVariants = (node.variants ?? []).map(v => v.identifier).join(', '); - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE, { - actualType: `variant '${String(__kind)}'`, - expectedType: `one of [${availableVariants}]`, - nodeKind: 'enumTypeNode', - }); - } - - if (isNode(variantNode, 'enumEmptyVariantTypeNode')) { - return { ...input, ...kindObj }; - } - - if (isNode(variantNode, 'enumStructVariantTypeNode')) { - const structTransform = visitOrElse(variantNode.struct, visitor, unexpectedNodeFallback); - const transformedFields = structTransform(rest); - if (!isObjectRecord(transformedFields)) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE, { - actualType: formatValueType(transformedFields), - expectedType: 'object', - nodeKind: 'enumStructVariantTypeNode', - }); - } - return { ...kindObj, ...transformedFields }; - } - - if (isNode(variantNode, 'enumTupleVariantTypeNode')) { - const tupleTransform = visitOrElse(variantNode.tuple, visitor, unexpectedNodeFallback); - if (!('fields' in rest) || !Array.isArray(rest.fields)) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE, { - actualType: formatValueType(rest.fields ?? rest), - expectedType: 'array (fields)', - nodeKind: 'enumTupleVariantTypeNode', - }); - } - return { ...kindObj, fields: tupleTransform(rest.fields) }; - } - - return input; - }; - }, - - visitFixedSizeType(node) { - return visitOrElse(node.type, visitor, unexpectedNodeFallback); - }, - - visitHiddenPrefixType(node) { - return visitOrElse(node.type, visitor, unexpectedNodeFallback); - }, - - visitHiddenSuffixType(node) { - return visitOrElse(node.type, visitor, unexpectedNodeFallback); - }, - - visitMapType(node) { - // Maps are represented as objects in dynamic-codecs - const valueTransform = visitOrElse(node.value, visitor, unexpectedNodeFallback); - return (input: unknown) => { - if (!isObjectRecord(input)) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE, { - actualType: formatValueType(input), - expectedType: 'object', - nodeKind: 'mapTypeNode', - }); - } - const result: Record = {}; - for (const [key, value] of Object.entries(input)) { - result[key] = valueTransform(value); - } - return result; - }; - }, - - // Primitive types (pass through) - visitNumberType() { - return (input: unknown) => input; - }, - - visitOptionType(node) { - const innerTransform = visitOrElse(node.item, visitor, unexpectedNodeFallback); - return (input: unknown) => { - if (input === null || input === undefined) return input; - return innerTransform(input); - }; - }, - - visitPostOffsetType(node) { - return visitOrElse(node.type, visitor, unexpectedNodeFallback); - }, - - visitPreOffsetType(node) { - return visitOrElse(node.type, visitor, unexpectedNodeFallback); - }, - - visitPublicKeyType() { - return (input: unknown) => input; - }, - - visitRemainderOptionType(node) { - const innerTransform = visitOrElse(node.item, visitor, unexpectedNodeFallback); - return (input: unknown) => { - if (input === null || input === undefined) return input; - return innerTransform(input); - }; - }, - - visitSentinelType(node) { - return visitOrElse(node.type, visitor, unexpectedNodeFallback); - }, - - visitSetType(node) { - // Sets are represented as arrays in dynamic-codecs - const itemTransform = visitOrElse(node.item, visitor, unexpectedNodeFallback); - return (input: unknown) => { - if (!Array.isArray(input)) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE, { - actualType: formatValueType(input), - expectedType: 'array', - nodeKind: 'setTypeNode', - }); - } - return input.map(itemTransform); - }; - }, - - visitSizePrefixType(node) { - return visitOrElse(node.type, visitor, unexpectedNodeFallback); - }, - - visitSolAmountType(node) { - return visitOrElse(node.number, visitor, unexpectedNodeFallback); - }, - - visitStringType() { - return (input: unknown) => input; - }, - - visitStructFieldType(node) { - return visitOrElse(node.type, visitor, unexpectedNodeFallback); - }, - - visitStructType(node) { - const fieldTransformers = (node.fields ?? []).map(field => { - const transform = visitOrElse(field, visitor, unexpectedNodeFallback); - return { name: field.identifier, transform }; - }); - return (input: unknown) => { - if (!isObjectRecord(input)) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE, { - actualType: formatValueType(input), - expectedType: 'object', - nodeKind: 'structTypeNode', - }); - } - const result = { ...input } as Record; - for (const { name, transform } of fieldTransformers) { - if (name in result) { - result[name] = transform(result[name]); - } - } - return result; - }; - }, - - visitTupleType(node) { - const itemTransforms = (node.items ?? []).map(item => visitOrElse(item, visitor, unexpectedNodeFallback)); - return (input: unknown) => { - if (!Array.isArray(input)) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE, { - actualType: formatValueType(input), - expectedType: 'array', - nodeKind: 'tupleTypeNode', - }); - } - if (input.length !== itemTransforms.length) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE, { - actualType: `array(length:${input.length})`, - expectedType: `array(length:${itemTransforms.length})`, - nodeKind: 'tupleTypeNode', - }); - } - return input.map((value: unknown, index) => itemTransforms[index](value)); - }; - }, - - visitZeroableOptionType(node) { - const innerTransform = visitOrElse(node.item, visitor, unexpectedNodeFallback); - return (input: unknown) => { - if (input === null || input === undefined) return input; - return innerTransform(input); - }; - }, - }; - - return visitor; -} - -/** - * Creates a transformer function that converts user input to codec-compatible input format. - * For example: user input Uint8Array for binary data but @codama/dynamic-codecs expects [BytesEncoding, string] as input - * - * @param typeNode - The Codama type node describing the expected structure - * @param root - Root node for resolving definedTypeLinkNode references - * @param options - Configuration options (encoding preference) - * @returns Transformer function that converts input to codec format - * - * @example - * const transformer = createCodecInputTransformer( - * bytesTypeNode(), - * root, - * { bytesEncoding: 'base16' } - * ); - * - * const input = new Uint8Array([72, 101, 108, 108, 111]); - * const transformed = transformer(input); // => ['base16', '48656c6c6f'] - * Usage with codec: codamaCodec.encode(['base16', '48656c6c6f']) - */ -export function createCodecInputTransformer( - typeNode: TypeNode, - root: RootNode, - options?: CodecInputTransformerOptions, -): CodecInputTransformer { - const visitor = createCodecInputTransformerVisitor(root, options); - return visitOrElse(typeNode, visitor, unexpectedNodeFallback); -} diff --git a/packages/dynamic-address-resolution/src/visitors/condition-node-value.ts b/packages/dynamic-address-resolution/src/visitors/condition-node-value.ts index 7c5979130..3e10068a2 100644 --- a/packages/dynamic-address-resolution/src/visitors/condition-node-value.ts +++ b/packages/dynamic-address-resolution/src/visitors/condition-node-value.ts @@ -1,71 +1,39 @@ -import { CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_EXECUTE_RESOLVER, CodamaError } from '@codama/errors'; -import type { Visitor } from 'codama'; -import type { AccountValueNode, ArgumentValueNode, ResolverValueNode } from 'codama'; +import { CODAMA_ERROR__UNEXPECTED_NODE_KIND, CodamaError } from '@codama/errors'; +import type { Node, Visitor } from 'codama'; +import { getDataValue, getInstruction } from '../resolvers/context'; import { resolveAccountValueNodeAddress } from '../resolvers/resolve-account-value-node-address'; -import type { BaseResolutionContext } from '../resolvers/types'; -import type { AccountsInput, ArgumentsInput, ResolverFnInput, ResolversInput } from '../shared/types'; +import type { ResolutionContext } from '../resolvers/types'; -export const CONDITION_NODE_SUPPORTED_NODE_KINDS = [ - 'accountValueNode', - 'argumentValueNode', - 'resolverValueNode', -] as const; +export const CONDITION_NODE_SUPPORTED_NODE_KINDS = ['accountValueNode', 'dataValueNode'] as const; type ConditionNodeSupportedNodeKind = (typeof CONDITION_NODE_SUPPORTED_NODE_KINDS)[number]; -/** - * Visitor for resolving condition nodes in ConditionalValueNode. - * Returns the runtime value of the condition (from accounts or arguments). - */ -export function createConditionNodeValueVisitor< - TAccounts extends AccountsInput = AccountsInput, - TArgs extends ArgumentsInput = ArgumentsInput, - TResolvers extends ResolverFnInput = ResolversInput, ->(ctx: BaseResolutionContext): Visitor, ConditionNodeSupportedNodeKind> { - const { root, ixNode, argumentsInput, accountsInput, resolutionPath, resolversInput } = ctx; +export function unexpectedConditionNode(node: Node): never { + throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { + expectedKinds: [...CONDITION_NODE_SUPPORTED_NODE_KINDS], + kind: node.kind, + node, + }); +} +/** Visitor resolving the condition of a `conditionalValueNode` to its runtime value. */ +export function createConditionNodeValueVisitor( + ctx: ResolutionContext, +): Visitor, ConditionNodeSupportedNodeKind> { return { - visitAccountValue: async (node: AccountValueNode) => { - // If the user explicitly provides null for a conditional account, - // return it for the conditionalValueNode ifFalse branch. - const accountAddressInput = accountsInput?.[node.identifier]; - if (accountAddressInput === null) { - return null; - } - - return await resolveAccountValueNodeAddress(node, { - accountsInput, - argumentsInput, - ixNode, - resolutionPath, - resolversInput, - root, - }); + visitAccountValue: async node => { + // An account explicitly provided as null does not exist. + const input = ctx.accountsInput?.[node.identifier]; + if (input === null) return null; + // Neither does an account that is not provided and cannot be resolved from a default value. + const account = (getInstruction(ctx).accounts ?? []).find( + account => account.identifier === node.identifier, + ); + if (input === undefined && account && account.defaultValue === undefined) return undefined; + return await resolveAccountValueNodeAddress(node, ctx); }, - visitArgumentValue: async (node: ArgumentValueNode) => { - const argInput = argumentsInput?.[node.name]; - return await Promise.resolve(argInput); - }, - - visitResolverValue: async (node: ResolverValueNode) => { - const resolverFn = resolversInput?.[node.name]; - if (!resolverFn) { - // ConditionalValueNode evaluates condition and based on result it chooses to take either ifTrue or ifFalse branch. - // If resolver is not provided, we assume condition is false and return undefined instead of throwing an error to take ifFalse branch. - return undefined; - } - try { - return await resolverFn(argumentsInput ?? {}, accountsInput ?? {}); - } catch (error) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_EXECUTE_RESOLVER, { - cause: error, - resolverName: node.name, - targetKind: 'conditionalValueNode', - targetName: node.name, - }); - } - }, + visitDataValue: node => Promise.resolve(getDataValue(ctx, node.path)), }; } diff --git a/packages/dynamic-address-resolution/src/visitors/default-value-encoder.ts b/packages/dynamic-address-resolution/src/visitors/default-value-encoder.ts deleted file mode 100644 index 049926cc2..000000000 --- a/packages/dynamic-address-resolution/src/visitors/default-value-encoder.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { ReadonlyUint8Array } from '@solana/codecs'; -import { pascalCase } from 'codama'; -import type { Visitor } from 'codama'; -import type { - BooleanValueNode, - BytesValueNode, - EnumValueNode, - NumberValueNode, - PublicKeyValueNode, - StringValueNode, -} from 'codama'; - -export const DEFAULT_VALUE_ENCODER_SUPPORTED_NODE_KINDS = [ - 'booleanValueNode', - 'bytesValueNode', - 'enumValueNode', - 'noneValueNode', - 'numberValueNode', - 'publicKeyValueNode', - 'stringValueNode', -] as const; - -type DefaultValueEncoderSupportedNodeKind = (typeof DEFAULT_VALUE_ENCODER_SUPPORTED_NODE_KINDS)[number]; - -/** - * Visitor for encoding default (omitted) values for instruction arguments. - * - * Today, Anchor/Codama primarily uses omitted defaults for discriminators - * (`bytesValueNode`), but this visitor is intentionally extensible as we - * expand node coverage over time. - */ -export function createDefaultValueEncoderVisitor(codec: { - encode: (value: unknown) => ReadonlyUint8Array; -}): Visitor { - return { - visitBooleanValue: (node: BooleanValueNode) => codec.encode(node.boolean), - visitBytesValue: (node: BytesValueNode) => codec.encode([node.encoding, node.data]), - visitEnumValue: (node: EnumValueNode) => codec.encode({ __kind: pascalCase(node.variant) }), - visitNoneValue: () => codec.encode(null), - visitNumberValue: (node: NumberValueNode) => codec.encode(node.number), - visitPublicKeyValue: (node: PublicKeyValueNode) => codec.encode(node.publicKey), - visitStringValue: (node: StringValueNode) => codec.encode(node.string), - }; -} diff --git a/packages/dynamic-address-resolution/src/visitors/index.ts b/packages/dynamic-address-resolution/src/visitors/index.ts index bac5313d0..81d5ca8a4 100644 --- a/packages/dynamic-address-resolution/src/visitors/index.ts +++ b/packages/dynamic-address-resolution/src/visitors/index.ts @@ -1,14 +1,3 @@ export { createAccountDefaultValueVisitor } from './account-default-value'; export { createConditionNodeValueVisitor } from './condition-node-value'; -export { createDefaultValueEncoderVisitor, DEFAULT_VALUE_ENCODER_SUPPORTED_NODE_KINDS } from './default-value-encoder'; -export { - createPdaSeedValueVisitor, - createConstantPdaSeedValueVisitor, - unexpectedConstantPdaSeedNodeFallback, - PDA_SEED_VALUE_SUPPORTED_NODE_KINDS, - CONSTANT_PDA_SEED_VALUE_SUPPORTED_NODE_KINDS, - type ConstantPdaSeedValueVisitorContext, - type PdaSeedValueVisitorContext, -} from './pda-seed-value'; -export { createValueNodeVisitor } from './value-node-value'; -export { createCodecInputTransformer, createCodecInputTransformerVisitor } from './codec-input-transformer'; +export { resolvePdaSeedValue } from './pda-seed-value'; diff --git a/packages/dynamic-address-resolution/src/visitors/pda-seed-value.ts b/packages/dynamic-address-resolution/src/visitors/pda-seed-value.ts index 9d8210fb3..9fba84f03 100644 --- a/packages/dynamic-address-resolution/src/visitors/pda-seed-value.ts +++ b/packages/dynamic-address-resolution/src/visitors/pda-seed-value.ts @@ -1,224 +1,24 @@ -import { getNodeCodec } from '@codama/dynamic-codecs'; -import { - CODAMA_ERROR__DYNAMIC_CLIENT__ARGUMENT_MISSING, - CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_DERIVE_PDA, - CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, - CODAMA_ERROR__DYNAMIC_CLIENT__NODE_REFERENCE_NOT_FOUND, - CODAMA_ERROR__UNEXPECTED_NODE_KIND, - CodamaError, -} from '@codama/errors'; -import type { Address } from '@solana/addresses'; -import type { ReadonlyUint8Array } from '@solana/codecs'; -import { - type AccountValueNode, - type ArgumentValueNode, - type BooleanValueNode, - type BytesValueNode, - ConstantValueNode, - extendVisitor, - isNode, - Node, - NoneValueNode, - NumberValueNode, - type ProgramIdValueNode, - type PublicKeyValueNode, - type SomeValueNode, - type StringValueNode, - type TypeNode, - type Visitor, - visitOrElse, -} from 'codama'; +import { CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_DERIVE_PDA, CodamaError } from '@codama/errors'; +import { isNode, type PdaSeedValueValue } from 'codama'; +import { getDataValue, getValue } from '../resolvers/context'; import { resolveAccountValueNodeAddress } from '../resolvers/resolve-account-value-node-address'; -import type { BaseResolutionContext } from '../resolvers/types'; -import { toAddress } from '../shared/address'; -import { getCodecFromBytesEncoding } from '../shared/bytes-encoding'; -import { getMemoizedAddressEncoder, getMemoizedBooleanEncoder, getMemoizedUtf8Codec } from '../shared/codecs'; -import { createCodecInputTransformer } from './codec-input-transformer'; - -export const CONSTANT_PDA_SEED_VALUE_SUPPORTED_NODE_KINDS = [ - 'booleanValueNode', - 'bytesValueNode', - 'constantValueNode', - 'noneValueNode', - 'numberValueNode', - 'programIdValueNode', - 'publicKeyValueNode', - 'someValueNode', - 'stringValueNode', -] as const; - -export const PDA_SEED_VALUE_SUPPORTED_NODE_KINDS = [ - ...CONSTANT_PDA_SEED_VALUE_SUPPORTED_NODE_KINDS, - 'accountValueNode', - 'argumentValueNode', -] as const; - -type PdaSeedValueSupportedNodeKind = (typeof PDA_SEED_VALUE_SUPPORTED_NODE_KINDS)[number]; - -export type ConstantPdaSeedValueVisitorContext = { - programId: Address; -}; - -export type PdaSeedValueVisitorContext = BaseResolutionContext & - ConstantPdaSeedValueVisitorContext & { - seedTypeNode?: TypeNode; - }; +import type { ResolutionContext } from '../resolvers/types'; /** - * Visitor for resolving PdaSeedValueNode value to raw bytes. - * Supports recursive resolution of dependent PDAs (accounts that are themselves auto-derived PDAs). - * This is used for both: - * - Variable seeds (e.g. seeds based on instruction accounts/arguments), and - * - Constant seeds (e.g. bytes/string/programId/publicKey constants). + * Resolve the value of a variable PDA seed, before it is encoded using the + * type of the seed. Returns `undefined` when the value is missing. */ -export function createPdaSeedValueVisitor( - ctx: PdaSeedValueVisitorContext, -): Visitor, PdaSeedValueSupportedNodeKind> { - const { root, ixNode, programId, seedTypeNode, resolversInput, resolutionPath } = ctx; - const accountsInput = ctx.accountsInput ?? {}; - const argumentsInput = ctx.argumentsInput ?? {}; - - const base = createConstantPdaSeedValueVisitor({ programId }); - - const visitor: Visitor, PdaSeedValueSupportedNodeKind> = extendVisitor(base, { - visitAccountValue: async (node: AccountValueNode) => { - const resolvedAddress = await resolveAccountValueNodeAddress(node, { - accountsInput, - argumentsInput, - ixNode, - resolutionPath, - resolversInput, - root, - }); - - if (resolvedAddress === null) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_DERIVE_PDA, { - accountName: node.identifier, - }); - } - - return getMemoizedAddressEncoder().encode(resolvedAddress); - }, - - visitArgumentValue: async (node: ArgumentValueNode) => { - const ixArgumentNode = (ixNode.arguments ?? []).find(arg => arg.identifier === node.name); - if (!ixArgumentNode) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__NODE_REFERENCE_NOT_FOUND, { - instructionName: ixNode.identifier, - referencedName: node.name, - }); - } - const argInput = argumentsInput[node.name]; - - // Use the PDA seed's declared type (e.g. plain stringTypeNode) rather than - // the instruction argument's type (e.g. sizePrefixTypeNode) so the seed - // bytes match what the on-chain program derives. - const typeNode = seedTypeNode ?? ixArgumentNode.type; - - if (argInput === undefined || argInput === null) { - // optional remainderOptionTypeNode seeds encodes to zero bytes. - if (isNode(typeNode, 'remainderOptionTypeNode')) { - return new Uint8Array(0); - } - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__ARGUMENT_MISSING, { - argumentName: node.name, - instructionName: ixNode.identifier, - }); - } - const codec = getNodeCodec([root, root.program, ixNode, { ...ixArgumentNode, type: typeNode }]); - const transformer = createCodecInputTransformer(typeNode, root, { - bytesEncoding: 'base16', - }); - const transformedInput = transformer(argInput); - return await Promise.resolve(codec.encode(transformedInput)); - }, - - // Override base recursion so wrapped account/argument values reach the extended handlers. - visitConstantValue: async node => await visitOrElse(node.value, visitor, unexpectedPdaSeedNodeFallback), - - visitSomeValue: async node => await visitOrElse(node.value, visitor, unexpectedPdaSeedNodeFallback), - }); - - return visitor; +export async function resolvePdaSeedValue(node: PdaSeedValueValue, ctx: ResolutionContext): Promise { + if (isNode(node, 'accountValueNode')) { + const address = await resolveAccountValueNodeAddress(node, ctx); + if (address === null) { + throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_DERIVE_PDA, { accountName: node.identifier }); + } + return address; + } + if (isNode(node, 'dataValueNode')) { + return getDataValue(ctx, node.path); + } + return getValue(ctx, ctx.instructionPath, node); } - -export const unexpectedPdaSeedNodeFallback = (node: Node): never => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...PDA_SEED_VALUE_SUPPORTED_NODE_KINDS], - kind: node.kind, - node, - }); -}; - -/** - * Base PDA seed value visitor that handles constant seed kinds only - * (boolean / bytes / constant / none / number / programId / publicKey / some / string). - * The account/argument handlers throw by default and are meant to be extended if needed (e.g. for variable seeds). - */ -export function createConstantPdaSeedValueVisitor( - ctx: ConstantPdaSeedValueVisitorContext, -): Visitor, PdaSeedValueSupportedNodeKind> { - const { programId } = ctx; - - const visitor: Visitor, PdaSeedValueSupportedNodeKind> = { - // Throws error by default since constant seeds should not depend on accounts. - visitAccountValue: async (node: AccountValueNode) => { - return await Promise.resolve(unexpectedConstantPdaSeedNodeFallback(node)); - }, - - // Throws error by default since constant seeds should not depend on arguments. - visitArgumentValue: async (node: ArgumentValueNode) => { - return await Promise.resolve(unexpectedConstantPdaSeedNodeFallback(node)); - }, - - visitBooleanValue: async (node: BooleanValueNode) => - await Promise.resolve(getMemoizedBooleanEncoder().encode(node.boolean)), - - visitBytesValue: async (node: BytesValueNode) => { - const encodedValue = getCodecFromBytesEncoding(node.encoding).encode(node.data); - return await Promise.resolve(encodedValue); - }, - - visitConstantValue: async (node: ConstantValueNode) => { - return await visitOrElse(node.value, visitor, unexpectedConstantPdaSeedNodeFallback); - }, - - visitNoneValue: async (_node: NoneValueNode) => await Promise.resolve(new Uint8Array(0)), - - visitNumberValue: async (node: NumberValueNode) => { - // Sanity check: a violation here indicates a malformed IDL, not a user input error. - if (!Number.isInteger(node.number) || node.number < 0 || node.number > 0xff) { - throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, { - message: `NumberValueNode PDA seed is out of range: must be a valid u8 (0–255), got ${node.number}`, - }); - } - return await Promise.resolve(new Uint8Array([node.number])); - }, - - visitProgramIdValue: async (_node: ProgramIdValueNode) => { - return await Promise.resolve(getMemoizedAddressEncoder().encode(toAddress(programId))); - }, - - visitPublicKeyValue: async (node: PublicKeyValueNode) => { - return await Promise.resolve(getMemoizedAddressEncoder().encode(toAddress(node.publicKey))); - }, - - visitSomeValue: async (node: SomeValueNode) => { - return await visitOrElse(node.value, visitor, unexpectedConstantPdaSeedNodeFallback); - }, - - visitStringValue: async (node: StringValueNode) => - await Promise.resolve(getMemoizedUtf8Codec().encode(node.string)), - }; - - return visitor; -} - -export const unexpectedConstantPdaSeedNodeFallback = (node: Node) => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...CONSTANT_PDA_SEED_VALUE_SUPPORTED_NODE_KINDS], - kind: node.kind, - node, - }); -}; diff --git a/packages/dynamic-address-resolution/src/visitors/value-node-value.ts b/packages/dynamic-address-resolution/src/visitors/value-node-value.ts deleted file mode 100644 index 7f5b56860..000000000 --- a/packages/dynamic-address-resolution/src/visitors/value-node-value.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { CODAMA_ERROR__UNEXPECTED_NODE_KIND, CodamaError } from '@codama/errors'; -import { address } from '@solana/addresses'; -import type { Visitor } from 'codama'; -import type { - ArrayValueNode, - BooleanValueNode, - BytesValueNode, - ConstantValueNode, - EnumValueNode, - MapValueNode, - NoneValueNode, - NumberValueNode, - PublicKeyValueNode, - SetValueNode, - SomeValueNode, - StringValueNode, - StructValueNode, - TupleValueNode, -} from 'codama'; -import { visitOrElse } from 'codama'; - -type ResolvedValue = { - encoding?: string; - kind: string; - value: unknown; -}; - -export const VALUE_NODE_SUPPORTED_NODE_KINDS = [ - 'arrayValueNode', - 'booleanValueNode', - 'bytesValueNode', - 'constantValueNode', - 'enumValueNode', - 'mapValueNode', - 'noneValueNode', - 'numberValueNode', - 'publicKeyValueNode', - 'setValueNode', - 'someValueNode', - 'stringValueNode', - 'structValueNode', - 'tupleValueNode', -] as const; - -type ValueNodeSupportedNodeKind = (typeof VALUE_NODE_SUPPORTED_NODE_KINDS)[number]; - -/** - * Visitor for resolving regular ValueNode types to their typed values. - */ -export function createValueNodeVisitor(): Visitor { - const visitor: Visitor = { - visitArrayValue: (node: ArrayValueNode) => ({ - kind: node.kind, - value: (node.items ?? []).map(item => - visitOrElse(item, visitor, n => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...VALUE_NODE_SUPPORTED_NODE_KINDS], - kind: n.kind, - node: n, - }); - }), - ), - }), - - visitBooleanValue: (node: BooleanValueNode) => ({ - kind: node.kind, - value: node.boolean, - }), - - visitBytesValue: (node: BytesValueNode) => ({ - encoding: node.encoding, - kind: node.kind, - value: node.data, - }), - - visitConstantValue: (node: ConstantValueNode) => { - return visitOrElse(node.value, visitor, innerNode => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...VALUE_NODE_SUPPORTED_NODE_KINDS], - kind: innerNode.kind, - node: innerNode, - }); - }); - }, - - visitEnumValue: (node: EnumValueNode) => ({ - kind: node.kind, - value: node.variant, - }), - - visitMapValue: (node: MapValueNode) => ({ - kind: node.kind, - value: (node.entries ?? []).map(entry => ({ - key: visitOrElse(entry.key, visitor, n => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...VALUE_NODE_SUPPORTED_NODE_KINDS], - kind: n.kind, - node: n, - }); - }), - value: visitOrElse(entry.value, visitor, n => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...VALUE_NODE_SUPPORTED_NODE_KINDS], - kind: n.kind, - node: n, - }); - }), - })), - }), - - visitNoneValue: (node: NoneValueNode) => ({ - kind: node.kind, - value: null, - }), - - visitNumberValue: (node: NumberValueNode) => ({ - kind: node.kind, - value: node.number, - }), - - visitPublicKeyValue: (node: PublicKeyValueNode) => ({ - kind: node.kind, - value: address(node.publicKey), - }), - - visitSetValue: (node: SetValueNode) => ({ - kind: node.kind, - value: (node.items ?? []).map(item => - visitOrElse(item, visitor, n => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...VALUE_NODE_SUPPORTED_NODE_KINDS], - kind: n.kind, - node: n, - }); - }), - ), - }), - - visitSomeValue: (node: SomeValueNode) => { - return visitOrElse(node.value, visitor, innerNode => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...VALUE_NODE_SUPPORTED_NODE_KINDS], - kind: innerNode.kind, - node: innerNode, - }); - }); - }, - - visitStringValue: (node: StringValueNode) => ({ - kind: node.kind, - value: node.string, - }), - - visitStructValue: (node: StructValueNode) => ({ - kind: node.kind, - value: Object.fromEntries( - (node.fields ?? []).map(field => [ - field.identifier, - visitOrElse(field.value, visitor, n => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...VALUE_NODE_SUPPORTED_NODE_KINDS], - kind: n.kind, - node: n, - }); - }), - ]), - ), - }), - - visitTupleValue: (node: TupleValueNode) => ({ - kind: node.kind, - value: (node.items ?? []).map(item => - visitOrElse(item, visitor, n => { - throw new CodamaError(CODAMA_ERROR__UNEXPECTED_NODE_KIND, { - expectedKinds: [...VALUE_NODE_SUPPORTED_NODE_KINDS], - kind: n.kind, - node: n, - }); - }), - ), - }), - }; - - return visitor; -} diff --git a/packages/dynamic-address-resolution/test/codegen/codama-type-to-ts.test.ts b/packages/dynamic-address-resolution/test/codegen/codama-type-to-ts.test.ts index a81e9aa15..2e0a24eee 100644 --- a/packages/dynamic-address-resolution/test/codegen/codama-type-to-ts.test.ts +++ b/packages/dynamic-address-resolution/test/codegen/codama-type-to-ts.test.ts @@ -1,117 +1,103 @@ -import { camelCase, type DefinedTypeNode } from 'codama'; +import { + arrayTypeNode, + booleanTypeNode, + bytesTypeNode, + dateTimeTypeNode, + definedTypeLinkNode, + definedTypeNode, + durationTypeNode, + enumTypeNode, + enumVariantTypeNode, + fixedPointTypeNode, + fixedSizeTransformNode, + floatTypeNode, + integerTypeNode, + integerValueNode, + optionTypeNode, + prefixedCountNode, + publicKeyTypeNode, + stringTypeNode, + structFieldTypeNode, + structTypeNode, + tupleTypeNode, +} from 'codama'; import { describe, expect, test } from 'vitest'; import { codamaTypeToTS } from '../../src/codegen/codama-type-to-ts'; -const NO_DEFINED: DefinedTypeNode[] = []; - describe('codamaTypeToTS', () => { - test('should map numeric format to number or bigint', () => { - expect(codamaTypeToTS({ endian: 'le', format: 'u32', kind: 'numberTypeNode' }, NO_DEFINED)).toBe('number'); - expect(codamaTypeToTS({ endian: 'le', format: 'u64', kind: 'numberTypeNode' }, NO_DEFINED)).toBe( - 'number | bigint', - ); - expect(codamaTypeToTS({ endian: 'le', format: 'i128', kind: 'numberTypeNode' }, NO_DEFINED)).toBe( - 'number | bigint', - ); + test('it maps numbers', () => { + expect(codamaTypeToTS(integerTypeNode('u8'), [])).toBe('number | bigint'); + expect(codamaTypeToTS(integerTypeNode('i128'), [])).toBe('number | bigint'); + expect(codamaTypeToTS(fixedPointTypeNode(integerTypeNode('u64'), 6), [])).toBe('number | bigint'); + expect(codamaTypeToTS(dateTimeTypeNode(integerTypeNode('i64')), [])).toBe('number | bigint'); + expect(codamaTypeToTS(durationTypeNode(integerTypeNode('u32')), [])).toBe('number | bigint'); + expect(codamaTypeToTS(floatTypeNode('f64'), [])).toBe('number'); }); - test('should map primitive types to TS scalars', () => { - expect(codamaTypeToTS({ kind: 'publicKeyTypeNode' }, NO_DEFINED)).toBe('Address'); - expect(codamaTypeToTS({ encoding: 'utf8', kind: 'stringTypeNode' }, NO_DEFINED)).toBe('string'); - expect( - codamaTypeToTS( - { kind: 'booleanTypeNode', size: { endian: 'le', format: 'u8', kind: 'numberTypeNode' } }, - NO_DEFINED, - ), - ).toBe('boolean'); - expect(codamaTypeToTS({ kind: 'bytesTypeNode' }, NO_DEFINED)).toBe('Uint8Array'); + test('it maps scalars', () => { + expect(codamaTypeToTS(publicKeyTypeNode(), [])).toBe('Address'); + expect(codamaTypeToTS(stringTypeNode('utf8'), [])).toBe('string'); + expect(codamaTypeToTS(booleanTypeNode(), [])).toBe('boolean'); + expect(codamaTypeToTS(bytesTypeNode(), [])).toBe('Uint8Array'); }); - test('should append | null for option types', () => { - expect( - codamaTypeToTS( - { - item: { endian: 'le', format: 'u8', kind: 'numberTypeNode' }, - kind: 'optionTypeNode', - prefix: { endian: 'le', format: 'u8', kind: 'numberTypeNode' }, - }, - NO_DEFINED, - ), - ).toBe('number | null'); + test('it ignores transforms', () => { + const type = stringTypeNode('utf8', { transforms: [fixedSizeTransformNode(32)] }); + expect(codamaTypeToTS(type, [])).toBe('string'); }); - test('should parenthesize union item types in arrays', () => { - expect( - codamaTypeToTS( - { - count: { kind: 'fixedCountNode', value: 4 }, - item: { - item: { endian: 'le', format: 'u8', kind: 'numberTypeNode' }, - kind: 'optionTypeNode', - prefix: { endian: 'le', format: 'u8', kind: 'numberTypeNode' }, - }, - kind: 'arrayTypeNode', - }, - NO_DEFINED, - ), - ).toBe('(number | null)[]'); + test('it appends null to options', () => { + expect(codamaTypeToTS(optionTypeNode(publicKeyTypeNode()), [])).toBe('Address | null'); }); - test('should emit field unions for struct types', () => { - const result = codamaTypeToTS( - { - fields: [ - { - kind: 'structFieldTypeNode', - name: camelCase('a'), - type: { endian: 'le', format: 'u8', kind: 'numberTypeNode' }, - }, - { - kind: 'structFieldTypeNode', - name: camelCase('b'), - type: { encoding: 'utf8', kind: 'stringTypeNode' }, - }, - ], - kind: 'structTypeNode', - }, - NO_DEFINED, - ); - expect(result).toBe('{ a: number; b: string }'); + test('it parenthesises union items in arrays', () => { + const type = arrayTypeNode(optionTypeNode(publicKeyTypeNode()), prefixedCountNode(integerTypeNode('u32'))); + expect(codamaTypeToTS(type, [])).toBe('(Address | null)[]'); }); - test('should emit a string union for all-empty enums', () => { - expect( - codamaTypeToTS( - { - kind: 'enumTypeNode', - size: { endian: 'le', format: 'u8', kind: 'numberTypeNode' }, - variants: [ - { kind: 'enumEmptyVariantTypeNode', name: camelCase('one') }, - { kind: 'enumEmptyVariantTypeNode', name: camelCase('two') }, - ], - }, - NO_DEFINED, - ), - ).toBe("'one' | 'two'"); + test('it maps structs, marking fields with default values as optional and skipping omitted ones', () => { + const type = structTypeNode([ + structFieldTypeNode({ + defaultValue: integerValueNode('1'), + defaultValueStrategy: 'omitted', + identifier: 'discriminator', + type: integerTypeNode('u8'), + }), + structFieldTypeNode({ identifier: 'owner', type: publicKeyTypeNode() }), + structFieldTypeNode({ + defaultValue: integerValueNode('5'), + identifier: 'fee', + type: integerTypeNode('u16'), + }), + structFieldTypeNode({ identifier: 'memo', type: optionTypeNode(stringTypeNode('utf8')) }), + ]); + expect(codamaTypeToTS(type, [])).toBe('{ owner: Address; fee?: number | bigint; memo?: string | null }'); }); - test('should resolve definedTypeLinkNode through definedTypes', () => { - const definedTypes: DefinedTypeNode[] = [ - { - docs: [], - kind: 'definedTypeNode', - name: camelCase('amount'), - type: { endian: 'le', format: 'u64', kind: 'numberTypeNode' }, - }, - ]; - expect(codamaTypeToTS({ kind: 'definedTypeLinkNode', name: camelCase('amount') }, definedTypes)).toBe( - 'number | bigint', - ); + test('it maps tuples', () => { + expect(codamaTypeToTS(tupleTypeNode([publicKeyTypeNode(), booleanTypeNode()]), [])).toBe('[Address, boolean]'); + }); + + test('it maps enums without data to their identifiers', () => { + const type = enumTypeNode([enumVariantTypeNode('frozen'), enumVariantTypeNode('initialized')]); + expect(codamaTypeToTS(type, [])).toBe("'frozen' | 'initialized'"); + }); + + test('it maps enums with data to discriminated unions', () => { + const type = enumTypeNode([ + enumVariantTypeNode('quit'), + enumVariantTypeNode('move', { + data: structTypeNode([structFieldTypeNode({ identifier: 'x', type: integerTypeNode('u8') })]), + }), + ]); + expect(codamaTypeToTS(type, [])).toBe("{ __kind: 'quit' } | { __kind: 'move'; data: { x: number | bigint } }"); }); - test('should fall back to unknown for unknown definedTypeLinkNode', () => { - expect(codamaTypeToTS({ kind: 'definedTypeLinkNode', name: camelCase('missing') }, NO_DEFINED)).toBe( + test('it resolves defined type links', () => { + const definedTypes = [definedTypeNode({ identifier: 'amount', type: integerTypeNode('u64') })]; + expect(codamaTypeToTS(definedTypeLinkNode('amount'), definedTypes)).toBe('number | bigint'); + expect(codamaTypeToTS(definedTypeLinkNode('missing'), [])).toBe( 'unknown /** DefinedTypeNode not found for definedTypeLinkNode */', ); }); diff --git a/packages/dynamic-address-resolution/test/codegen/collect-pda-nodes.test.ts b/packages/dynamic-address-resolution/test/codegen/collect-pda-nodes.test.ts index ae0b16138..9e4321016 100644 --- a/packages/dynamic-address-resolution/test/codegen/collect-pda-nodes.test.ts +++ b/packages/dynamic-address-resolution/test/codegen/collect-pda-nodes.test.ts @@ -17,7 +17,7 @@ import { makeRoot } from '../test-utils'; describe('collectPdaNodesFromIdl', () => { test('should collect inline PDA from instruction account default', () => { const inline = pdaNode({ - name: 'inline', + identifier: 'inline', seeds: [variablePdaSeedNode('mint', publicKeyTypeNode())], }); const root = makeRoot([ @@ -25,12 +25,12 @@ describe('collectPdaNodesFromIdl', () => { accounts: [ instructionAccountNode({ defaultValue: pdaValueNode(inline), + identifier: 'acc', isSigner: false, isWritable: false, - name: 'acc', }), ], - name: 'test', + identifier: 'test', }), ]); @@ -41,30 +41,30 @@ describe('collectPdaNodesFromIdl', () => { test('should prefer program.pdas over inline duplicate of the same name', () => { const topLevel = pdaNode({ - name: 'shared', + identifier: 'shared', seeds: [constantPdaSeedNodeFromString('utf8', 'top')], }); const inline = pdaNode({ - name: 'shared', + identifier: 'shared', seeds: [constantPdaSeedNodeFromString('utf8', 'inline')], }); const root = rootNode( programNode({ + identifier: 'program', instructions: [ instructionNode({ accounts: [ instructionAccountNode({ defaultValue: pdaValueNode(inline), + identifier: 'acc', isSigner: false, isWritable: false, - name: 'acc', }), ], - name: 'test', + identifier: 'test', }), ], - name: 'program', pdas: [topLevel], publicKey: '11111111111111111111111111111111', }), diff --git a/packages/dynamic-address-resolution/test/codegen/collect-resolver-names.test.ts b/packages/dynamic-address-resolution/test/codegen/collect-resolver-names.test.ts deleted file mode 100644 index e7e07ec26..000000000 --- a/packages/dynamic-address-resolution/test/codegen/collect-resolver-names.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - conditionalValueNode, - instructionAccountNode, - instructionArgumentNode, - instructionNode, - numberTypeNode, - resolverValueNode, -} from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { collectResolverNames } from '../../src/codegen/collect-resolver-names'; - -describe('collectResolverNames', () => { - test('should dedupe resolver names across accounts and arguments', () => { - const ix = instructionNode({ - accounts: [ - instructionAccountNode({ - defaultValue: resolverValueNode('shared'), - isSigner: false, - isWritable: false, - name: 'a', - }), - ], - arguments: [ - instructionArgumentNode({ - defaultValue: resolverValueNode('shared'), - name: 'arg', - type: numberTypeNode('u8'), - }), - ], - name: 'doThing', - }); - - const names = collectResolverNames(ix); - expect(names).toEqual(new Set(['shared'])); - }); - - test('should recurse into conditionalValueNode branches', () => { - const ix = instructionNode({ - accounts: [ - instructionAccountNode({ - defaultValue: conditionalValueNode({ - condition: resolverValueNode('condition'), - ifFalse: resolverValueNode('no'), - ifTrue: resolverValueNode('yes'), - }), - isSigner: false, - isWritable: false, - name: 'a', - }), - ], - name: 'branch', - }); - - const names = collectResolverNames(ix); - expect(names).toEqual(new Set(['condition', 'yes', 'no'])); - }); -}); diff --git a/packages/dynamic-address-resolution/test/codegen/generate-pda-types.test.ts b/packages/dynamic-address-resolution/test/codegen/generate-pda-types.test.ts index d63045148..441e2baa6 100644 --- a/packages/dynamic-address-resolution/test/codegen/generate-pda-types.test.ts +++ b/packages/dynamic-address-resolution/test/codegen/generate-pda-types.test.ts @@ -18,8 +18,8 @@ describe('generatePdaTypes', () => { test('should return null mapTypeName when there are no PDAs', () => { const root = rootNode( programNode({ + identifier: 'noPdaProgram', instructions: [], - name: 'noPdaProgram', publicKey: '11111111111111111111111111111111', }), ); @@ -30,7 +30,7 @@ describe('generatePdaTypes', () => { test('should emit seed types and aggregate map for PDAs', () => { const pda = pdaNode({ - name: 'config', + identifier: 'config', seeds: [ constantPdaSeedNodeFromString('utf8', 'config'), variablePdaSeedNode('authority', publicKeyTypeNode()), @@ -38,77 +38,77 @@ describe('generatePdaTypes', () => { }); const root = rootNode( programNode({ + identifier: 'myProgram', instructions: [], - name: 'myProgram', pdas: [pda], publicKey: '11111111111111111111111111111111', }), ); const { mapTypeName, typeBlock } = generatePdaTypes(root); expect(mapTypeName).toBe('MyProgramPdas'); - expect(typeBlock).toContain('export type ConfigPdaSeeds'); + expect(typeBlock).toContain('export type ConfigSeeds'); expect(typeBlock).toContain('authority: Address;'); expect(typeBlock).toContain('export type MyProgramPdas'); - expect(typeBlock).toContain('config: (seeds: ConfigPdaSeeds) => Promise;'); + expect(typeBlock).toContain('config: (seeds: ConfigSeeds) => Promise;'); }); test('should discover inline PDAs on instruction account defaults', () => { const inlinePda = pdaNode({ - name: 'inline', + identifier: 'inline', seeds: [variablePdaSeedNode('mint', publicKeyTypeNode())], }); const root = rootNode( programNode({ + identifier: 'inlineProgram', instructions: [ instructionNode({ accounts: [ instructionAccountNode({ defaultValue: pdaValueNode(inlinePda), + identifier: 'inlineAccount', isSigner: false, isWritable: true, - name: 'inlineAccount', }), ], - name: 'doThing', + identifier: 'doThing', }), ], - name: 'inlineProgram', publicKey: '11111111111111111111111111111111', }), ); const { mapTypeName, typeBlock } = generatePdaTypes(root); expect(mapTypeName).toBe('InlineProgramPdas'); - expect(typeBlock).toContain('export type InlinePdaSeeds'); - expect(typeBlock).toContain('inline: (seeds: InlinePdaSeeds) => Promise;'); + expect(typeBlock).toContain('export type InlineSeeds'); + expect(typeBlock).toContain('inline: (seeds: InlineSeeds) => Promise;'); }); test('should emit seedless variant for PDAs with only constant seeds', () => { const pda = pdaNode({ - name: 'fixed', + identifier: 'fixed', seeds: [constantPdaSeedNodeFromString('utf8', 'fixed')], }); const root = rootNode( programNode({ + identifier: 'fixedProgram', instructions: [], - name: 'fixedProgram', pdas: [pda], publicKey: '11111111111111111111111111111111', }), ); const { typeBlock } = generatePdaTypes(root); - expect(typeBlock).not.toContain('FixedPdaSeeds'); + expect(typeBlock).not.toContain('FixedSeeds'); expect(typeBlock).toContain('fixed: (seeds?: Record) => Promise;'); }); test('should use string seed type', () => { const pda = pdaNode({ - name: 'named', + identifier: 'named', seeds: [variablePdaSeedNode('label', stringTypeNode('utf8'))], }); const root = rootNode( programNode({ + identifier: 'namedProgram', instructions: [], - name: 'namedProgram', pdas: [pda], publicKey: '11111111111111111111111111111111', }), diff --git a/packages/dynamic-address-resolution/test/codegen/generate-resolution-input-types.test.ts b/packages/dynamic-address-resolution/test/codegen/generate-resolution-input-types.test.ts index 95b87e350..2f9be3e44 100644 --- a/packages/dynamic-address-resolution/test/codegen/generate-resolution-input-types.test.ts +++ b/packages/dynamic-address-resolution/test/codegen/generate-resolution-input-types.test.ts @@ -1,195 +1,88 @@ import { - camelCase, - instructionAccountNode, - instructionArgumentNode, instructionNode, instructionRemainingAccountsNode, + integerTypeNode, + integerValueNode, + payerValueNode, + programIdValueNode, + stringTypeNode, + structFieldTypeNode, + structTypeNode, } from 'codama'; import { describe, expect, test } from 'vitest'; import { generateResolutionInputTypes } from '../../src/codegen/generate-resolution-input-types'; -import { makeRoot } from '../test-utils'; +import { account, makeRoot } from '../test-utils'; describe('generateResolutionInputTypes', () => { - test('should generate Args type with correct TS types', () => { - const root = makeRoot([ - instructionNode({ - arguments: [ - instructionArgumentNode({ - name: 'amount', - type: { endian: 'le', format: 'u64', kind: 'numberTypeNode' }, - }), - instructionArgumentNode({ - name: 'memo', - type: { encoding: 'utf8', kind: 'stringTypeNode' }, - }), - ], - name: 'transfer', - }), - ]); - const output = generateResolutionInputTypes(root); - expect(output).toContain('export type TransferArgs'); - expect(output).toContain('amount: number | bigint;'); - expect(output).toContain('memo: string;'); - }); - - test('should filter omitted arguments from Args type', () => { - const root = makeRoot([ - instructionNode({ - arguments: [ - instructionArgumentNode({ - name: 'visible', - type: { endian: 'le', format: 'u8', kind: 'numberTypeNode' }, - }), - instructionArgumentNode({ - defaultValue: { kind: 'numberValueNode', number: 0 }, - defaultValueStrategy: 'omitted', - name: 'hidden', - type: { endian: 'le', format: 'u8', kind: 'numberTypeNode' }, - }), - ], - name: 'init', - }), - ]); - const output = generateResolutionInputTypes(root); - expect(output).toContain('visible: number;'); - expect(output).not.toContain('hidden'); - }); - - test('should skip Args block when there are no arguments', () => { - const root = makeRoot([ - instructionNode({ - accounts: [instructionAccountNode({ isSigner: true, isWritable: false, name: 'authority' })], - name: 'noArgs', - }), - ]); - const output = generateResolutionInputTypes(root); - expect(output).not.toContain('NoArgsArgs'); - }); - - test('should mark auto-resolvable accounts with ? and required ones plain', () => { - const root = makeRoot([ - instructionNode({ - accounts: [ - instructionAccountNode({ - defaultValue: { kind: 'payerValueNode' }, - isSigner: true, - isWritable: true, - name: 'payer', - }), - instructionAccountNode({ - defaultValue: { - kind: 'publicKeyValueNode', - publicKey: '11111111111111111111111111111111', - }, - isSigner: false, - isWritable: false, - name: 'systemProgram', - }), - instructionAccountNode({ isSigner: false, isWritable: true, name: 'target' }), - ], - name: 'create', - }), - ]); - const output = generateResolutionInputTypes(root); - expect(output).toContain('export type CreateAccounts'); - expect(output).toContain('payer: Address;'); - expect(output).toContain('systemProgram?: Address;'); - expect(output).toContain('target: Address;'); - }); - - test('should emit | null for optional accounts', () => { - const root = makeRoot([ - instructionNode({ - accounts: [ - instructionAccountNode({ - isOptional: true, - isSigner: false, - isWritable: false, - name: 'closeAuthority', - }), - ], - name: 'maybeClose', - }), - ]); - const output = generateResolutionInputTypes(root); - expect(output).toContain('closeAuthority: Address | null;'); - }); - - test('should emit Resolvers type when resolverValueNode exists', () => { - const root = makeRoot([ - instructionNode({ - accounts: [instructionAccountNode({ isSigner: true, isWritable: false, name: 'authority' })], - arguments: [ - instructionArgumentNode({ - defaultValue: { kind: 'resolverValueNode', name: camelCase('computeValue') }, - name: 'computedValue', - type: { endian: 'le', format: 'u64', kind: 'numberTypeNode' }, - }), - ], - name: 'customResolve', - }), - ]); - const output = generateResolutionInputTypes(root); - expect(output).toContain('export type CustomResolveResolvers'); - expect(output).toContain('computeValue: ResolverFn;'); + test('it generates the instruction data type', () => { + const output = generateResolutionInputTypes( + makeRoot([ + instructionNode({ + data: structTypeNode([ + structFieldTypeNode({ + defaultValue: integerValueNode('2'), + defaultValueStrategy: 'omitted', + identifier: 'discriminator', + type: integerTypeNode('u8'), + }), + structFieldTypeNode({ identifier: 'amount', type: integerTypeNode('u64') }), + structFieldTypeNode({ identifier: 'memo', type: stringTypeNode('utf8') }), + ]), + identifier: 'transfer', + }), + ]), + ); + expect(output).toContain( + 'export type TransferInstructionDataArgs = { amount: number | bigint; memo: string };', + ); }); - test('should mark optional type arguments with ?', () => { - const root = makeRoot([ - instructionNode({ - arguments: [ - instructionArgumentNode({ - name: 'maybeValue', - type: { - item: { endian: 'le', format: 'u32', kind: 'numberTypeNode' }, - kind: 'optionTypeNode', - prefix: { endian: 'le', format: 'u8', kind: 'numberTypeNode' }, - }, - }), - ], - name: 'optionalArgs', - }), - ]); - const output = generateResolutionInputTypes(root); - expect(output).toContain('maybeValue?: number | null;'); + test('it skips the data type when there is no data to provide', () => { + const output = generateResolutionInputTypes(makeRoot([instructionNode({ identifier: 'noop' })])); + expect(output).not.toContain('NoopInstructionDataArgs'); }); - test('should emit remaining account arguments in Args type', () => { - const root = makeRoot([ - instructionNode({ - name: 'multiSig', - remainingAccounts: [ - instructionRemainingAccountsNode( - { kind: 'argumentValueNode', name: camelCase('multiSigners') }, - { isSigner: true, isWritable: false }, - ), - ], - }), - ]); - const output = generateResolutionInputTypes(root); - expect(output).toContain('export type MultiSigArgs'); - expect(output).toContain('multiSigners: Address[];'); + test('it generates the accounts type, including remaining accounts', () => { + const output = generateResolutionInputTypes( + makeRoot([ + instructionNode({ + accounts: [ + account('payer', { defaultValue: payerValueNode() }), + account('systemProgram', { defaultValue: programIdValueNode() }), + account('closeAuthority', { isOptional: true }), + account('target'), + ], + identifier: 'create', + remainingAccounts: [ + instructionRemainingAccountsNode('signers', { isSigner: true }), + instructionRemainingAccountsNode('extras', { isOptional: true }), + ], + }), + ]), + ); + expect(output).toContain( + [ + 'export type CreateAccounts = {', + ' payer: Address;', + ' systemProgram?: Address;', + ' closeAuthority: Address | null;', + ' target: Address;', + ' signers: Address[];', + ' extras?: Address[];', + '};', + ].join('\n'), + ); + expect(output).toContain( + 'export type CreateAccountsWithData = CreateAccounts & Record;', + ); }); - test('should emit empty Accounts fallback for instructions without accounts', () => { - const root = makeRoot([instructionNode({ name: 'noAccounts' })]); - const output = generateResolutionInputTypes(root); + test('it emits loose accounts types for instructions without accounts', () => { + const output = generateResolutionInputTypes(makeRoot([instructionNode({ identifier: 'noAccounts' })])); expect(output).toContain('export type NoAccountsAccounts = Record;'); expect(output).toContain( 'export type NoAccountsAccountsWithData = Record;', ); }); - - test('should not emit Signers or InstructionBuilders blocks', () => { - const root = makeRoot([ - instructionNode({ - accounts: [instructionAccountNode({ isSigner: 'either', isWritable: false, name: 'authority' })], - name: 'transfer', - }), - ]); - const output = generateResolutionInputTypes(root); - expect(output).not.toContain('TransferSigners'); - expect(output).not.toContain('InstructionBuilders'); - }); }); diff --git a/packages/dynamic-address-resolution/test/codegen/generate-types.test.ts b/packages/dynamic-address-resolution/test/codegen/generate-types.test.ts index cb6d42a4a..63051b8c6 100644 --- a/packages/dynamic-address-resolution/test/codegen/generate-types.test.ts +++ b/packages/dynamic-address-resolution/test/codegen/generate-types.test.ts @@ -16,9 +16,9 @@ describe('generateTypes', () => { test('should include ProgramDerivedAddress in the header when PDAs are present', () => { const root = rootNode( programNode({ + identifier: 'p', instructions: [], - name: 'p', - pdas: [pdaNode({ name: 'cfg', seeds: [constantPdaSeedNodeFromString('utf8', 'cfg')] })], + pdas: [pdaNode({ identifier: 'cfg', seeds: [constantPdaSeedNodeFromString('utf8', 'cfg')] })], publicKey: '11111111111111111111111111111111', }), ); diff --git a/packages/dynamic-address-resolution/test/codegen/get-resolution-refs.test.ts b/packages/dynamic-address-resolution/test/codegen/get-resolution-refs.test.ts index 88c08cdb1..a51d7095a 100644 --- a/packages/dynamic-address-resolution/test/codegen/get-resolution-refs.test.ts +++ b/packages/dynamic-address-resolution/test/codegen/get-resolution-refs.test.ts @@ -1,119 +1,94 @@ import { - argumentValueNode, booleanTypeNode, - instructionAccountNode, - instructionArgumentNode, + definedTypeLinkNode, + definedTypeNode, instructionNode, - instructionRemainingAccountsNode, + integerTypeNode, + integerValueNode, optionTypeNode, - resolverValueNode, + structFieldTypeNode, + structTypeNode, } from 'codama'; import { describe, expect, test } from 'vitest'; import { getResolutionRefs } from '../../src/codegen/get-resolution-refs'; describe('getResolutionRefs', () => { - test('should return null argsRef when there are no non-omitted arguments and no argument-valued remaining accounts', () => { - const ix = instructionNode({ accounts: [], arguments: [], name: 'noop' }); - const refs = getResolutionRefs(ix); - expect(refs.argsRef).toBeNull(); - expect(refs.hasArgs).toBe(false); - expect(refs.accountsRef).toBe('NoopAccounts'); + test('it names the accounts types', () => { + const refs = getResolutionRefs(instructionNode({ identifier: 'transfer' })); + expect(refs.accountsRef).toBe('TransferAccounts'); + expect(refs.accountsWithDataRef).toBe('TransferAccountsWithData'); }); - test('should return ${Name}Args when an argument exists', () => { - const ix = instructionNode({ - accounts: [], - arguments: [instructionArgumentNode({ name: 'flag', type: booleanTypeNode() })], - name: 'setFlag', - }); - const refs = getResolutionRefs(ix); - expect(refs.argsRef).toBe('SetFlagArgs'); - expect(refs.hasArgs).toBe(true); + test('it returns no data type for instructions without data to provide', () => { + const withoutData = getResolutionRefs(instructionNode({ identifier: 'noop' })); + const withOmittedData = getResolutionRefs( + instructionNode({ + data: structTypeNode([ + structFieldTypeNode({ + defaultValue: integerValueNode('1'), + defaultValueStrategy: 'omitted', + identifier: 'discriminator', + type: integerTypeNode('u8'), + }), + ]), + identifier: 'noop', + }), + ); + for (const refs of [withoutData, withOmittedData]) { + expect(refs.dataRef).toBeNull(); + expect(refs.hasData).toBe(false); + expect(refs.hasRequiredData).toBe(false); + } }); - test('should ignore arguments with defaultValueStrategy "omitted"', () => { - const ix = instructionNode({ - accounts: [], - arguments: [ - instructionArgumentNode({ - defaultValueStrategy: 'omitted', - name: 'discriminator', - type: booleanTypeNode(), - }), - ], - name: 'omitted', - }); - const refs = getResolutionRefs(ix); - expect(refs.argsRef).toBeNull(); + test('it names the data type after the renderers-js convention', () => { + const refs = getResolutionRefs( + instructionNode({ + data: structTypeNode([structFieldTypeNode({ identifier: 'flag', type: booleanTypeNode() })]), + identifier: 'setFlag', + }), + ); + expect(refs.dataRef).toBe('SetFlagInstructionDataArgs'); + expect(refs.hasData).toBe(true); + expect(refs.hasRequiredData).toBe(true); }); - test('should treat argumentValueNode remaining accounts as args', () => { - const ix = instructionNode({ - accounts: [], - arguments: [], - name: 'extra', - remainingAccounts: [instructionRemainingAccountsNode(argumentValueNode('extras'))], - }); - const refs = getResolutionRefs(ix); - expect(refs.argsRef).toBe('ExtraArgs'); - expect(refs.hasArgs).toBe(true); + test('it does not require data fields with default values or optional types', () => { + const refs = getResolutionRefs( + instructionNode({ + data: structTypeNode([ + structFieldTypeNode({ + defaultValue: integerValueNode('5'), + identifier: 'fee', + type: integerTypeNode('u8'), + }), + structFieldTypeNode({ identifier: 'memo', type: optionTypeNode(booleanTypeNode()) }), + ]), + identifier: 'pay', + }), + ); + expect(refs.hasData).toBe(true); + expect(refs.hasRequiredData).toBe(false); }); - test('should report hasRequiredArgs true when at least one non-optional argument exists', () => { - const ix = instructionNode({ - accounts: [], - arguments: [instructionArgumentNode({ name: 'flag', type: booleanTypeNode() })], - name: 'setFlag', - }); - const refs = getResolutionRefs(ix); - expect(refs.hasRequiredArgs).toBe(true); - expect(refs.hasRequiredRemainingAccounts).toBe(false); + test('it follows linked data', () => { + const definedTypes = [ + definedTypeNode({ + identifier: 'args', + type: structTypeNode([structFieldTypeNode({ identifier: 'flag', type: booleanTypeNode() })]), + }), + ]; + const refs = getResolutionRefs( + instructionNode({ data: definedTypeLinkNode('args'), identifier: 'set' }), + definedTypes, + ); + expect(refs.hasRequiredData).toBe(true); }); - test('should report hasRequiredArgs false when every argument is optional-kinded', () => { - const ix = instructionNode({ - accounts: [], - arguments: [instructionArgumentNode({ name: 'maybeFlag', type: optionTypeNode(booleanTypeNode()) })], - name: 'setMaybeFlag', - }); - const refs = getResolutionRefs(ix); - expect(refs.hasArgs).toBe(true); - expect(refs.hasRequiredArgs).toBe(false); - }); - - test('should report hasRequiredRemainingAccounts based on the remaining-account argument isOptional flag', () => { - const required = instructionNode({ - accounts: [], - arguments: [], - name: 'requiredExtras', - remainingAccounts: [instructionRemainingAccountsNode(argumentValueNode('extras'), { isOptional: false })], - }); - const optional = instructionNode({ - accounts: [], - arguments: [], - name: 'optionalExtras', - remainingAccounts: [instructionRemainingAccountsNode(argumentValueNode('extras'), { isOptional: true })], - }); - expect(getResolutionRefs(required).hasRequiredRemainingAccounts).toBe(true); - expect(getResolutionRefs(optional).hasRequiredRemainingAccounts).toBe(false); - }); - - test('should emit resolversRef when an account default is a resolverValueNode', () => { - const ix = instructionNode({ - accounts: [ - instructionAccountNode({ - defaultValue: resolverValueNode('resolveOwner'), - isSigner: false, - isWritable: false, - name: 'owner', - }), - ], - arguments: [], - name: 'doStuff', - }); - const refs = getResolutionRefs(ix); - expect(refs.resolversRef).toBe('DoStuffResolvers'); - expect(refs.hasResolvers).toBe(true); + test('it keeps unresolved linked data as required data', () => { + const refs = getResolutionRefs(instructionNode({ data: definedTypeLinkNode('unknown'), identifier: 'set' })); + expect(refs.dataRef).toBe('SetInstructionDataArgs'); + expect(refs.hasRequiredData).toBe(true); }); }); diff --git a/packages/dynamic-address-resolution/test/codegen/is-account-auto-resolvable.test.ts b/packages/dynamic-address-resolution/test/codegen/is-account-auto-resolvable.test.ts index 80dfe9002..a05ad60e7 100644 --- a/packages/dynamic-address-resolution/test/codegen/is-account-auto-resolvable.test.ts +++ b/packages/dynamic-address-resolution/test/codegen/is-account-auto-resolvable.test.ts @@ -1,26 +1,39 @@ -import { instructionAccountNode, pdaNode, pdaValueNode, publicKeyTypeNode, variablePdaSeedNode } from 'codama'; +import { + accountBumpValueNode, + accountDataValueNode, + injectedValueNode, + instructionAccountNode, + instructionNode, + payerValueNode, + pdaNode, + pdaValueNode, + providedNode, + publicKeyTypeNode, + publicKeyValueNode, + variablePdaSeedNode, +} from 'codama'; import { describe, expect, test } from 'vitest'; import { isAccountAutoResolvable } from '../../src/codegen/is-account-auto-resolvable'; describe('isAccountAutoResolvable', () => { test('should return false when account has no defaultValue', () => { - const acc = instructionAccountNode({ isSigner: false, isWritable: false, name: 'plain' }); + const acc = instructionAccountNode({ identifier: 'plain', isSigner: false, isWritable: false }); expect(isAccountAutoResolvable(acc)).toBe(false); }); test.each([ instructionAccountNode({ defaultValue: { kind: 'identityValueNode' }, + identifier: 'identity', isSigner: true, isWritable: false, - name: 'identity', }), instructionAccountNode({ defaultValue: { kind: 'payerValueNode' }, + identifier: 'payer', isSigner: true, isWritable: false, - name: 'payer', }), ])('should return false for identityValueNode and payerValueNode', acc => { expect(isAccountAutoResolvable(acc)).toBe(false); @@ -28,15 +41,48 @@ describe('isAccountAutoResolvable', () => { test('should return true for pdaValueNode default', () => { const pda = pdaNode({ - name: 'thing', + identifier: 'thing', seeds: [variablePdaSeedNode('owner', publicKeyTypeNode())], }); const acc = instructionAccountNode({ defaultValue: pdaValueNode(pda), + identifier: 'thing', isSigner: false, isWritable: true, - name: 'thing', }); expect(isAccountAutoResolvable(acc)).toBe(true); }); + + test('it returns false for defaults requiring account fetches', () => { + const bump = instructionAccountNode({ + defaultValue: accountBumpValueNode('pda'), + identifier: 'a', + isSigner: false, + isWritable: false, + }); + const data = instructionAccountNode({ + defaultValue: accountDataValueNode('mint', { path: 'owner' }), + identifier: 'a', + isSigner: false, + isWritable: false, + }); + expect(isAccountAutoResolvable(bump)).toBe(false); + expect(isAccountAutoResolvable(data)).toBe(false); + }); + + test('it resolves injected defaults from the provides of the instruction', () => { + const acc = instructionAccountNode({ + defaultValue: injectedValueNode({ key: 'pool' }), + identifier: 'pool', + isSigner: false, + isWritable: false, + }); + const providing = (node: Parameters[1]) => + instructionNode({ accounts: [acc], identifier: 'ix', provides: [providedNode('pool', node)] }); + expect(isAccountAutoResolvable(acc)).toBe(false); + expect(isAccountAutoResolvable(acc, providing(publicKeyValueNode('11111111111111111111111111111111')))).toBe( + true, + ); + expect(isAccountAutoResolvable(acc, providing(payerValueNode()))).toBe(false); + }); }); diff --git a/packages/dynamic-address-resolution/test/codegen/resolver-fn-declaration.test.ts b/packages/dynamic-address-resolution/test/codegen/resolver-fn-declaration.test.ts deleted file mode 100644 index 5989c4116..000000000 --- a/packages/dynamic-address-resolution/test/codegen/resolver-fn-declaration.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { instructionAccountNode, instructionNode, resolverValueNode } from 'codama'; -import { describe, expect, expectTypeOf, test } from 'vitest'; - -import { - generateResolutionInputTypes, - RESOLVER_FN_DECLARATION, -} from '../../src/codegen/generate-resolution-input-types'; -import type { ResolverFn } from '../../src/shared/types'; -import { makeRoot } from '../test-utils'; - -describe('emitted ResolverFn declaration', () => { - test('should emit the ResolverFn declaration verbatim', () => { - expect(RESOLVER_FN_DECLARATION).toMatchInlineSnapshot(` - "type ResolverFn = (argumentsInput: TArgumentsInput, accountsInput: TAccountsInput) => Promise; - - " - `); - }); - - test('should be emitted verbatim when at least one instruction has resolvers', () => { - const root = makeRoot([ - instructionNode({ - accounts: [ - instructionAccountNode({ - defaultValue: resolverValueNode('resolveOwner'), - isSigner: false, - isWritable: false, - name: 'owner', - }), - ], - arguments: [], - name: 'doStuff', - }), - ]); - const output = generateResolutionInputTypes(root); - expect(output).toContain(RESOLVER_FN_DECLARATION); - }); - - test('should match the structural shape of the runtime ResolverFn', () => { - // Mirror of RESOLVER_FN_DECLARATION (which is emitted as a string). - // This asserts the runtime ResolverFn keeps the same structural shape. - type EmittedResolverFn = (argumentsInput: TArgs, accountsInput: TAccs) => Promise; - expectTypeOf>().toEqualTypeOf< - EmittedResolverFn<{ x: number }, { a: string }> - >(); - }); - - test('should not be emitted when no instruction has resolvers', () => { - const root = makeRoot([ - instructionNode({ - accounts: [instructionAccountNode({ isSigner: false, isWritable: false, name: 'authority' })], - arguments: [], - name: 'plain', - }), - ]); - const output = generateResolutionInputTypes(root); - expect(output).not.toContain('type ResolverFn'); - }); -}); diff --git a/packages/dynamic-address-resolution/test/resolvers/resolve-conditional.test.ts b/packages/dynamic-address-resolution/test/resolvers/resolve-conditional.test.ts deleted file mode 100644 index 703b18521..000000000 --- a/packages/dynamic-address-resolution/test/resolvers/resolve-conditional.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - camelCase, - type ConditionalValueNode, - identityValueNode, - type InstructionAccountNode, - instructionAccountNode, - instructionArgumentNode, - instructionNode, - numberTypeNode, - programNode, - rootNode, -} from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { resolveConditionalValueNodeCondition } from '../../src/resolvers/resolve-conditional'; - -const sourceAccount = instructionAccountNode({ - isSigner: false, - isWritable: true, - name: 'source', -}); -const transferIx = instructionNode({ - accounts: [ - sourceAccount, - instructionAccountNode({ - isSigner: false, - isWritable: true, - name: 'destination', - }), - instructionAccountNode({ - defaultValue: identityValueNode(), - isSigner: 'either', - isWritable: false, - name: 'authority', - }), - ], - arguments: [ - instructionArgumentNode({ - name: 'amount', - type: numberTypeNode('u64'), - }), - ], - name: 'transfer', -}); -const tokenRoot = rootNode( - programNode({ - instructions: [transferIx], - name: 'splToken', - publicKey: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', - }), -); - -describe('resolveConditionalValueNodeCondition: INVARIANT_VIOLATION', () => { - test('should throw when conditionalValueNode has no value and no branches', async () => { - const ixAccountNode: InstructionAccountNode = sourceAccount; - - const invalidConditional: ConditionalValueNode = { - condition: { kind: 'accountValueNode', name: camelCase('source') }, - kind: 'conditionalValueNode', - }; - - await expect( - resolveConditionalValueNodeCondition({ - accountsInput: {}, - argumentsInput: {}, - conditionalValueNode: invalidConditional, - ixAccountNode, - ixNode: transferIx, - resolutionPath: [], - resolversInput: {}, - root: tokenRoot, - }), - ).rejects.toThrow( - 'Internal invariant violation: [Invalid conditionalValueNode: missing value and branches for account source in transfer].', - ); - }); -}); diff --git a/packages/dynamic-address-resolution/test/resolvers/resolve-constant-pda-seed-value.test.ts b/packages/dynamic-address-resolution/test/resolvers/resolve-constant-pda-seed-value.test.ts deleted file mode 100644 index 6e4cd209d..000000000 --- a/packages/dynamic-address-resolution/test/resolvers/resolve-constant-pda-seed-value.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { address, getAddressEncoder } from '@solana/addresses'; -import { getUtf8Codec } from '@solana/codecs'; -import { - constantValueNode, - mapValueNode, - noneValueNode, - numberValueNode, - programIdValueNode, - programNode, - rootNode, - someValueNode, - stringTypeNode, - stringValueNode, -} from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { resolveConstantPdaSeedValue } from '../../src/resolvers/resolve-constant-pda-seed-value'; -import { CONSTANT_PDA_SEED_VALUE_SUPPORTED_NODE_KINDS } from '../../src/visitors'; - -const PROGRAM_PUBLIC_KEY = '11111111111111111111111111111111'; -const ctx = { - programId: address(PROGRAM_PUBLIC_KEY), - root: rootNode(programNode({ name: 'test', publicKey: PROGRAM_PUBLIC_KEY })), -}; - -describe('resolveConstantPdaSeedValue', () => { - test('should dispatch to the correct visitor method (stringValueNode)', async () => { - const result = await resolveConstantPdaSeedValue(stringValueNode('hello'), ctx); - expect(result).toEqual(getUtf8Codec().encode('hello')); - }); - - test('should dispatch programIdValueNode and encode ctx programId', async () => { - const result = await resolveConstantPdaSeedValue(programIdValueNode(), ctx); - expect(result).toEqual(getAddressEncoder().encode(ctx.programId)); - }); - - test('should dispatch numberValueNode as a u8 byte', async () => { - const result = await resolveConstantPdaSeedValue(numberValueNode(42), ctx); - expect(result).toEqual(new Uint8Array([42])); - }); - - test('should dispatch someValueNode and recurse into the wrapped value', async () => { - const result = await resolveConstantPdaSeedValue(someValueNode(stringValueNode('hi')), ctx); - expect(result).toEqual(getUtf8Codec().encode('hi')); - }); - - test('should dispatch constantValueNode and recurse into the wrapped value', async () => { - const result = await resolveConstantPdaSeedValue( - constantValueNode(stringTypeNode('utf8'), stringValueNode('hi')), - ctx, - ); - expect(result).toEqual(getUtf8Codec().encode('hi')); - }); - - test('should dispatch noneValueNode as zero bytes', async () => { - const result = await resolveConstantPdaSeedValue(noneValueNode(), ctx); - expect(result).toEqual(new Uint8Array(0)); - }); - - test('should throw UNEXPECTED_NODE_KIND for unsupported value node kind', async () => { - await expect(resolveConstantPdaSeedValue(mapValueNode([]), ctx)).rejects.toThrow( - `Expected node of kind [${CONSTANT_PDA_SEED_VALUE_SUPPORTED_NODE_KINDS.join(',')}], got [mapValueNode]`, - ); - }); -}); diff --git a/packages/dynamic-address-resolution/test/resolvers/resolve-instruction-account-address.test.ts b/packages/dynamic-address-resolution/test/resolvers/resolve-instruction-account-address.test.ts index 7266bef7d..012423a81 100644 --- a/packages/dynamic-address-resolution/test/resolvers/resolve-instruction-account-address.test.ts +++ b/packages/dynamic-address-resolution/test/resolvers/resolve-instruction-account-address.test.ts @@ -1,216 +1,353 @@ -import { CodamaError } from '@codama/errors'; +import { + CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING, + CODAMA_ERROR__DYNAMIC_CLIENT__CIRCULAR_ACCOUNT_DEPENDENCY, + CODAMA_ERROR__DYNAMIC_CLIENT__DATA_MISSING, + CODAMA_ERROR__DYNAMIC_CLIENT__NODE_REFERENCE_NOT_FOUND, + CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ADDRESS_TYPE, + CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_NODE, + CODAMA_ERROR__INJECTED_VALUE_NOT_PROVIDED, + CodamaError, +} from '@codama/errors'; import type { Address } from '@solana/addresses'; -import { address } from '@solana/addresses'; -import { instructionAccountNode, instructionNode, programIdValueNode, programNode, rootNode } from 'codama'; +import { + accountBumpValueNode, + accountDataValueNode, + accountValueNode, + bytesTypeNode, + bytesValueNode, + conditionalValueNode, + dataValueNode, + definedTypeLinkNode, + definedTypeNode, + enumTypeNode, + enumValueNode, + enumVariantTypeNode, + identityValueNode, + injectedValueNode, + instructionNode, + integerTypeNode, + integerValueNode, + payerValueNode, + programIdValueNode, + programLinkNode, + programNode, + providedNode, + publicKeyTypeNode, + publicKeyValueNode, + rootNode, + structFieldTypeNode, + structTypeNode, +} from 'codama'; import { describe, expect, expectTypeOf, test } from 'vitest'; -import type { ResolverFn } from '../../src'; -import type { ResolveInstructionAccountAddressInput } from '../../src/resolvers'; -import { resolveInstructionAccountAddress } from '../../src/resolvers'; -import { generateAddress } from '../test-utils'; +import { resolveInstructionAccountAddress, type ResolveInstructionAccountAddressInput } from '../../src'; +import { account, generateAddress, getAccountPath, PROGRAM_ADDRESS } from '../test-utils'; -const PROGRAM_PUBLIC_KEY = '11111111111111111111111111111111'; -const programAddress = address(PROGRAM_PUBLIC_KEY); -const root = rootNode(programNode({ name: 'test', publicKey: PROGRAM_PUBLIC_KEY })); +const OTHER_ADDRESS = 'SysvarRent111111111111111111111111111111111' as Address; -describe('resolveInstructionAccountAddress', () => { - const ixNode = instructionNode({ name: 'testInstruction' }); +/** Expect the promise to reject with a Codama error with exactly the given context. */ +async function expectCodamaError(promise: Promise, expected: CodamaError): Promise { + await expect(promise).rejects.toThrow(expect.objectContaining({ context: expected.context })); +} - const requiredAccountNode = instructionAccountNode({ - isOptional: false, - isSigner: false, - isWritable: false, - name: 'myAccount', +describe('provided accounts', () => { + test('it returns the provided address', async () => { + const address = await generateAddress(); + const { path } = getAccountPath(account('myAccount', { defaultValue: programIdValueNode() })); + expect(await resolveInstructionAccountAddress({ accountsInput: { myAccount: address }, path })).toBe(address); }); - const optionalAccountNode = instructionAccountNode({ - isOptional: true, - isSigner: false, - isWritable: false, - name: 'myAccount', + test('it accepts PublicKey-like objects', async () => { + const address = await generateAddress(); + const { path } = getAccountPath(account('myAccount')); + const accountsInput = { myAccount: { toBase58: () => address } }; + expect(await resolveInstructionAccountAddress({ accountsInput, path })).toBe(address); }); - const requiredAccountWithDefaultNode = instructionAccountNode({ - defaultValue: programIdValueNode(), - isOptional: false, - isSigner: false, - isWritable: false, - name: 'myAccount', + test('it throws when a required account without default value is missing', async () => { + const { path } = getAccountPath(account('myAccount')); + const error = new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING, { + accountName: path[3].identifier, + instructionName: path[2].identifier, + }); + await expectCodamaError(resolveInstructionAccountAddress({ path }), error); + await expectCodamaError(resolveInstructionAccountAddress({ accountsInput: { myAccount: null }, path }), error); }); +}); - const optionalAccountWithDefaultNode = instructionAccountNode({ - defaultValue: programIdValueNode(), - isOptional: true, - isSigner: false, - isWritable: false, - name: 'myAccount', +describe('optional accounts', () => { + test('it uses the programId strategy by default when null is provided', async () => { + const { path } = getAccountPath(account('myAccount', { isOptional: true })); + expect(await resolveInstructionAccountAddress({ accountsInput: { myAccount: null }, path })).toBe( + PROGRAM_ADDRESS, + ); }); - test('should return the address when accountsInput contains an Address', async () => { - const addr = await generateAddress(); - const result = await resolveInstructionAccountAddress({ - accountsInput: { myAccount: addr }, - ixAccountNode: requiredAccountNode, - ixNode, - root, + test('it uses the omitted strategy when null is provided', async () => { + const { path } = getAccountPath(account('myAccount', { isOptional: true }), { + optionalAccountStrategy: 'omitted', }); - expect(result).toBe(addr); + expect(await resolveInstructionAccountAddress({ accountsInput: { myAccount: null }, path })).toBeNull(); }); - test('should return the address when accountsInput contains a PublicKey-like object', async () => { - const addr = await generateAddress(); - const publicKeyLike = { toBase58: () => addr }; - const result = await resolveInstructionAccountAddress({ - accountsInput: { myAccount: publicKeyLike }, - ixAccountNode: requiredAccountNode, - ixNode, - root, + test('it uses the default value when undefined and the strategy when null', async () => { + const defaultValue = publicKeyValueNode(OTHER_ADDRESS); + const { path } = getAccountPath(account('myAccount', { defaultValue, isOptional: true }), { + optionalAccountStrategy: 'omitted', }); - expect(result).toBe(addr); + expect(await resolveInstructionAccountAddress({ path })).toBe(OTHER_ADDRESS); + expect(await resolveInstructionAccountAddress({ accountsInput: { myAccount: null }, path })).toBeNull(); + }); +}); + +describe('default values', () => { + test('it resolves public keys and program IDs', async () => { + const publicKey = getAccountPath(account('a', { defaultValue: publicKeyValueNode(OTHER_ADDRESS) })).path; + const programId = getAccountPath(account('a', { defaultValue: programIdValueNode() })).path; + expect(await resolveInstructionAccountAddress({ path: publicKey })).toBe(OTHER_ADDRESS); + expect(await resolveInstructionAccountAddress({ path: programId })).toBe(PROGRAM_ADDRESS); }); - test('should return resolved address from defaultValue when account not in input', async () => { - const result = await resolveInstructionAccountAddress({ - accountsInput: {}, - ixAccountNode: requiredAccountWithDefaultNode, - ixNode, - root, + test('it resolves program links to the address of the linked program', async () => { + // Given an account defaulting to another program of the root. + const target = account('tokenProgram', { defaultValue: programLinkNode('token') }); + const { instruction, root: base } = getAccountPath(target); + const root = rootNode(base.program, { + additionalPrograms: [programNode({ identifier: 'token', publicKey: OTHER_ADDRESS })], }); - expect(result).toBe(programAddress); + + // Then it resolves to the address of that program. + const path = [root, root.program, instruction, target] as const; + expect(await resolveInstructionAccountAddress({ path })).toBe(OTHER_ADDRESS); }); - test('should throw ACCOUNT_MISSING when key is absent', async () => { - const ixNodeOmitted = instructionNode({ name: 'testInstruction', optionalAccountStrategy: 'omitted' }); - await expect( - resolveInstructionAccountAddress({ - accountsInput: {}, - ixAccountNode: optionalAccountNode, - ixNode: ixNodeOmitted, - root, - }), - ).rejects.toThrow(/Missing account \[myAccount\] in \[testInstruction\] instruction/); + test('it resolves account values, recursively', async () => { + const address = await generateAddress(); + const target = account('target', { defaultValue: accountValueNode('middle') }); + const { path } = getAccountPath(target, { + accounts: [account('middle', { defaultValue: accountValueNode('source') }), account('source')], + }); + expect(await resolveInstructionAccountAddress({ accountsInput: { source: address }, path })).toBe(address); }); - test('should throw ACCOUNT_MISSING error for required account with no default', async () => { - await expect( - resolveInstructionAccountAddress({ - accountsInput: {}, - ixAccountNode: requiredAccountNode, - ixNode, - root, + test('it throws when an account value refers to an unknown account', async () => { + const { path } = getAccountPath(account('target', { defaultValue: accountValueNode('unknown') })); + await expectCodamaError( + resolveInstructionAccountAddress({ path }), + new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__NODE_REFERENCE_NOT_FOUND, { + instructionName: path[2].identifier, + referencedName: accountValueNode('unknown').identifier, }), - ).rejects.toThrow(CodamaError); + ); }); - test('should return program address when null input for optional account with programId strategy is provided', async () => { - const ixNodeProgramId = instructionNode({ - name: 'testInstruction', - optionalAccountStrategy: 'programId', - }); - const result = await resolveInstructionAccountAddress({ - accountsInput: { myAccount: null }, - ixAccountNode: optionalAccountNode, - ixNode: ixNodeProgramId, - root, - }); - expect(result).toBe(programAddress); + test('it throws on circular account dependencies', async () => { + const target = account('a', { defaultValue: accountValueNode('b') }); + const { path } = getAccountPath(target, { accounts: [account('b', { defaultValue: accountValueNode('a') })] }); + await expectCodamaError( + resolveInstructionAccountAddress({ path }), + new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__CIRCULAR_ACCOUNT_DEPENDENCY, { chain: 'b -> a -> b' }), + ); }); - test('should return null when null input for optional account with omitted strategy is provided', async () => { - const ixNodeOmitted = instructionNode({ name: 'testInstruction', optionalAccountStrategy: 'omitted' }); - const result = await resolveInstructionAccountAddress({ - accountsInput: { myAccount: null }, - ixAccountNode: optionalAccountNode, - ixNode: ixNodeOmitted, - root, - }); - expect(result).toBeNull(); + test('it resolves data values, including nested ones', async () => { + const address = await generateAddress(); + const data = structTypeNode([ + structFieldTypeNode({ + identifier: 'config', + type: structTypeNode([structFieldTypeNode({ identifier: 'owner', type: publicKeyTypeNode() })]), + }), + ]); + const { path } = getAccountPath(account('owner', { defaultValue: dataValueNode('config.owner') }), { data }); + expect(await resolveInstructionAccountAddress({ dataInput: { config: { owner: address } }, path })).toBe( + address, + ); }); - test('should throw ACCOUNT_MISSING error for required account with no default when null is provided', async () => { - await expect( - resolveInstructionAccountAddress({ - accountsInput: { myAccount: null }, - ixAccountNode: requiredAccountNode, - ixNode, - root, + test('it throws when a data value is missing or not an address', async () => { + const data = structTypeNode([structFieldTypeNode({ identifier: 'owner', type: publicKeyTypeNode() })]); + const { path } = getAccountPath(account('owner', { defaultValue: dataValueNode('owner') }), { data }); + await expectCodamaError( + resolveInstructionAccountAddress({ dataInput: {}, path }), + new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__DATA_MISSING, { + instructionName: path[2].identifier, + path: dataValueNode('owner').path, }), - ).rejects.toThrow(CodamaError); + ); + await expectCodamaError( + resolveInstructionAccountAddress({ dataInput: { owner: 42 }, path }), + new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ADDRESS_TYPE, { + accountName: path[3].identifier, + actualType: 'number', + expectedType: 'Address | PublicKey', + }), + ); + }); + + test('it requires the payer and identity to be provided', async () => { + const address = await generateAddress(); + for (const defaultValue of [payerValueNode(), identityValueNode()]) { + const { path } = getAccountPath(account('payer', { defaultValue })); + expect(await resolveInstructionAccountAddress({ accountsInput: { payer: address }, path })).toBe(address); + await expect(resolveInstructionAccountAddress({ path })).rejects.toThrow( + expect.objectContaining({ + context: expect.objectContaining({ __code: CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING }), + }), + ); + } }); - test('should resolve from defaultValue for required account when null is provided', async () => { - const result = await resolveInstructionAccountAddress({ - accountsInput: { myAccount: null }, - ixAccountNode: requiredAccountWithDefaultNode, - ixNode, - root, + test('it does not support account bumps and account data', async () => { + const bump = getAccountPath(account('a', { defaultValue: accountBumpValueNode('pda') })).path; + const data = getAccountPath( + account('a', { defaultValue: accountDataValueNode('mint', { path: 'owner' }) }), + ).path; + await expectCodamaError( + resolveInstructionAccountAddress({ path: bump }), + new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_NODE, { nodeKind: 'accountBumpValueNode' }), + ); + await expectCodamaError( + resolveInstructionAccountAddress({ path: data }), + new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_NODE, { nodeKind: 'accountDataValueNode' }), + ); + }); +}); + +describe('injected values', () => { + test('it resolves injected values provided by the instruction and its parents', async () => { + // Given a sub-instruction account defaulting to a value provided by its parent. + const target = account('pool', { defaultValue: injectedValueNode({ key: 'pool' }) }); + const child = instructionNode({ accounts: [target], identifier: 'child' }); + const parent = instructionNode({ + identifier: 'parent', + provides: [providedNode('pool', publicKeyValueNode(OTHER_ADDRESS))], + subInstructions: [child], }); - expect(result).toBe(programAddress); + const root = rootNode(programNode({ identifier: 'p', instructions: [parent], publicKey: PROGRAM_ADDRESS })); + + // Then it resolves from the provided value. + const path = [root, root.program, parent, child, target] as const; + expect(await resolveInstructionAccountAddress({ path })).toBe(OTHER_ADDRESS); + }); + + test('it throws when the injected value is not provided', async () => { + const injected = injectedValueNode({ key: 'pool' }); + const { path } = getAccountPath(account('pool', { defaultValue: injected })); + await expectCodamaError( + resolveInstructionAccountAddress({ path }), + new CodamaError(CODAMA_ERROR__INJECTED_VALUE_NOT_PROVIDED, { injectedValue: injected, key: injected.key }), + ); }); +}); - test('should resolve from defaultValue for optional account when input is omitted', async () => { - const result = await resolveInstructionAccountAddress({ - accountsInput: {}, - ixAccountNode: optionalAccountWithDefaultNode, - ixNode, - root, +describe('conditional values', () => { + const ifTrue = publicKeyValueNode(OTHER_ADDRESS); + const ifFalse = programIdValueNode(); + const modeData = structTypeNode([structFieldTypeNode({ identifier: 'mode', type: integerTypeNode('u8') })]); + + test('it compares integers regardless of their JavaScript type', async () => { + const defaultValue = conditionalValueNode({ + condition: dataValueNode('mode'), + ifFalse, + ifTrue, + value: integerValueNode('2'), }); - expect(result).toBe(programAddress); + const { path } = getAccountPath(account('a', { defaultValue }), { data: modeData }); + expect(await resolveInstructionAccountAddress({ dataInput: { mode: 2 }, path })).toBe(OTHER_ADDRESS); + expect(await resolveInstructionAccountAddress({ dataInput: { mode: 2n }, path })).toBe(OTHER_ADDRESS); + expect(await resolveInstructionAccountAddress({ dataInput: { mode: 3 }, path })).toBe(PROGRAM_ADDRESS); }); - test('should resolve via programId strategy for optional account with default when null is provided', async () => { - const ixNodeProgramId = instructionNode({ - name: 'testInstruction', - optionalAccountStrategy: 'programId', + test('it compares enum values', async () => { + // Given an account whose default depends on an enum field linking to a program enum. + const mode = definedTypeNode({ + identifier: 'mode', + type: enumTypeNode([enumVariantTypeNode('fast'), enumVariantTypeNode('slow')]), }); - const result = await resolveInstructionAccountAddress({ - accountsInput: { myAccount: null }, - ixAccountNode: optionalAccountWithDefaultNode, - ixNode: ixNodeProgramId, - root, + const data = structTypeNode([structFieldTypeNode({ identifier: 'mode', type: definedTypeLinkNode('mode') })]); + const defaultValue = conditionalValueNode({ + condition: dataValueNode('mode'), + ifFalse, + ifTrue, + value: enumValueNode('mode', 'slow'), }); - expect(result).toBe(programAddress); + const { path } = getAccountPath(account('a', { defaultValue }), { data }, { definedTypes: [mode] }); + + // Then variants compare by identifier, whether given as a string or an object. + expect(await resolveInstructionAccountAddress({ dataInput: { mode: 'slow' }, path })).toBe(OTHER_ADDRESS); + expect(await resolveInstructionAccountAddress({ dataInput: { mode: { __kind: 'slow' } }, path })).toBe( + OTHER_ADDRESS, + ); + expect(await resolveInstructionAccountAddress({ dataInput: { mode: 'fast' }, path })).toBe(PROGRAM_ADDRESS); }); - test('should resolve via omitted strategy for optional account with default when null is provided', async () => { - const ixNodeOmitted = instructionNode({ name: 'testInstruction', optionalAccountStrategy: 'omitted' }); - const result = await resolveInstructionAccountAddress({ - accountsInput: { myAccount: null }, - ixAccountNode: optionalAccountWithDefaultNode, - ixNode: ixNodeOmitted, - root, + test('it checks whether the condition exists without a value', async () => { + const address = await generateAddress(); + const defaultValue = conditionalValueNode({ condition: accountValueNode('source'), ifFalse, ifTrue }); + const { path } = getAccountPath(account('a', { defaultValue }), { + accounts: [account('source', { isOptional: true })], }); - expect(result).toBeNull(); + expect(await resolveInstructionAccountAddress({ accountsInput: { source: address }, path })).toBe( + OTHER_ADDRESS, + ); + expect(await resolveInstructionAccountAddress({ accountsInput: { source: null }, path })).toBe(PROGRAM_ADDRESS); }); -}); -describe('resolveInstructionAccountAddress generics', () => { - type GeneratedAccounts = { authority: Address; mint?: Address | null }; - type GeneratedArgs = { name: string }; - type ResolversFn = { - resolveTags: ResolverFn; - }; - test('should return Promise
regardless of generics', () => { - expectTypeOf(resolveInstructionAccountAddress).returns.toEqualTypeOf>(); + test('it takes the ifFalse branch for accounts that are not provided and have no default value', async () => { + const defaultValue = conditionalValueNode({ condition: accountValueNode('source'), ifFalse, ifTrue }); + const { path } = getAccountPath(account('a', { defaultValue }), { accounts: [account('source')] }); + expect(await resolveInstructionAccountAddress({ accountsInput: {}, path })).toBe(PROGRAM_ADDRESS); }); - test('default-parameter input matches the loose ResolveInstructionAccountAddressInput', () => { - expectTypeOf().toExtend< - Parameters[0] - >(); + test('it compares bytes regardless of their representation', async () => { + const data = structTypeNode([structFieldTypeNode({ identifier: 'tag', type: bytesTypeNode() })]); + const defaultValue = conditionalValueNode({ + condition: dataValueNode('tag'), + ifFalse, + ifTrue, + value: bytesValueNode('base16', '0102'), + }); + const { path } = getAccountPath(account('a', { defaultValue }), { data }); + expect(await resolveInstructionAccountAddress({ dataInput: { tag: new Uint8Array([1, 2]) }, path })).toBe( + OTHER_ADDRESS, + ); + expect(await resolveInstructionAccountAddress({ dataInput: { tag: ['base64', 'AQI='] }, path })).toBe( + OTHER_ADDRESS, + ); + expect(await resolveInstructionAccountAddress({ dataInput: { tag: new Uint8Array([1, 3]) }, path })).toBe( + PROGRAM_ADDRESS, + ); }); - test('narrows accountsInput / argumentsInput / resolversInput to provided generic types', () => { - type NarrowedInput = ResolveInstructionAccountAddressInput; - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); + test('it uses the optional account strategy when no branch matches', async () => { + const defaultValue = conditionalValueNode({ + condition: dataValueNode('mode'), + ifTrue, + value: integerValueNode('2'), + }); + const optional = getAccountPath(account('a', { defaultValue, isOptional: true }), { + data: modeData, + optionalAccountStrategy: 'omitted', + }).path; + const required = getAccountPath(account('a', { defaultValue }), { data: modeData }).path; + expect(await resolveInstructionAccountAddress({ dataInput: { mode: 3 }, path: optional })).toBeNull(); + await expectCodamaError( + resolveInstructionAccountAddress({ dataInput: { mode: 3 }, path: required }), + new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING, { + accountName: required[3].identifier, + instructionName: required[2].identifier, + }), + ); }); +}); - test('resolver callbacks see narrowed args/accounts via TResolvers', () => { - type NarrowedResolvers = NonNullable< - ResolveInstructionAccountAddressInput['resolversInput'] - >; - expectTypeOf().parameters.toEqualTypeOf<[GeneratedArgs, GeneratedAccounts]>(); +describe('types', () => { + test('it narrows the account and data inputs', () => { + type Accounts = { authority: Address }; + type Data = { amount: bigint }; + type Input = ResolveInstructionAccountAddressInput; + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf(resolveInstructionAccountAddress).returns.toEqualTypeOf>(); }); }); diff --git a/packages/dynamic-address-resolution/test/resolvers/resolve-pda-address.test.ts b/packages/dynamic-address-resolution/test/resolvers/resolve-pda-address.test.ts new file mode 100644 index 000000000..ce61ab2ea --- /dev/null +++ b/packages/dynamic-address-resolution/test/resolvers/resolve-pda-address.test.ts @@ -0,0 +1,387 @@ +import { + CODAMA_ERROR__CANNOT_RESOLVE_PATH, + CODAMA_ERROR__DYNAMIC_CLIENT__DATA_MISSING, + CODAMA_ERROR__DYNAMIC_CLIENT__NODE_REFERENCE_NOT_FOUND, + CODAMA_ERROR__DYNAMIC_CLIENT__PDA_SEED_MISSING, + CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ADDRESS_TYPE, + CodamaError, +} from '@codama/errors'; +import { type Address, getAddressEncoder, getProgramDerivedAddress } from '@solana/addresses'; +import { getU16Encoder, getU64Encoder, getUtf8Encoder, type ReadonlyUint8Array } from '@solana/codecs'; +import { + accountValueNode, + constantPdaSeedNode, + dataValueNode, + definedTypeLinkNode, + definedTypeNode, + enumTypeNode, + enumValueNode, + enumVariantTypeNode, + injectedValueNode, + instructionNode, + integerTypeNode, + integerValueNode, + optionTypeNode, + pathString, + pdaLinkNode, + pdaNode, + pdaSeedValueNode, + pdaValueNode, + programIdValueNode, + programLinkNode, + programNode, + providedNode, + publicKeyTypeNode, + publicKeyValueNode, + remainderOptionTypeNode, + rootNode, + sizePrefixTransformNode, + stringTypeNode, + stringValueNode, + structFieldTypeNode, + structFieldValueNode, + structTypeNode, + structValueNode, + variablePdaSeedNode, +} from 'codama'; +import { describe, expect, test } from 'vitest'; + +import { resolveInstructionAccountAddress } from '../../src'; +import { account, generateAddress, getAccountPath, PROGRAM_ADDRESS } from '../test-utils'; + +const OTHER_PROGRAM = 'SysvarRent111111111111111111111111111111111' as Address; +const utf8 = (value: string) => getUtf8Encoder().encode(value); +const pubkey = (value: Address) => getAddressEncoder().encode(value); + +async function derive(seeds: readonly ReadonlyUint8Array[], programAddress: Address = PROGRAM_ADDRESS) { + return (await getProgramDerivedAddress({ programAddress, seeds: [...seeds] }))[0]; +} + +async function expectCodamaError(promise: Promise, expected: CodamaError): Promise { + await expect(promise).rejects.toThrow(expect.objectContaining({ context: expected.context })); +} + +describe('constant seeds', () => { + test('it encodes constant seeds using their type', async () => { + const pda = pdaNode({ + identifier: 'vault', + seeds: [ + constantPdaSeedNode(stringTypeNode('utf8'), stringValueNode('vault')), + constantPdaSeedNode(publicKeyTypeNode(), programIdValueNode()), + constantPdaSeedNode(integerTypeNode('u16'), integerValueNode('300')), + ], + }); + const { path } = getAccountPath( + account('vault', { defaultValue: pdaValueNode(pdaLinkNode('vault')) }), + {}, + { + pdas: [pda], + }, + ); + const expected = await derive([utf8('vault'), pubkey(PROGRAM_ADDRESS), getU16Encoder().encode(300)]); + expect(await resolveInstructionAccountAddress({ path })).toBe(expected); + }); + + test('it encodes enum seeds using the linked enum of their program', async () => { + const kind = definedTypeNode({ + identifier: 'kind', + type: enumTypeNode([enumVariantTypeNode('a'), enumVariantTypeNode('b')]), + }); + const pda = pdaNode({ + identifier: 'vault', + seeds: [constantPdaSeedNode(definedTypeLinkNode('kind'), enumValueNode('kind', 'b'))], + }); + const { path } = getAccountPath( + account('vault', { defaultValue: pdaValueNode(pdaLinkNode('vault')) }), + {}, + { + definedTypes: [kind], + pdas: [pda], + }, + ); + expect(await resolveInstructionAccountAddress({ path })).toBe(await derive([new Uint8Array([1])])); + }); +}); + +describe('variable seeds', () => { + const ownerPda = pdaNode({ identifier: 'vault', seeds: [variablePdaSeedNode('owner', publicKeyTypeNode())] }); + + test('it resolves seeds from accounts', async () => { + const owner = await generateAddress(); + const defaultValue = pdaValueNode(pdaLinkNode('vault'), { + seeds: [pdaSeedValueNode('owner', accountValueNode('owner'))], + }); + const { path } = getAccountPath( + account('vault', { defaultValue }), + { accounts: [account('owner')] }, + { + pdas: [ownerPda], + }, + ); + expect(await resolveInstructionAccountAddress({ accountsInput: { owner }, path })).toBe( + await derive([pubkey(owner)]), + ); + }); + + test('it resolves seeds from nested data using the type of the seed', async () => { + // Given a size-prefixed string within the instruction data, used as a raw string seed. + const pda = pdaNode({ identifier: 'vault', seeds: [variablePdaSeedNode('name', stringTypeNode('utf8'))] }); + const data = structTypeNode([ + structFieldTypeNode({ + identifier: 'config', + type: structTypeNode([ + structFieldTypeNode({ + identifier: 'names', + type: structTypeNode([ + structFieldTypeNode({ + identifier: 'first', + type: stringTypeNode('utf8', { + transforms: [sizePrefixTransformNode(integerTypeNode('u32'))], + }), + }), + ]), + }), + ]), + }), + ]); + const defaultValue = pdaValueNode(pdaLinkNode('vault'), { + seeds: [pdaSeedValueNode('name', dataValueNode('config.names.first'))], + }); + const { path } = getAccountPath(account('vault', { defaultValue }), { data }, { pdas: [pda] }); + + // Then the seed is the raw string, without the size prefix of the data. + const dataInput = { config: { names: { first: 'hello' } } }; + expect(await resolveInstructionAccountAddress({ dataInput, path })).toBe(await derive([utf8('hello')])); + }); + + test('it uses the default value of data fields', async () => { + const pda = pdaNode({ identifier: 'vault', seeds: [variablePdaSeedNode('nonce', integerTypeNode('u64'))] }); + const data = structTypeNode([ + structFieldTypeNode({ + defaultValue: integerValueNode('7'), + identifier: 'nonce', + type: integerTypeNode('u8'), + }), + ]); + const defaultValue = pdaValueNode(pdaLinkNode('vault'), { + seeds: [pdaSeedValueNode('nonce', dataValueNode('nonce'))], + }); + const { path } = getAccountPath(account('vault', { defaultValue }), { data }, { pdas: [pda] }); + expect(await resolveInstructionAccountAddress({ dataInput: {}, path })).toBe( + await derive([getU64Encoder().encode(7)]), + ); + expect(await resolveInstructionAccountAddress({ dataInput: { nonce: 9 }, path })).toBe( + await derive([getU64Encoder().encode(9)]), + ); + }); + + test('it resolves seeds from injected values', async () => { + const owner = await generateAddress(); + const defaultValue = pdaValueNode(pdaLinkNode('vault'), { + seeds: [pdaSeedValueNode('owner', injectedValueNode({ key: 'owner' }))], + }); + const { path } = getAccountPath( + account('vault', { defaultValue }), + { + provides: [providedNode('owner', publicKeyValueNode(owner))], + }, + { pdas: [ownerPda] }, + ); + expect(await resolveInstructionAccountAddress({ path })).toBe(await derive([pubkey(owner)])); + }); + + test('it encodes missing remainder option seeds as zero bytes', async () => { + const pda = pdaNode({ + identifier: 'vault', + seeds: [variablePdaSeedNode('authority', remainderOptionTypeNode(publicKeyTypeNode()))], + }); + const data = structTypeNode([ + structFieldTypeNode({ identifier: 'authority', type: remainderOptionTypeNode(publicKeyTypeNode()) }), + ]); + const defaultValue = pdaValueNode(pdaLinkNode('vault'), { + seeds: [pdaSeedValueNode('authority', dataValueNode('authority'))], + }); + const { path } = getAccountPath(account('vault', { defaultValue }), { data }, { pdas: [pda] }); + expect(await resolveInstructionAccountAddress({ dataInput: {}, path })).toBe(await derive([new Uint8Array(0)])); + }); + + test('it encodes null or missing option seeds as None', async () => { + const pda = pdaNode({ + identifier: 'vault', + seeds: [variablePdaSeedNode('authority', optionTypeNode(publicKeyTypeNode()))], + }); + const data = structTypeNode([ + structFieldTypeNode({ identifier: 'authority', type: optionTypeNode(publicKeyTypeNode()) }), + ]); + const defaultValue = pdaValueNode(pdaLinkNode('vault'), { + seeds: [pdaSeedValueNode('authority', dataValueNode('authority'))], + }); + const { path } = getAccountPath(account('vault', { defaultValue }), { data }, { pdas: [pda] }); + const none = await derive([new Uint8Array([0])]); + expect(await resolveInstructionAccountAddress({ dataInput: { authority: null }, path })).toBe(none); + expect(await resolveInstructionAccountAddress({ dataInput: {}, path })).toBe(none); + }); + + test('it reads nested values from the default value of their parents', async () => { + // Given a seed nested within a field whose default value is a struct. + const owner = await generateAddress(); + const pda = pdaNode({ identifier: 'vault', seeds: [variablePdaSeedNode('owner', publicKeyTypeNode())] }); + const data = structTypeNode([ + structFieldTypeNode({ + defaultValue: structValueNode([structFieldValueNode('owner', publicKeyValueNode(owner))]), + identifier: 'config', + type: structTypeNode([structFieldTypeNode({ identifier: 'owner', type: publicKeyTypeNode() })]), + }), + ]); + const defaultValue = pdaValueNode(pdaLinkNode('vault'), { + seeds: [pdaSeedValueNode('owner', dataValueNode('config.owner'))], + }); + const { path } = getAccountPath(account('vault', { defaultValue }), { data }, { pdas: [pda] }); + + // Then the seed uses the parent's default value when the parent is missing, as its codec would. + expect(await resolveInstructionAccountAddress({ dataInput: {}, path })).toBe(await derive([pubkey(owner)])); + const other = await generateAddress(); + expect(await resolveInstructionAccountAddress({ dataInput: { config: { owner: other } }, path })).toBe( + await derive([pubkey(other)]), + ); + }); + + test('it throws when a seed value is missing', async () => { + const data = structTypeNode([structFieldTypeNode({ identifier: 'owner', type: publicKeyTypeNode() })]); + const defaultValue = pdaValueNode(pdaLinkNode('vault'), { + seeds: [pdaSeedValueNode('owner', dataValueNode('owner'))], + }); + const { path } = getAccountPath(account('vault', { defaultValue }), { data }, { pdas: [ownerPda] }); + await expectCodamaError( + resolveInstructionAccountAddress({ dataInput: {}, path }), + new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__PDA_SEED_MISSING, { + pdaName: ownerPda.identifier, + seedName: ownerPda.seeds![0].identifier, + }), + ); + }); + + test('it throws when a seed value is not provided by the PDA value', async () => { + const { path } = getAccountPath( + account('vault', { defaultValue: pdaValueNode(pdaLinkNode('vault')) }), + {}, + { + pdas: [ownerPda], + }, + ); + await expectCodamaError( + resolveInstructionAccountAddress({ path }), + new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__NODE_REFERENCE_NOT_FOUND, { + instructionName: path[2].identifier, + referencedName: ownerPda.seeds![0].identifier, + }), + ); + }); + + test('it throws when a data path does not exist', async () => { + const data = structTypeNode([structFieldTypeNode({ identifier: 'owner', type: publicKeyTypeNode() })]); + const defaultValue = pdaValueNode(pdaLinkNode('vault'), { + seeds: [pdaSeedValueNode('owner', dataValueNode('missing'))], + }); + const { path } = getAccountPath(account('vault', { defaultValue }), { data }, { pdas: [ownerPda] }); + await expect(resolveInstructionAccountAddress({ dataInput: {}, path })).rejects.toThrow( + expect.objectContaining({ + context: expect.objectContaining({ + __code: CODAMA_ERROR__CANNOT_RESOLVE_PATH, + path: pathString('missing'), + segment: 'missing', + }), + }), + ); + }); +}); + +describe('program addresses', () => { + const pda = pdaNode({ + identifier: 'vault', + seeds: [constantPdaSeedNode(stringTypeNode('utf8'), stringValueNode('v'))], + }); + + test('it uses the program ID of the PDA value, from data or accounts', async () => { + const data = structTypeNode([structFieldTypeNode({ identifier: 'program', type: publicKeyTypeNode() })]); + const fromData = pdaValueNode(pdaLinkNode('vault'), { programId: dataValueNode('program') }); + const fromAccount = pdaValueNode(pdaLinkNode('vault'), { programId: accountValueNode('program') }); + const expected = await derive([utf8('v')], OTHER_PROGRAM); + + const dataPath = getAccountPath(account('vault', { defaultValue: fromData }), { data }, { pdas: [pda] }).path; + expect(await resolveInstructionAccountAddress({ dataInput: { program: OTHER_PROGRAM }, path: dataPath })).toBe( + expected, + ); + const accountPath = getAccountPath( + account('vault', { defaultValue: fromAccount }), + { + accounts: [account('program')], + }, + { pdas: [pda] }, + ).path; + expect( + await resolveInstructionAccountAddress({ accountsInput: { program: OTHER_PROGRAM }, path: accountPath }), + ).toBe(expected); + }); + + test('it throws when the program ID data is not an address', async () => { + const data = structTypeNode([structFieldTypeNode({ identifier: 'program', type: publicKeyTypeNode() })]); + const defaultValue = pdaValueNode(pdaLinkNode('vault'), { programId: dataValueNode('program') }); + const { path } = getAccountPath(account('vault', { defaultValue }), { data }, { pdas: [pda] }); + await expectCodamaError( + resolveInstructionAccountAddress({ dataInput: { program: 42 }, path }), + new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ADDRESS_TYPE, { + accountName: path[3].identifier, + actualType: 'number', + expectedType: 'Address | PublicKey', + }), + ); + }); + + test('it encodes constant program ID seeds with the resolved program address', async () => { + const withProgramIdSeed = pdaNode({ + identifier: 'vault', + seeds: [constantPdaSeedNode(publicKeyTypeNode(), programIdValueNode())], + }); + const data = structTypeNode([structFieldTypeNode({ identifier: 'program', type: publicKeyTypeNode() })]); + const defaultValue = pdaValueNode(pdaLinkNode('vault'), { programId: dataValueNode('program') }); + const { path } = getAccountPath(account('vault', { defaultValue }), { data }, { pdas: [withProgramIdSeed] }); + expect(await resolveInstructionAccountAddress({ dataInput: { program: OTHER_PROGRAM }, path })).toBe( + await derive([pubkey(OTHER_PROGRAM)], OTHER_PROGRAM), + ); + }); + + test('it throws when the program ID data is missing', async () => { + const data = structTypeNode([structFieldTypeNode({ identifier: 'program', type: publicKeyTypeNode() })]); + const defaultValue = pdaValueNode(pdaLinkNode('vault'), { programId: dataValueNode('program') }); + const { path } = getAccountPath(account('vault', { defaultValue }), { data }, { pdas: [pda] }); + await expectCodamaError( + resolveInstructionAccountAddress({ dataInput: {}, path }), + new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__DATA_MISSING, { + instructionName: path[2].identifier, + path: pathString('program'), + }), + ); + }); + + test('it uses the program of linked PDAs', async () => { + // Given an instruction of programA using a PDA of programB. + const target = account('vault', { + defaultValue: pdaValueNode(pdaLinkNode('vault', { program: programLinkNode('b') })), + }); + const instruction = instructionNode({ accounts: [target], identifier: 'ix' }); + const programA = programNode({ identifier: 'a', instructions: [instruction], publicKey: PROGRAM_ADDRESS }); + const programB = programNode({ identifier: 'b', pdas: [pda], publicKey: OTHER_PROGRAM }); + const root = rootNode(programA, { additionalPrograms: [programB] }); + + // Then the PDA is derived from programB. + const path = [root, programA, instruction, target] as const; + expect(await resolveInstructionAccountAddress({ path })).toBe(await derive([utf8('v')], OTHER_PROGRAM)); + }); + + test('it uses the program ID of the PDA when provided', async () => { + const withProgramId = pdaNode({ ...pda, programId: OTHER_PROGRAM }); + const { path } = getAccountPath(account('vault', { defaultValue: pdaValueNode(withProgramId) })); + expect(await resolveInstructionAccountAddress({ path })).toBe(await derive([utf8('v')], OTHER_PROGRAM)); + }); +}); diff --git a/packages/dynamic-address-resolution/test/resolvers/resolve-standalone-pda.test.ts b/packages/dynamic-address-resolution/test/resolvers/resolve-standalone-pda.test.ts index 8f94e692d..6085a69e0 100644 --- a/packages/dynamic-address-resolution/test/resolvers/resolve-standalone-pda.test.ts +++ b/packages/dynamic-address-resolution/test/resolvers/resolve-standalone-pda.test.ts @@ -1,10 +1,10 @@ -import { address, getAddressEncoder, getProgramDerivedAddress } from '@solana/addresses'; -import { getU32Encoder, getUtf8Codec } from '@solana/codecs'; +import { CODAMA_ERROR__DYNAMIC_CLIENT__PDA_SEED_MISSING, CodamaError } from '@codama/errors'; +import { getAddressEncoder, getProgramDerivedAddress } from '@solana/addresses'; +import { getU32Encoder, getUtf8Encoder } from '@solana/codecs'; import { constantPdaSeedNode, - mapValueNode, - numberTypeNode, - numberValueNode, + integerTypeNode, + integerValueNode, pdaNode, programIdValueNode, programNode, @@ -18,102 +18,78 @@ import { } from 'codama'; import { describe, expect, test } from 'vitest'; -import { resolveStandalonePda } from '../../src/resolvers/resolve-standalone-pda'; -import { generateAddress } from '../test-utils'; +import { resolveStandalonePda } from '../../src'; +import { generateAddress, PROGRAM_ADDRESS } from '../test-utils'; -const PROGRAM_ADDRESS = address('11111111111111111111111111111111'); - -function makeRoot() { - return rootNode(programNode({ name: 'test', publicKey: PROGRAM_ADDRESS })); +function getPdaPath(pda: ReturnType) { + const root = rootNode(programNode({ identifier: 'test', pdas: [pda], publicKey: PROGRAM_ADDRESS })); + return [root, root.program, pda] as const; } describe('resolveStandalonePda', () => { - test('should derive PDA from mixed constant seeds', async () => { + test('it derives PDAs from constant seeds', async () => { const extraKey = await generateAddress(); const pda = pdaNode({ - name: 'mixed', + identifier: 'mixed', seeds: [ constantPdaSeedNode(stringTypeNode('utf8'), stringValueNode('prefix')), constantPdaSeedNode(publicKeyTypeNode(), programIdValueNode()), - constantPdaSeedNode(numberTypeNode('u8'), numberValueNode(7)), + constantPdaSeedNode(integerTypeNode('u8'), integerValueNode('7')), constantPdaSeedNode(publicKeyTypeNode(), publicKeyValueNode(extraKey)), ], }); - const expected = await getProgramDerivedAddress({ programAddress: PROGRAM_ADDRESS, seeds: [ - getUtf8Codec().encode('prefix'), + getUtf8Encoder().encode('prefix'), getAddressEncoder().encode(PROGRAM_ADDRESS), new Uint8Array([7]), getAddressEncoder().encode(extraKey), ], }); - - const result = await resolveStandalonePda(makeRoot(), pda); - expect(result).toEqual(expected); + expect(await resolveStandalonePda({ path: getPdaPath(pda) })).toEqual(expected); }); - test('should derive PDA from variable string seed', async () => { + test('it derives PDAs from variable seeds', async () => { const pda = pdaNode({ - name: 'test', - seeds: [variablePdaSeedNode('label', stringTypeNode('utf8'))], + identifier: 'test', + seeds: [ + variablePdaSeedNode('label', stringTypeNode('utf8')), + variablePdaSeedNode('count', integerTypeNode('u32')), + ], }); - const expected = await getProgramDerivedAddress({ programAddress: PROGRAM_ADDRESS, - seeds: [getUtf8Codec().encode('hello')], + seeds: [getUtf8Encoder().encode('hello'), getU32Encoder().encode(123_456)], }); - - const result = await resolveStandalonePda(makeRoot(), pda, { label: 'hello' }); - expect(result).toEqual(expected); - }); - - test('should derive PDA from variable number seed', async () => { - const pda = pdaNode({ - name: 'test', - seeds: [variablePdaSeedNode('count', numberTypeNode('u32'))], - }); - - const expected = await getProgramDerivedAddress({ - programAddress: PROGRAM_ADDRESS, - seeds: [getU32Encoder().encode(123_456)], + const result = await resolveStandalonePda({ + path: getPdaPath(pda), + seedsInput: { count: 123_456, label: 'hello' }, }); - - const result = await resolveStandalonePda(makeRoot(), pda, { count: 123_456 }); expect(result).toEqual(expected); }); - test('should encode optional remainderOptionTypeNode seed as zero bytes when input is missing', async () => { + test('it encodes missing remainder option seeds as zero bytes', async () => { const pda = pdaNode({ - name: 'optional', + identifier: 'optional', seeds: [variablePdaSeedNode('maybe', remainderOptionTypeNode(stringTypeNode('utf8')))], }); - const expected = await getProgramDerivedAddress({ programAddress: PROGRAM_ADDRESS, seeds: [new Uint8Array(0)], }); - - const result = await resolveStandalonePda(makeRoot(), pda); - expect(result).toEqual(expected); - }); - - test('should throw UNEXPECTED_NODE_KIND for unsupported constant seed value kind', async () => { - const pda = pdaNode({ - name: 'bad', - seeds: [constantPdaSeedNode(numberTypeNode('u8'), mapValueNode([])) as never], - }); - - await expect(resolveStandalonePda(makeRoot(), pda)).rejects.toThrow(/mapValueNode/); + expect(await resolveStandalonePda({ path: getPdaPath(pda) })).toEqual(expected); }); - test('should throw ARGUMENT_MISSING when a required variable seed input is omitted', async () => { - const pda = pdaNode({ - name: 'requiresInput', - seeds: [variablePdaSeedNode('owner', publicKeyTypeNode())], - }); - - await expect(resolveStandalonePda(makeRoot(), pda)).rejects.toThrow(/owner/); + test('it throws when a variable seed is missing', async () => { + const pda = pdaNode({ identifier: 'vault', seeds: [variablePdaSeedNode('owner', publicKeyTypeNode())] }); + await expect(resolveStandalonePda({ path: getPdaPath(pda) })).rejects.toThrow( + expect.objectContaining({ + context: new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__PDA_SEED_MISSING, { + pdaName: pda.identifier, + seedName: pda.seeds![0].identifier, + }).context, + }), + ); }); }); diff --git a/packages/dynamic-address-resolution/test/shared/bytes-encoding.test.ts b/packages/dynamic-address-resolution/test/shared/bytes-encoding.test.ts deleted file mode 100644 index 3e0ac4582..000000000 --- a/packages/dynamic-address-resolution/test/shared/bytes-encoding.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import { getCodecFromBytesEncoding, isUint8Array, uint8ArrayToEncodedString } from '../../src/shared/bytes-encoding'; - -describe('uint8ArrayToEncodedString', () => { - const helloBytes = new Uint8Array([72, 101, 108, 108, 111]); - - test('should encode to base16', () => { - expect(uint8ArrayToEncodedString(helloBytes, 'base16')).toBe('48656c6c6f'); - }); - - test('should encode to base58', () => { - expect(uint8ArrayToEncodedString(helloBytes, 'base58')).toBe('9Ajdvzr'); - }); - - test('should encode to base64', () => { - expect(uint8ArrayToEncodedString(helloBytes, 'base64')).toBe('SGVsbG8='); - }); - - test('should encode to utf8', () => { - expect(uint8ArrayToEncodedString(helloBytes, 'utf8')).toBe('Hello'); - }); - - test('should handle empty bytes', () => { - expect(uint8ArrayToEncodedString(new Uint8Array(), 'base16')).toBe(''); - }); -}); - -describe('getCodecFromBytesEncoding', () => { - test('should return codec for base16', () => { - const codec = getCodecFromBytesEncoding('base16'); - expect(codec).toBeDefined(); - expect(codec.encode('ff')).toEqual(new Uint8Array([255])); - }); - - test('should return codec for base58', () => { - const codec = getCodecFromBytesEncoding('base58'); - expect(codec).toBeDefined(); - }); - - test('should return codec for base64', () => { - const codec = getCodecFromBytesEncoding('base64'); - expect(codec).toBeDefined(); - }); - - test('should return codec for utf8', () => { - const codec = getCodecFromBytesEncoding('utf8'); - expect(codec).toBeDefined(); - expect(codec.encode('Hi')).toEqual(new Uint8Array([72, 105])); - }); - - test('should throw for unsupported encoding', () => { - // @ts-expect-error testing invalid input - expect(() => getCodecFromBytesEncoding('rot13')).toThrow(/Unrecognized bytes encoding \[.*rot13.*\]/); - }); -}); - -describe('isUint8Array', () => { - test('should return true for Uint8Array', () => { - expect(isUint8Array(new Uint8Array([1, 2, 3]))).toBe(true); - expect(isUint8Array(new Uint8Array())).toBe(true); - }); - - test('should return false for regular arrays', () => { - expect(isUint8Array([1, 2, 3])).toBe(false); - }); - - test('should return false for strings', () => { - expect(isUint8Array('hello')).toBe(false); - }); - - test('should return false for null and undefined', () => { - expect(isUint8Array(null)).toBe(false); - expect(isUint8Array(undefined)).toBe(false); - }); - - test('should return false for other typed arrays', () => { - expect(isUint8Array(new Uint16Array([1, 2]))).toBe(false); - expect(isUint8Array(new Int8Array([1, 2]))).toBe(false); - }); -}); diff --git a/packages/dynamic-address-resolution/test/shared/codecs.test.ts b/packages/dynamic-address-resolution/test/shared/codecs.test.ts deleted file mode 100644 index ece155625..000000000 --- a/packages/dynamic-address-resolution/test/shared/codecs.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import { - getMemoizedAddressEncoder, - getMemoizedBase16Codec, - getMemoizedBase58Codec, - getMemoizedBase64Codec, - getMemoizedBooleanEncoder, - getMemoizedUtf8Codec, - getMemoizedUtf8Encoder, -} from '../../src/shared/codecs'; -import { generateAddress } from '../test-utils'; - -describe('shared codecs (memoized):', () => { - test('should encode a base58 address to 32 bytes (getMemoizedAddressEncoder)', async () => { - const addr = await generateAddress(); - const encoder = getMemoizedAddressEncoder(); - const result = encoder.encode(addr); - expect(result.length).toBe(32); - }); - - test('should encode true to a single byte (getMemoizedBooleanEncoder)', () => { - const encoder = getMemoizedBooleanEncoder(); - expect(encoder.encode(true)).toEqual(new Uint8Array([1])); - }); - - test.each([ - { - expected: new Uint8Array([72, 101, 108, 108, 111]), - getCodec: getMemoizedUtf8Encoder, - input: 'Hello', - name: 'getMemoizedUtf8Encoder', - }, - { - expected: new Uint8Array([72, 105]), - getCodec: getMemoizedUtf8Codec, - input: 'Hi', - name: 'getMemoizedUtf8Codec', - }, - ])('should encode a string to utf8 bytes ($name)', ({ getCodec, input, expected }) => { - expect(getCodec().encode(input)).toEqual(expected); - }); - - test('should encode a hex string to bytes (getMemoizedBase16Codec)', () => { - const codec = getMemoizedBase16Codec(); - expect(codec.encode('ff')).toEqual(new Uint8Array([255])); - }); - - test('should encode a base58 string to bytes (getMemoizedBase58Codec)', () => { - const codec = getMemoizedBase58Codec(); - expect(codec.encode('1')).toEqual(new Uint8Array([0])); - }); - - test('should encode a base64 string to bytes (getMemoizedBase64Codec)', () => { - const codec = getMemoizedBase64Codec(); - expect(codec.encode('AQ==')).toEqual(new Uint8Array([1])); - }); -}); diff --git a/packages/dynamic-address-resolution/test/shared/types.test.ts b/packages/dynamic-address-resolution/test/shared/types.test.ts index f21efe24e..95fd91d7c 100644 --- a/packages/dynamic-address-resolution/test/shared/types.test.ts +++ b/packages/dynamic-address-resolution/test/shared/types.test.ts @@ -2,47 +2,21 @@ import type { Address } from '@solana/addresses'; import { describe, expectTypeOf, test } from 'vitest'; import type { AddressInput } from '../../src/shared/address'; -import type { AccountsInput, ArgumentsInput, ResolverFn, ResolversInput } from '../../src/shared/types'; +import type { AccountsInput, DataInput } from '../../src/shared/types'; describe('AccountsInput', () => { - test('should accept partial record of string to AddressInput | null', () => { + test('it accepts a partial record of AddressInput or null', () => { expectTypeOf().toExtend>>(); - }); - - test('should allow null values for optional accounts', () => { expectTypeOf<{ mint: null }>().toExtend(); - }); - - test('should allow AddressInput values', () => { expectTypeOf<{ mint: Address }>().toExtend(); - }); - - test('should allow partial (missing keys)', () => { // eslint-disable-next-line @typescript-eslint/no-empty-object-type expectTypeOf<{}>().toExtend(); }); }); -describe('ArgumentsInput', () => { - test('should accept partial record of string to unknown', () => { - expectTypeOf().toExtend>>(); - }); - - test('should allow any value types', () => { - expectTypeOf<{ amount: bigint; name: string }>().toExtend(); - }); -}); - -describe('ResolverFn', () => { - test('should accept arguments and accounts input and return Promise', () => { - expectTypeOf().toBeFunction(); - expectTypeOf().parameters.toEqualTypeOf<[ArgumentsInput, AccountsInput]>(); - expectTypeOf().returns.toEqualTypeOf>(); - }); -}); - -describe('ResolversInput', () => { - test('should be a record of string to ResolverFn', () => { - expectTypeOf().toEqualTypeOf>(); +describe('DataInput', () => { + test('it accepts any codec input', () => { + expectTypeOf<{ amount: bigint; name: string }>().toExtend(); + expectTypeOf<[number, string]>().toExtend(); }); }); diff --git a/packages/dynamic-address-resolution/test/shared/util.test.ts b/packages/dynamic-address-resolution/test/shared/util.test.ts index 1e9876a76..c7c420eeb 100644 --- a/packages/dynamic-address-resolution/test/shared/util.test.ts +++ b/packages/dynamic-address-resolution/test/shared/util.test.ts @@ -1,6 +1,6 @@ import { describe, expect, expectTypeOf, test } from 'vitest'; -import { formatValueType, isObjectRecord, safeStringify } from '../../src/shared/util'; +import { formatValueType, isObjectRecord, isValueEqual, safeStringify } from '../../src/shared/util'; describe('isObjectRecord', () => { test('should return true for plain objects', () => { @@ -103,3 +103,38 @@ describe('safeStringify', () => { expectTypeOf(safeStringify).returns.toBeString(); }); }); + +describe('isValueEqual', () => { + test('it compares integers regardless of their JavaScript type', () => { + expect(isValueEqual(2, 2n)).toBe(true); + expect(isValueEqual(2n, 2)).toBe(true); + expect(isValueEqual(2, 3n)).toBe(false); + expect(isValueEqual(1.5, 1.5)).toBe(true); + }); + + test('it compares enum variants by identifier', () => { + expect(isValueEqual('slow', { __discriminator: 1, __kind: 'slow' })).toBe(true); + expect(isValueEqual({ __kind: 'slow' }, { __discriminator: 1, __kind: 'slow' })).toBe(true); + expect(isValueEqual('fast', { __discriminator: 1, __kind: 'slow' })).toBe(false); + expect(isValueEqual('move', { __discriminator: 1, __kind: 'move', data: 1n })).toBe(false); + expect(isValueEqual({ __kind: 'move', data: 1 }, { __discriminator: 1, __kind: 'move', data: 1n })).toBe(true); + }); + + test('it compares arrays and objects deeply', () => { + expect(isValueEqual([1, { a: 2 }], [1n, { a: 2n }])).toBe(true); + expect(isValueEqual([1], [1, 2])).toBe(false); + expect(isValueEqual({ a: 1 }, { a: 1n, b: 2n })).toBe(false); + }); + + test('it compares bytes by content', () => { + expect(isValueEqual(new Uint8Array([1, 2]), ['base16', '0102'])).toBe(true); + expect(isValueEqual(['base64', 'AQI='], ['base16', '0102'])).toBe(true); + expect(isValueEqual(new Uint8Array([1, 2]), ['base16', '0103'])).toBe(false); + }); + + test('it compares other values strictly', () => { + expect(isValueEqual('a', 'a')).toBe(true); + expect(isValueEqual(true, true)).toBe(true); + expect(isValueEqual(null, undefined)).toBe(false); + }); +}); diff --git a/packages/dynamic-address-resolution/test/test-utils.ts b/packages/dynamic-address-resolution/test/test-utils.ts index 88ca4bfa2..230c5c3eb 100644 --- a/packages/dynamic-address-resolution/test/test-utils.ts +++ b/packages/dynamic-address-resolution/test/test-utils.ts @@ -1,18 +1,60 @@ import type { Address } from '@solana/addresses'; import { generateKeyPairSigner } from '@solana/kit'; -import { instructionNode, programNode, rootNode } from 'codama'; +import { + type InstructionAccountNode, + instructionAccountNode, + type InstructionAccountNodeInput, + type InstructionNode, + instructionNode, + type InstructionNodeInput, + type ProgramNode, + programNode, + type ProgramNodeInput, + type RootNode, + rootNode, +} from 'codama'; + +export const PROGRAM_ADDRESS = '11111111111111111111111111111111' as Address; export async function generateAddress(): Promise
{ const signer = await generateKeyPairSigner(); return signer.address; } -export function makeRoot(instructions: ReturnType[], name = 'testProgram') { - return rootNode( +export function makeRoot(instructions: InstructionNode[], identifier = 'testProgram') { + return rootNode(programNode({ identifier, instructions, publicKey: PROGRAM_ADDRESS })); +} + +/** An account that is neither a signer nor writable. */ +export function account(identifier: string, input: Partial = {}): InstructionAccountNode { + return instructionAccountNode({ identifier, isSigner: false, isWritable: false, ...input }); +} + +/** + * Build a root node with a single instruction, and return the path of the + * given account within it, e.g. to resolve its address. + */ +export function getAccountPath( + ixAccountNode: InstructionAccountNode, + instructionInput: Partial = {}, + programInput: Partial = {}, +): { + instruction: InstructionNode; + path: readonly [RootNode, ProgramNode, InstructionNode, InstructionAccountNode]; + root: RootNode; +} { + const instruction = instructionNode({ + identifier: 'testInstruction', + ...instructionInput, + accounts: [...(instructionInput.accounts ?? []), ixAccountNode], + }); + const root = rootNode( programNode({ - instructions, - name, - publicKey: '11111111111111111111111111111111', + identifier: 'testProgram', + publicKey: PROGRAM_ADDRESS, + ...programInput, + instructions: [instruction], }), ); + return { instruction, path: [root, root.program, instruction, ixAccountNode], root }; } diff --git a/packages/dynamic-address-resolution/test/visitors/account-default-value/account-default-value-test-utils.ts b/packages/dynamic-address-resolution/test/visitors/account-default-value/account-default-value-test-utils.ts deleted file mode 100644 index bc041db16..000000000 --- a/packages/dynamic-address-resolution/test/visitors/account-default-value/account-default-value-test-utils.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { address } from '@solana/addresses'; -import type { InstructionAccountNode } from 'codama'; -import { instructionAccountNode, instructionNode, programNode, rootNode } from 'codama'; - -import { createAccountDefaultValueVisitor } from '../../../src/visitors/account-default-value'; - -export const programAddress = address('11111111111111111111111111111111'); -export const rootNodeMock = rootNode(programNode({ name: 'test', publicKey: programAddress })); - -export const ixNodeStub = instructionNode({ name: 'testInstruction' }); - -export const ixAccountNodeStub: InstructionAccountNode = instructionAccountNode({ - isOptional: false, - isSigner: false, - isWritable: false, - name: 'testAccount', -}); - -export function makeVisitor(overrides?: Partial[0]>) { - return createAccountDefaultValueVisitor({ - accountsInput: undefined, - argumentsInput: undefined, - ixAccountNode: ixAccountNodeStub, - ixNode: ixNodeStub, - resolutionPath: [], - resolversInput: undefined, - root: rootNodeMock, - ...overrides, - }); -} diff --git a/packages/dynamic-address-resolution/test/visitors/account-default-value/accountBumpValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/account-default-value/accountBumpValueNode.test.ts deleted file mode 100644 index 869fd0ebd..000000000 --- a/packages/dynamic-address-resolution/test/visitors/account-default-value/accountBumpValueNode.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { accountBumpValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './account-default-value-test-utils'; - -describe('account-default-value: visitAccountBumpValue', () => { - test('should throw "not yet supported"', async () => { - const visitor = makeVisitor(); - await expect(visitor.visitAccountBumpValue(accountBumpValueNode('seed'))).rejects.toThrow( - /Unsupported node kind \[accountBumpValueNode\]/, - ); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/account-default-value/accountValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/account-default-value/accountValueNode.test.ts deleted file mode 100644 index 897896fd6..000000000 --- a/packages/dynamic-address-resolution/test/visitors/account-default-value/accountValueNode.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { accountValueNode, instructionAccountNode, instructionNode, publicKeyValueNode } from 'codama'; -import { beforeAll, describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeVisitor } from './account-default-value-test-utils'; - -describe('account-default-value: visitAccountValue', () => { - let refAddress: Awaited>; - - beforeAll(async () => { - refAddress = await generateAddress(); - }); - - const ixNodeWithAccount = instructionNode({ - accounts: [ - instructionAccountNode({ - isOptional: false, - isSigner: false, - isWritable: false, - name: 'refAccount', - }), - ], - name: 'testInstruction', - }); - - test('should return address when user provides address in accountsInput', async () => { - const visitor = makeVisitor({ - accountsInput: { refAccount: refAddress }, - ixNode: ixNodeWithAccount, - }); - const result = await visitor.visitAccountValue(accountValueNode('refAccount')); - expect(result).toBe(refAddress); - }); - - test('should return null for optional account with null input and omitted strategy', async () => { - const ixNodeWithOptional = instructionNode({ - accounts: [ - instructionAccountNode({ - isOptional: true, - isSigner: false, - isWritable: false, - name: 'refAccount', - }), - ], - name: 'testInstruction', - optionalAccountStrategy: 'omitted', - }); - const visitor = makeVisitor({ - accountsInput: { refAccount: null }, - ixNode: ixNodeWithOptional, - }); - const result = await visitor.visitAccountValue(accountValueNode('refAccount')); - expect(result).toBeNull(); - }); - - test('should resolve referenced account defaultValue', async () => { - const expectedDefaultAddress = await generateAddress(); - const ixNodeWithDefault = instructionNode({ - accounts: [ - instructionAccountNode({ - defaultValue: publicKeyValueNode(expectedDefaultAddress), - isOptional: false, - isSigner: false, - isWritable: false, - name: 'refAccount', - }), - ], - name: 'testInstruction', - }); - const visitor = makeVisitor({ ixNode: ixNodeWithDefault }); - const result = await visitor.visitAccountValue(accountValueNode('refAccount')); - expect(result).toBe(expectedDefaultAddress); - }); - - test('should throw for unknown account reference', async () => { - const visitor = makeVisitor(); - await expect(visitor.visitAccountValue(accountValueNode('unknown'))).rejects.toThrow( - /Referenced node \[unknown\] not found in \[testInstruction\]/, - ); - }); - - test('should throw on circular dependency', async () => { - const visitor = makeVisitor({ - ixNode: ixNodeWithAccount, - resolutionPath: ['refAccount'], - }); - await expect(visitor.visitAccountValue(accountValueNode('refAccount'))).rejects.toThrow( - /Circular dependency detected: \[/, - ); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/account-default-value/argumentValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/account-default-value/argumentValueNode.test.ts deleted file mode 100644 index 2965574fd..000000000 --- a/packages/dynamic-address-resolution/test/visitors/account-default-value/argumentValueNode.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { argumentValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeVisitor } from './account-default-value-test-utils'; - -describe('account-default-value: visitArgumentValue', () => { - test('should return address from argument value', async () => { - const addr = await generateAddress(); - const visitor = makeVisitor({ argumentsInput: { myArg: addr } }); - const result = await visitor.visitArgumentValue(argumentValueNode('myArg')); - expect(result).toBe(addr); - }); - - test('should throw when argument is missing', async () => { - const visitor = makeVisitor({ argumentsInput: {} }); - await expect(visitor.visitArgumentValue(argumentValueNode('myArg'))).rejects.toThrow( - /Missing argument \[myArg\] in \[testInstruction\]/, - ); - }); - - test('should throw when argument is null', async () => { - const visitor = makeVisitor({ argumentsInput: { myArg: null } }); - await expect(visitor.visitArgumentValue(argumentValueNode('myArg'))).rejects.toThrow( - /Missing argument \[myArg\] in \[testInstruction\]/, - ); - }); - - test('should throw when argument cannot be converted to Address', async () => { - const visitors: ReturnType[] = [ - makeVisitor({ argumentsInput: { myArg: 'not-a-valid-base58' } }), - makeVisitor({ argumentsInput: { myArg: { a: 42 } } }), - ]; - for (const visitor of visitors) { - await expect(visitor.visitArgumentValue(argumentValueNode('myArg'))).rejects.toThrow( - /Expected \[Address \| PublicKey\] for account \[testAccount\]/, - ); - } - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/account-default-value/conditionalValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/account-default-value/conditionalValueNode.test.ts deleted file mode 100644 index 52c9933a5..000000000 --- a/packages/dynamic-address-resolution/test/visitors/account-default-value/conditionalValueNode.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { - argumentValueNode, - conditionalValueNode, - instructionAccountNode, - numberValueNode, - publicKeyValueNode, - resolverValueNode, -} from 'codama'; -import { beforeAll, describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { ixAccountNodeStub, makeVisitor } from './account-default-value-test-utils'; - -describe('account-default-value: visitConditionalValue', () => { - let ifTrueAddress: Awaited>; - let ifFalseAddress: Awaited>; - - beforeAll(async () => { - ifTrueAddress = await generateAddress(); - ifFalseAddress = await generateAddress(); - }); - - test('should resolve ifTrue branch when argument condition is truthy', async () => { - const node = conditionalValueNode({ - condition: argumentValueNode('flag'), - ifTrue: publicKeyValueNode(ifTrueAddress), - }); - const visitor = makeVisitor({ argumentsInput: { flag: true } }); - const result = await visitor.visitConditionalValue(node); - expect(result).toBe(ifTrueAddress); - }); - - test('should resolve ifFalse branch when argument condition is falsy', async () => { - const node = conditionalValueNode({ - condition: argumentValueNode('flag'), - ifFalse: publicKeyValueNode(ifFalseAddress), - ifTrue: publicKeyValueNode(ifTrueAddress), - }); - const visitor = makeVisitor({ argumentsInput: { flag: false } }); - const result = await visitor.visitConditionalValue(node); - expect(result).toBe(ifFalseAddress); - }); - - test('should return null for optional account when no ifFalse and condition is falsy', async () => { - const optionalAccountNode = instructionAccountNode({ - isOptional: true, - isSigner: false, - isWritable: false, - name: 'optionalAccount', - }); - const node = conditionalValueNode({ - condition: argumentValueNode('flag'), - ifTrue: publicKeyValueNode(ifTrueAddress), - }); - const visitor = makeVisitor({ - argumentsInput: { flag: false }, - ixAccountNode: optionalAccountNode, - }); - const result = await visitor.visitConditionalValue(node); - expect(result).toBeNull(); - }); - - test('should throw for required account when no ifFalse and condition is falsy', async () => { - const node = conditionalValueNode({ - condition: argumentValueNode('flag'), - ifTrue: publicKeyValueNode(ifTrueAddress), - }); - const visitor = makeVisitor({ - argumentsInput: { flag: false }, - ixAccountNode: { - ...ixAccountNodeStub, - isOptional: false, - }, - }); - await expect(visitor.visitConditionalValue(node)).rejects.toThrow(/Missing account \[/); - }); - - test('should resolve ifTrue when primitive value matches', async () => { - const node = conditionalValueNode({ - condition: argumentValueNode('tokenStandard'), - ifFalse: publicKeyValueNode(ifFalseAddress), - ifTrue: publicKeyValueNode(ifTrueAddress), - value: numberValueNode(0), - }); - const visitor = makeVisitor({ argumentsInput: { tokenStandard: 0 } }); - const result = await visitor.visitConditionalValue(node); - expect(result).toBe(ifTrueAddress); - }); - - test('should resolve ifFalse when value does not match', async () => { - const node = conditionalValueNode({ - condition: argumentValueNode('tokenStandard'), - ifFalse: publicKeyValueNode(ifFalseAddress), - ifTrue: publicKeyValueNode(ifTrueAddress), - value: numberValueNode(0), - }); - const visitor = makeVisitor({ argumentsInput: { tokenStandard: 1 } }); - const result = await visitor.visitConditionalValue(node); - expect(result).toBe(ifFalseAddress); - }); - - test('should resolve with resolver condition', async () => { - const nodeWithTrue = conditionalValueNode({ - condition: resolverValueNode('myResolver'), - ifFalse: publicKeyValueNode(ifFalseAddress), - ifTrue: publicKeyValueNode(ifTrueAddress), - }); - const visitorWithTrue = makeVisitor({ resolversInput: { myResolver: () => Promise.resolve(true) } }); - const result = await visitorWithTrue.visitConditionalValue(nodeWithTrue); - expect(result).toBe(ifTrueAddress); - - const visitorWithFalse = makeVisitor({ resolversInput: { myResolver: () => Promise.resolve(false) } }); - const resultFalse = await visitorWithFalse.visitConditionalValue(nodeWithTrue); - expect(resultFalse).toBe(ifFalseAddress); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/account-default-value/identityValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/account-default-value/identityValueNode.test.ts deleted file mode 100644 index 113357178..000000000 --- a/packages/dynamic-address-resolution/test/visitors/account-default-value/identityValueNode.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { identityValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeVisitor } from './account-default-value-test-utils'; - -describe('account-default-value: visitIdentityValue', () => { - test('should return provided address', async () => { - const addr = await generateAddress(); - const visitor = makeVisitor({ accountsInput: { testAccount: addr } }); - const result = await visitor.visitIdentityValue(identityValueNode()); - expect(result).toBe(addr); - }); - - test('should throw when address not provided (undefined)', async () => { - const visitor = makeVisitor({ accountsInput: { testAccount: undefined } }); - await expect(visitor.visitIdentityValue(identityValueNode())).rejects.toThrow( - /Missing account \[testAccount\]/, - ); - }); - - test('should throw when address not provided (null)', async () => { - const visitor = makeVisitor({ accountsInput: { testAccount: null } }); - await expect(visitor.visitIdentityValue(identityValueNode())).rejects.toThrow( - /Missing account \[testAccount\]/, - ); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/account-default-value/payerValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/account-default-value/payerValueNode.test.ts deleted file mode 100644 index 5ada582a3..000000000 --- a/packages/dynamic-address-resolution/test/visitors/account-default-value/payerValueNode.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { payerValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeVisitor } from './account-default-value-test-utils'; - -describe('account-default-value: visitPayerValue', () => { - test('should return provided address', async () => { - const addr = await generateAddress(); - const visitor = makeVisitor({ accountsInput: { testAccount: addr } }); - const result = await visitor.visitPayerValue(payerValueNode()); - expect(result).toBe(addr); - }); - - test('should throw when address not provided (undefined)', async () => { - const visitor = makeVisitor({ accountsInput: { testAccount: undefined } }); - await expect(visitor.visitPayerValue(payerValueNode())).rejects.toThrow(/Missing account \[testAccount\]/); - }); - - test('should throw when address not provided (null)', async () => { - const visitor = makeVisitor({ accountsInput: { testAccount: null } }); - await expect(visitor.visitPayerValue(payerValueNode())).rejects.toThrow(/Missing account \[testAccount\]/); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/account-default-value/pdaValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/account-default-value/pdaValueNode.test.ts deleted file mode 100644 index 51e169571..000000000 --- a/packages/dynamic-address-resolution/test/visitors/account-default-value/pdaValueNode.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { getAddressEncoder, getProgramDerivedAddress } from '@solana/addresses'; -import { getUtf8Codec } from '@solana/codecs'; -import { - argumentValueNode, - bytesTypeNode, - constantPdaSeedNode, - instructionArgumentNode, - pdaLinkNode, - pdaNode, - pdaSeedValueNode, - pdaValueNode, - programNode, - publicKeyTypeNode, - rootNode, - stringTypeNode, - stringValueNode, - variablePdaSeedNode, -} from 'codama'; -import { beforeAll, describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { ixNodeStub, makeVisitor } from './account-default-value-test-utils'; - -describe('account-default-value: visitPdaValue', () => { - let testProgramAddress: Awaited>; - - beforeAll(async () => { - testProgramAddress = await generateAddress(); - }); - - test('should derive PDA with constant seed', async () => { - const pda = pdaNode({ - name: 'testPda', - seeds: [constantPdaSeedNode(stringTypeNode('utf8'), stringValueNode('prefix'))], - }); - const root = rootNode(programNode({ name: 'test', pdas: [pda], publicKey: testProgramAddress })); - const node = pdaValueNode(pdaLinkNode('testPda')); - - const expectedPda = await getProgramDerivedAddress({ - programAddress: testProgramAddress, - seeds: [getUtf8Codec().encode('prefix')], - }); - - const visitor = makeVisitor({ root }); - const result = await visitor.visitPdaValue(node); - expect(result).toBe(expectedPda[0]); - }); - - test('should derive PDA with variable seed from argument', async () => { - const ownerAddress = await generateAddress(); - const pda = pdaNode({ - name: 'testPda', - seeds: [variablePdaSeedNode('owner', publicKeyTypeNode())], - }); - const root = rootNode(programNode({ name: 'test', pdas: [pda], publicKey: testProgramAddress })); - const node = pdaValueNode(pdaLinkNode('testPda'), [pdaSeedValueNode('owner', argumentValueNode('ownerArg'))]); - const ixNode = { - ...ixNodeStub, - arguments: [instructionArgumentNode({ name: 'ownerArg', type: publicKeyTypeNode() })], - }; - - const expectedPda = await getProgramDerivedAddress({ - programAddress: testProgramAddress, - seeds: [getAddressEncoder().encode(ownerAddress)], - }); - - const visitor = makeVisitor({ - argumentsInput: { ownerArg: ownerAddress }, - ixNode, - root, - }); - const result = await visitor.visitPdaValue(node); - expect(result).toBe(expectedPda[0]); - }); - - test('should throw when variable seed value node is missing', async () => { - const pda = pdaNode({ - name: 'testPda', - seeds: [variablePdaSeedNode('owner', bytesTypeNode())], - }); - const root = rootNode(programNode({ name: 'test', pdas: [pda], publicKey: testProgramAddress })); - const node = pdaValueNode(pdaLinkNode('testPda')); - - const visitor = makeVisitor({ root }); - await expect(visitor.visitPdaValue(node)).rejects.toThrow( - /Referenced node \[owner\] not found in \[testInstruction\]/, - ); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/account-default-value/programIdValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/account-default-value/programIdValueNode.test.ts deleted file mode 100644 index 399511b22..000000000 --- a/packages/dynamic-address-resolution/test/visitors/account-default-value/programIdValueNode.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { programIdValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor, programAddress } from './account-default-value-test-utils'; - -describe('account-default-value: visitProgramIdValue', () => { - test('should return program public key from root', async () => { - const visitor = makeVisitor(); - const result = await visitor.visitProgramIdValue(programIdValueNode()); - expect(result).toBe(programAddress); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/account-default-value/publicKeyValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/account-default-value/publicKeyValueNode.test.ts deleted file mode 100644 index e049fdb5b..000000000 --- a/packages/dynamic-address-resolution/test/visitors/account-default-value/publicKeyValueNode.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { publicKeyValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeVisitor } from './account-default-value-test-utils'; - -describe('account-default-value: visitPublicKeyValue', () => { - test('should return public key address', async () => { - const addr = await generateAddress(); - const visitor = makeVisitor(); - const result = await visitor.visitPublicKeyValue(publicKeyValueNode(addr)); - expect(result).toBe(addr); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/account-default-value/resolverValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/account-default-value/resolverValueNode.test.ts deleted file mode 100644 index 9e3c87c50..000000000 --- a/packages/dynamic-address-resolution/test/visitors/account-default-value/resolverValueNode.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { resolverValueNode } from 'codama'; -import { describe, expect, test, vi } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeVisitor } from './account-default-value-test-utils'; - -describe('account-default-value: visitResolverValue', () => { - test('should call resolver and return address', async () => { - const addr = await generateAddress(); - const visitor = makeVisitor({ - resolversInput: { myResolver: () => Promise.resolve(addr) }, - }); - const result = await visitor.visitResolverValue(resolverValueNode('myResolver')); - expect(result).toBe(addr); - }); - - test('should throw when resolver is not provided', async () => { - const visitor = makeVisitor(); - await expect(visitor.visitResolverValue(resolverValueNode('myResolver'))).rejects.toThrow( - /Resolver \[myResolver\] not provided for account \[testAccount\]/, - ); - }); - - test('should throw when resolver returns null', async () => { - const visitor = makeVisitor({ - resolversInput: { myResolver: () => Promise.resolve(null) }, - }); - await expect(visitor.visitResolverValue(resolverValueNode('myResolver'))).rejects.toThrow( - /Invalid account address \[testAccount\]: \[null\]/, - ); - }); - - test('should throw when resolver returns undefined', async () => { - const visitor = makeVisitor({ - resolversInput: { myResolver: () => Promise.resolve(undefined) }, - }); - await expect(visitor.visitResolverValue(resolverValueNode('myResolver'))).rejects.toThrow( - /Invalid account address \[testAccount\]: \[undefined\]/, - ); - }); - - test('should pass arguments and accounts to resolver', async () => { - const addr = await generateAddress(); - const resolverSpy = vi.fn().mockResolvedValue(addr); - const argumentsInput = { amount: 100 }; - const accountsInput = { treasury: addr }; - - const visitor = makeVisitor({ - accountsInput, - argumentsInput, - resolversInput: { myResolver: resolverSpy }, - }); - await visitor.visitResolverValue(resolverValueNode('myResolver')); - - expect(resolverSpy).toHaveBeenCalledWith(argumentsInput, accountsInput); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/amountTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/amountTypeNode.test.ts deleted file mode 100644 index 7e35115ec..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/amountTypeNode.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { amountTypeNode, numberTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('amountTypeNode', () => { - test('should delegate to inner number type (pass-through)', () => { - const transformer = createCodecInputTransformer(amountTypeNode(numberTypeNode('u64'), 2, 'USD'), rootNodeMock); - expect(transformer(100)).toBe(100); - expect(transformer(0)).toBe(0); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/arrayTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/arrayTypeNode.test.ts deleted file mode 100644 index 4aad54cc4..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/arrayTypeNode.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { arrayTypeNode, bytesTypeNode, numberTypeNode, prefixedCountNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('arrayTypeNode', () => { - test('should transform array items with bytes inner type', () => { - const transformer = createCodecInputTransformer( - arrayTypeNode(bytesTypeNode(), prefixedCountNode(numberTypeNode('u32'))), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - const input = [new Uint8Array([0x01]), new Uint8Array([0x02])]; - expect(transformer(input)).toEqual([ - ['base16', '01'], - ['base16', '02'], - ]); - }); - - test('should pass through array of primitives', () => { - const transformer = createCodecInputTransformer( - arrayTypeNode(numberTypeNode('u8'), prefixedCountNode(numberTypeNode('u32'))), - rootNodeMock, - ); - expect(transformer([1, 2, 3])).toEqual([1, 2, 3]); - }); - - test('should throw for non-array input', () => { - const transformer = createCodecInputTransformer( - arrayTypeNode(numberTypeNode('u8'), prefixedCountNode(numberTypeNode('u32'))), - rootNodeMock, - ); - expect(() => transformer('not an array')).toThrow(/Expected \[array\] for \[arrayTypeNode\]/); - expect(() => transformer(42)).toThrow(/Expected \[array\] for \[arrayTypeNode\]/); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/booleanTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/booleanTypeNode.test.ts deleted file mode 100644 index ee761fca3..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/booleanTypeNode.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { booleanTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('booleanTypeNode', () => { - test('should pass through boolean values', () => { - const transformer = createCodecInputTransformer(booleanTypeNode(), rootNodeMock); - expect(transformer(true)).toBe(true); - expect(transformer(false)).toBe(false); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/bytesTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/bytesTypeNode.test.ts deleted file mode 100644 index 347ef7171..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/bytesTypeNode.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { bytesTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('bytesTypeNode', () => { - test('should transform Uint8Array or number[] to tuple for bytesTypeNode', () => { - const transformer = createCodecInputTransformer(bytesTypeNode(), rootNodeMock, { bytesEncoding: 'base16' }); - - // 'Hello' as bytes: [72, 101, 108, 108, 111] -> base16: '48656c6c6f' - const inputNumbers = [72, 101, 108, 108, 111]; - const inputUint8 = new Uint8Array(inputNumbers); - const resultWithUint8 = transformer(inputUint8); - const resultWithNumbers = transformer(inputNumbers); - const expectedResult = ['base16', '48656c6c6f']; - - expect(resultWithUint8).toEqual(expectedResult); - expect(resultWithNumbers).toEqual(expectedResult); - }); - - test('should throw error for non-Uint8Array input', () => { - const transformer = createCodecInputTransformer(bytesTypeNode(), rootNodeMock, { bytesEncoding: 'base16' }); - const expectedMessage = /Expected \[Uint8Array \| number\[\]\] for \[bytesTypeNode\]/; - expect(() => transformer(null)).toThrow(expectedMessage); - expect(() => transformer(undefined)).toThrow(expectedMessage); - expect(() => transformer('not a Uint8Array')).toThrow(expectedMessage); - expect(() => transformer(123)).toThrow(expectedMessage); - expect(() => transformer({ data: [1, 2, 3] })).toThrow(expectedMessage); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/codec-input-transformer-test-utils.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/codec-input-transformer-test-utils.ts deleted file mode 100644 index 554288693..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/codec-input-transformer-test-utils.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { programNode, rootNode } from 'codama'; - -// Shared root node mock for tests -export const rootNodeMock = rootNode( - programNode({ - name: 'test', - publicKey: '11111111111111111111111111111111', - }), -); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/dateTimeTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/dateTimeTypeNode.test.ts deleted file mode 100644 index 15f224990..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/dateTimeTypeNode.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { dateTimeTypeNode, numberTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('dateTimeTypeNode', () => { - test('should delegate to inner number type (pass-through)', () => { - const transformer = createCodecInputTransformer(dateTimeTypeNode(numberTypeNode('i64')), rootNodeMock); - expect(transformer(1700000000)).toBe(1700000000); - expect(transformer(0)).toBe(0); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/definedTypeLinkNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/definedTypeLinkNode.test.ts deleted file mode 100644 index 4d2182022..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/definedTypeLinkNode.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { definedTypeLinkNode, definedTypeNode, numberTypeNode, programNode, rootNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; - -describe('definedTypeLinkNode', () => { - test('should resolve defined type and delegate', () => { - const root = rootNode( - programNode({ - definedTypes: [definedTypeNode({ name: 'myNumber', type: numberTypeNode('u64') })], - name: 'test', - publicKey: '11111111111111111111111111111111', - }), - ); - const transformer = createCodecInputTransformer(definedTypeLinkNode('myNumber'), root); - expect(transformer(42)).toBe(42); - }); - - test('should throw for unknown type name', () => { - const root = rootNode( - programNode({ - name: 'test', - publicKey: '11111111111111111111111111111111', - }), - ); - expect(() => createCodecInputTransformer(definedTypeLinkNode('nonExistent'), root)).toThrow( - /Could not find linked node \[nonExistent\] from \[definedTypeLinkNode\]/, - ); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/enumTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/enumTypeNode.test.ts deleted file mode 100644 index 6faf29c20..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/enumTypeNode.test.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { - arrayTypeNode, - bytesTypeNode, - definedTypeLinkNode, - definedTypeNode, - enumEmptyVariantTypeNode, - enumStructVariantTypeNode, - enumTupleVariantTypeNode, - enumTypeNode, - numberTypeNode, - optionTypeNode, - prefixedCountNode, - programNode, - rootNode, - structFieldTypeNode, - structTypeNode, - tupleTypeNode, -} from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('enumTypeNode', () => { - // Based on pmp-idl.json, mpl-token-metadata-idl.json, token-2022-idl.json - - test('should resolve scalar enum (number)', () => { - // Based on pmp-idl.json: accountDiscriminator enum - const scalarEnum = enumTypeNode([ - enumEmptyVariantTypeNode('empty'), - enumEmptyVariantTypeNode('buffer'), - enumEmptyVariantTypeNode('metadata'), - ]); - const transformer = createCodecInputTransformer(scalarEnum, rootNodeMock); - - expect(transformer(0)).toEqual({ __kind: 'Empty' }); - expect(transformer(1)).toEqual({ __kind: 'Buffer' }); - expect(transformer(2)).toEqual({ __kind: 'Metadata' }); - }); - - test('should resolve a PascalCase __kind object for a camelCase variant', () => { - const scalarEnum = enumTypeNode([enumEmptyVariantTypeNode('arm'), enumEmptyVariantTypeNode('bar')]); - const transformer = createCodecInputTransformer(scalarEnum, rootNodeMock); - - expect(transformer({ __kind: 'Arm' })).toStrictEqual({ __kind: 'Arm' }); - expect(transformer({ __kind: 'arm' })).toStrictEqual({ __kind: 'Arm' }); - }); - - test('should resolve scalar enum (string)', () => { - const scalarEnum = enumTypeNode([enumEmptyVariantTypeNode('initialized'), enumEmptyVariantTypeNode('frozen')]); - const transformer = createCodecInputTransformer(scalarEnum, rootNodeMock); - - expect(transformer('initialized')).toEqual({ __kind: 'Initialized' }); - expect(transformer('frozen')).toEqual({ __kind: 'Frozen' }); - }); - - test('should pass through empty variant enum', () => { - const dataEnum = enumTypeNode([ - enumEmptyVariantTypeNode('none'), - enumEmptyVariantTypeNode('utf8'), - enumEmptyVariantTypeNode('base58'), - ]); - const transformer = createCodecInputTransformer(dataEnum, rootNodeMock); - - const input = { __kind: 'none' }; - expect(transformer(input)).toEqual({ __kind: 'None' }); - }); - - test('should transform bytes in struct variant', () => { - // Based on mpl-token-metadata-idl.json: complex enum with struct variants - const enumWithStructVariant = enumTypeNode([ - enumEmptyVariantTypeNode('empty'), - enumStructVariantTypeNode( - 'withData', - structTypeNode([ - structFieldTypeNode({ name: 'id', type: numberTypeNode('u32') }), - structFieldTypeNode({ name: 'data', type: bytesTypeNode() }), - ]), - ), - ]); - - const transformer = createCodecInputTransformer(enumWithStructVariant, rootNodeMock, { - bytesEncoding: 'base16', - }); - - const input = { - __kind: 'withData', - data: new Uint8Array([1, 2, 3]), - id: 42, - }; - - expect(transformer(input)).toEqual({ - __kind: 'WithData', - data: ['base16', '010203'], - id: 42, - }); - }); - - test('should transform bytes in tuple variant', () => { - const enumWithTupleVariant = enumTypeNode([ - enumEmptyVariantTypeNode('empty'), - enumTupleVariantTypeNode('withBytes', tupleTypeNode([bytesTypeNode(), numberTypeNode('u8')])), - ]); - - const transformer = createCodecInputTransformer(enumWithTupleVariant, rootNodeMock, { - bytesEncoding: 'base16', - }); - - const input = { - __kind: 'withBytes', - fields: [new Uint8Array([0xde, 0xad]), 255], - }; - - expect(transformer(input)).toEqual({ - __kind: 'WithBytes', - fields: [['base16', 'dead'], 255], - }); - }); - - test('should throw on unknown variant', () => { - const enumWithVariants = enumTypeNode([enumEmptyVariantTypeNode('known1'), enumEmptyVariantTypeNode('known2')]); - - const transformer = createCodecInputTransformer(enumWithVariants, rootNodeMock); - - const input = { __kind: 'unknownVariant', someData: 123 }; - expect(() => transformer(input)).toThrow(/Expected \[one of \[known1, known2\]\] for \[enumTypeNode\]/); - }); - - test('should pass through non-object input for enumTypeNode', () => { - const enumNode = enumTypeNode([enumEmptyVariantTypeNode('variant1'), enumEmptyVariantTypeNode('variant2')]); - - const transformer = createCodecInputTransformer(enumNode, rootNodeMock); - - expect(transformer(null)).toBe(null); - expect(transformer(undefined)).toBe(undefined); - expect(transformer('string')).toBe('string'); - }); - - test('should handle enum without __kind discriminator', () => { - const enumNode = enumTypeNode([enumEmptyVariantTypeNode('variant1')]); - const transformer = createCodecInputTransformer(enumNode, rootNodeMock); - - // Object without __kind should pass through - const input = { someField: 'value' }; - expect(transformer(input)).toEqual(input); - }); - - test('should handle nested enum with multiple bytes fields in struct variant', () => { - // Complex real-world scenario - const complexEnum = enumTypeNode([ - enumEmptyVariantTypeNode('none'), - enumStructVariantTypeNode( - 'complex', - structTypeNode([ - structFieldTypeNode({ name: 'id', type: numberTypeNode('u32') }), - structFieldTypeNode({ name: 'key', type: bytesTypeNode() }), - structFieldTypeNode({ name: 'value', type: bytesTypeNode() }), - structFieldTypeNode({ - name: 'nested', - type: optionTypeNode(bytesTypeNode()), - }), - ]), - ), - ]); - - const transformer = createCodecInputTransformer(complexEnum, rootNodeMock, { bytesEncoding: 'base16' }); - - const input = { - __kind: 'complex', - id: 100, - key: new Uint8Array([1, 2]), - nested: new Uint8Array([5, 6]), - value: new Uint8Array([3, 4]), - }; - - expect(transformer(input)).toEqual({ - __kind: 'Complex', - id: 100, - key: ['base16', '0102'], - nested: ['base16', '0506'], - value: ['base16', '0304'], - }); - }); - - test('should handle enum with array of bytes in struct variant', () => { - const enumWithArray = enumTypeNode([ - enumStructVariantTypeNode( - 'withArray', - structTypeNode([ - structFieldTypeNode({ - name: 'items', - type: arrayTypeNode(bytesTypeNode(), prefixedCountNode(numberTypeNode('u32'))), - }), - ]), - ), - ]); - - const transformer = createCodecInputTransformer(enumWithArray, rootNodeMock, { bytesEncoding: 'base16' }); - - const input = { - __kind: 'withArray', - items: [new Uint8Array([1, 2]), new Uint8Array([3, 4])], - }; - - expect(transformer(input)).toEqual({ - __kind: 'WithArray', - items: [ - ['base16', '0102'], - ['base16', '0304'], - ], - }); - }); - - test('should throw on tuple variant without fields', () => { - const enumWithTuple = enumTypeNode([ - enumTupleVariantTypeNode('tuple', tupleTypeNode([numberTypeNode('u32'), bytesTypeNode()])), - ]); - - const transformer = createCodecInputTransformer(enumWithTuple, rootNodeMock, { bytesEncoding: 'base16' }); - - const input = { __kind: 'tuple', someOtherProp: 123 }; - expect(() => transformer(input)).toThrow(/Expected \[array \(fields\)\] for \[enumTupleVariantTypeNode\]/); - }); - - test('should handle deeply nested enum variants', () => { - // Enum containing another enum in struct variant - const innerEnum = enumTypeNode([ - enumEmptyVariantTypeNode('inner1'), - enumStructVariantTypeNode( - 'inner2', - structTypeNode([structFieldTypeNode({ name: 'data', type: bytesTypeNode() })]), - ), - ]); - - const root = rootNode( - programNode({ - definedTypes: [ - definedTypeNode({ - name: 'InnerEnum', - type: innerEnum, - }), - ], - name: 'test', - publicKey: '11111111111111111111111111111111', - }), - ); - - const outerEnum = enumTypeNode([ - enumStructVariantTypeNode( - 'outer', - structTypeNode([structFieldTypeNode({ name: 'inner', type: definedTypeLinkNode('InnerEnum') })]), - ), - ]); - - const transformer = createCodecInputTransformer(outerEnum, root, { bytesEncoding: 'base16' }); - - const input = { - __kind: 'outer', - inner: { - __kind: 'inner2', - data: new Uint8Array([0xff, 0xee]), - }, - }; - - expect(transformer(input)).toEqual({ - __kind: 'Outer', - inner: { - __kind: 'Inner2', - data: ['base16', 'ffee'], - }, - }); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/fixedSizeTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/fixedSizeTypeNode.test.ts deleted file mode 100644 index f7522de87..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/fixedSizeTypeNode.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { bytesTypeNode, fixedSizeTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('fixedSizeTypeNode', () => { - test('should delegate to inner type (bytes transform visible)', () => { - const transformer = createCodecInputTransformer(fixedSizeTypeNode(bytesTypeNode(), 4), rootNodeMock, { - bytesEncoding: 'base16', - }); - const input = new Uint8Array([0xde, 0xad, 0xbe, 0xef]); - expect(transformer(input)).toEqual(['base16', 'deadbeef']); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/hiddenPrefixTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/hiddenPrefixTypeNode.test.ts deleted file mode 100644 index 71ba7a9fc..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/hiddenPrefixTypeNode.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - bytesTypeNode, - bytesValueNode, - constantValueNode, - fixedSizeTypeNode, - hiddenPrefixTypeNode, - numberTypeNode, - stringTypeNode, - stringValueNode, -} from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('hiddenPrefixTypeNode', () => { - test('should delegate to inner type [bytesTypeNode]', () => { - const transformer = createCodecInputTransformer( - hiddenPrefixTypeNode(bytesTypeNode(), [constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ff'))]), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - const input = new Uint8Array([0x01, 0x02]); - expect(transformer(input)).toEqual(['base16', '0102']); - }); - - test('should delegate to inner type [numberTypeNode]', () => { - const transformer = createCodecInputTransformer( - hiddenPrefixTypeNode(numberTypeNode('u32'), [ - constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff')), - ]), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - expect(transformer(42)).toEqual(42); - }); - - test('should delegate to inner type [stringTypeNode]', () => { - const transformer = createCodecInputTransformer( - hiddenPrefixTypeNode(fixedSizeTypeNode(stringTypeNode('utf8'), 10), [ - constantValueNode(stringTypeNode('utf8'), stringValueNode('Hello')), - ]), - rootNodeMock, - ); - expect(transformer('World')).toEqual('World'); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/hiddenSuffixTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/hiddenSuffixTypeNode.test.ts deleted file mode 100644 index 8602c7c58..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/hiddenSuffixTypeNode.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { bytesTypeNode, bytesValueNode, constantValueNode, hiddenSuffixTypeNode, numberTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('hiddenSuffixTypeNode', () => { - test('should delegate to inner type', () => { - const transformer = createCodecInputTransformer( - hiddenSuffixTypeNode(bytesTypeNode(), [constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ff'))]), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - const input = new Uint8Array([0x01, 0x02]); - expect(transformer(input)).toEqual(['base16', '0102']); - }); - - test('should delegate to inner type [numberTypeNode]', () => { - const transformer = createCodecInputTransformer( - hiddenSuffixTypeNode(numberTypeNode('u32'), [ - constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ffff')), - ]), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - expect(transformer(42)).toEqual(42); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/mapTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/mapTypeNode.test.ts deleted file mode 100644 index 461df7264..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/mapTypeNode.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { bytesTypeNode, mapTypeNode, numberTypeNode, prefixedCountNode, stringTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('mapTypeNode', () => { - test('should transform map values with bytes inner type', () => { - const transformer = createCodecInputTransformer( - mapTypeNode(stringTypeNode('utf8'), bytesTypeNode(), prefixedCountNode(numberTypeNode('u32'))), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - const input = { key1: new Uint8Array([0x01]), key2: new Uint8Array([0x02]) }; - expect(transformer(input)).toEqual({ key1: ['base16', '01'], key2: ['base16', '02'] }); - }); - - test('should pass through map with primitive values', () => { - const transformer = createCodecInputTransformer( - mapTypeNode(stringTypeNode('utf8'), numberTypeNode('u64'), prefixedCountNode(numberTypeNode('u32'))), - rootNodeMock, - ); - expect(transformer({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 }); - }); - - test('should throw for non-object input', () => { - const transformer = createCodecInputTransformer( - mapTypeNode(stringTypeNode('utf8'), numberTypeNode('u8'), prefixedCountNode(numberTypeNode('u32'))), - rootNodeMock, - ); - expect(() => transformer('not an object')).toThrow(/Expected \[object\] for \[mapTypeNode\]/); - expect(() => transformer([1, 2])).toThrow(/Expected \[object\] for \[mapTypeNode\]/); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/numberTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/numberTypeNode.test.ts deleted file mode 100644 index bcd8cd378..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/numberTypeNode.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { numberTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('numberTypeNode', () => { - test('should pass through number values', () => { - const transformer = createCodecInputTransformer(numberTypeNode('u64'), rootNodeMock); - expect(transformer(0)).toBe(0); - expect(transformer(42)).toBe(42); - expect(transformer(999)).toBe(999); - expect(transformer(Number.MAX_SAFE_INTEGER)).toBe(Number.MAX_SAFE_INTEGER); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/optionTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/optionTypeNode.test.ts deleted file mode 100644 index 8e32a9d2b..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/optionTypeNode.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { bytesTypeNode, numberTypeNode, optionTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('optionTypeNode', () => { - test('should pass through null and undefined', () => { - const transformer = createCodecInputTransformer( - optionTypeNode(numberTypeNode('u8'), { prefix: numberTypeNode('u8') }), - rootNodeMock, - ); - expect(transformer(null)).toBe(null); - expect(transformer(undefined)).toBe(undefined); - }); - - test('should transform non-null inner value', () => { - const transformer = createCodecInputTransformer( - optionTypeNode(bytesTypeNode(), { prefix: numberTypeNode('u8') }), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - expect(transformer(new Uint8Array([0xff]))).toEqual(['base16', 'ff']); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/postOffsetTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/postOffsetTypeNode.test.ts deleted file mode 100644 index 4bf606146..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/postOffsetTypeNode.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { bytesTypeNode, numberTypeNode, postOffsetTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('postOffsetTypeNode', () => { - test('should delegate to inner type [bytesTypeNode]', () => { - const transformer = createCodecInputTransformer(postOffsetTypeNode(bytesTypeNode(), 0), rootNodeMock, { - bytesEncoding: 'base16', - }); - const input = new Uint8Array([0xab, 0xcd]); - expect(transformer(input)).toEqual(['base16', 'abcd']); - }); - - test('should delegate to inner type [numberTypeNode]', () => { - const transformer = createCodecInputTransformer(postOffsetTypeNode(numberTypeNode('u32'), 0), rootNodeMock); - expect(transformer(42)).toEqual(42); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/preOffsetTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/preOffsetTypeNode.test.ts deleted file mode 100644 index 5b4114421..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/preOffsetTypeNode.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { bytesTypeNode, preOffsetTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('preOffsetTypeNode', () => { - test('should delegate to inner type', () => { - const transformer = createCodecInputTransformer(preOffsetTypeNode(bytesTypeNode(), 0), rootNodeMock, { - bytesEncoding: 'base16', - }); - const input = new Uint8Array([0xab, 0xcd]); - expect(transformer(input)).toEqual(['base16', 'abcd']); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/publicKeyTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/publicKeyTypeNode.test.ts deleted file mode 100644 index 3597fa6c2..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/publicKeyTypeNode.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { publicKeyTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { generateAddress } from '../../test-utils'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('publicKeyTypeNode', () => { - test('should pass through public key string', async () => { - const transformer = createCodecInputTransformer(publicKeyTypeNode(), rootNodeMock); - const pubkey = await generateAddress(); - expect(transformer(pubkey)).toBe(pubkey); - }); - - test('should pass through any value unchanged', () => { - const transformer = createCodecInputTransformer(publicKeyTypeNode(), rootNodeMock); - expect(transformer(null)).toBe(null); - expect(transformer(123)).toBe(123); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/remainderOptionTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/remainderOptionTypeNode.test.ts deleted file mode 100644 index 4d379331c..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/remainderOptionTypeNode.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { bytesTypeNode, remainderOptionTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('remainderOptionTypeNode', () => { - test('should pass through null and undefined', () => { - const transformer = createCodecInputTransformer(remainderOptionTypeNode(bytesTypeNode()), rootNodeMock); - expect(transformer(null)).toBe(null); - expect(transformer(undefined)).toBe(undefined); - }); - - test('should transform non-null inner value', () => { - const transformer = createCodecInputTransformer(remainderOptionTypeNode(bytesTypeNode()), rootNodeMock, { - bytesEncoding: 'base16', - }); - expect(transformer(new Uint8Array([0xab]))).toEqual(['base16', 'ab']); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/sentinelTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/sentinelTypeNode.test.ts deleted file mode 100644 index 8c9e6a1ac..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/sentinelTypeNode.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { bytesTypeNode, bytesValueNode, constantValueNode, sentinelTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('sentinelTypeNode', () => { - test('should delegate to inner type', () => { - const transformer = createCodecInputTransformer( - sentinelTypeNode(bytesTypeNode(), constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'ff'))), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - const input = new Uint8Array([0xab, 0xcd]); - expect(transformer(input)).toEqual(['base16', 'abcd']); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/setTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/setTypeNode.test.ts deleted file mode 100644 index 5f780fc38..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/setTypeNode.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { bytesTypeNode, numberTypeNode, prefixedCountNode, setTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('setTypeNode', () => { - test('should transform set items with bytes inner type', () => { - const transformer = createCodecInputTransformer( - setTypeNode(bytesTypeNode(), prefixedCountNode(numberTypeNode('u32'))), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - const input = [new Uint8Array([0xaa]), new Uint8Array([0xbb])]; - expect(transformer(input)).toEqual([ - ['base16', 'aa'], - ['base16', 'bb'], - ]); - }); - - test('should throw for non-array input', () => { - const transformer = createCodecInputTransformer( - setTypeNode(numberTypeNode('u8'), prefixedCountNode(numberTypeNode('u32'))), - rootNodeMock, - ); - expect(() => transformer('not an array')).toThrow(/Expected \[array\] for \[setTypeNode\]/); - expect(() => transformer({ a: 1 })).toThrow(/Expected \[array\] for \[setTypeNode\]/); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/sizePrefixTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/sizePrefixTypeNode.test.ts deleted file mode 100644 index 9f3c689ad..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/sizePrefixTypeNode.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { bytesTypeNode, numberTypeNode, sizePrefixTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('sizePrefixTypeNode', () => { - test('should delegate to inner type', () => { - const transformer = createCodecInputTransformer( - sizePrefixTypeNode(bytesTypeNode(), numberTypeNode('u32')), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - const input = new Uint8Array([0x01, 0x02]); - expect(transformer(input)).toEqual(['base16', '0102']); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/solAmountTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/solAmountTypeNode.test.ts deleted file mode 100644 index 4906948c8..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/solAmountTypeNode.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { numberTypeNode, solAmountTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('solAmountTypeNode', () => { - test('should delegate to inner number type (pass-through)', () => { - const transformer = createCodecInputTransformer(solAmountTypeNode(numberTypeNode('u64')), rootNodeMock); - expect(transformer(1000000000)).toBe(1000000000); - expect(transformer(0)).toBe(0); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/stringTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/stringTypeNode.test.ts deleted file mode 100644 index e36df3949..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/stringTypeNode.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { stringTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('stringTypeNode', () => { - test('should pass through string values', () => { - const transformer = createCodecInputTransformer(stringTypeNode('utf8'), rootNodeMock); - expect(transformer('hello')).toBe('hello'); - expect(transformer('')).toBe(''); - expect(transformer('🎉 unicode')).toBe('🎉 unicode'); - }); - - test('should pass through non-string values unchanged', () => { - const transformer = createCodecInputTransformer(stringTypeNode('utf8'), rootNodeMock); - expect(transformer(42)).toBe(42); - expect(transformer(null)).toBe(null); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/structTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/structTypeNode.test.ts deleted file mode 100644 index 1cb058dba..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/structTypeNode.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { - arrayTypeNode, - bytesTypeNode, - fixedCountNode, - numberTypeNode, - optionTypeNode, - stringTypeNode, - structFieldTypeNode, - structTypeNode, -} from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('structTypeNode', () => { - test('should transform struct with bytes field', () => { - const transformer = createCodecInputTransformer( - structTypeNode([ - structFieldTypeNode({ name: 'name', type: stringTypeNode('utf8') }), - structFieldTypeNode({ name: 'data', type: bytesTypeNode() }), - ]), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - - const input = { - data: new Uint8Array([1, 2, 3]), - name: 'test', - }; - - expect(transformer(input)).toEqual({ - data: ['base16', '010203'], - name: 'test', - }); - }); - - test('should transform complex nested structure with bytes in array', () => { - const transformer = createCodecInputTransformer( - structTypeNode([ - structFieldTypeNode({ name: 'id', type: numberTypeNode('u32') }), - structFieldTypeNode({ - name: 'items', - type: arrayTypeNode( - structTypeNode([ - structFieldTypeNode({ name: 'name', type: stringTypeNode('utf8') }), - structFieldTypeNode({ name: 'data', type: bytesTypeNode() }), - ]), - fixedCountNode(2), - ), - }), - ]), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - - const input = { - id: 123, - items: [ - { data: new Uint8Array([1, 2, 3]), name: 'item1' }, - { data: new Uint8Array([4, 5]), name: 'item2' }, - ], - }; - - expect(transformer(input)).toEqual({ - id: 123, - items: [ - { data: ['base16', '010203'], name: 'item1' }, - { data: ['base16', '0405'], name: 'item2' }, - ], - }); - }); - - test('should throw error for non-object input', () => { - const transformer = createCodecInputTransformer( - structTypeNode([structFieldTypeNode({ name: 'data', type: bytesTypeNode() })]), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - - expect(() => transformer(null)).toThrow(/Expected \[object\] for \[structTypeNode\]/); - expect(() => transformer(undefined)).toThrow(/Expected \[object\] for \[structTypeNode\]/); - expect(() => transformer('not an object')).toThrow(/Expected \[object\] for \[structTypeNode\]/); - expect(() => transformer(123)).toThrow(/Expected \[object\] for \[structTypeNode\]/); - expect(() => transformer([1, 2, 3])).toThrow(/Expected \[object\] for \[structTypeNode\]/); - }); - - test('should throw error for Date, Map, and Set inputs', () => { - const transformer = createCodecInputTransformer( - structTypeNode([structFieldTypeNode({ name: 'data', type: bytesTypeNode() })]), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - - expect(() => transformer(new Date())).toThrow(/Expected \[object\] for \[structTypeNode\]/); - expect(() => transformer(new Map())).toThrow(/Expected \[object\] for \[structTypeNode\]/); - expect(() => transformer(new Set())).toThrow(/Expected \[object\] for \[structTypeNode\]/); - }); - - test('should transform multiple bytes fields in struct', () => { - const transformer = createCodecInputTransformer( - structTypeNode([ - structFieldTypeNode({ name: 'key', type: bytesTypeNode() }), - structFieldTypeNode({ name: 'value', type: bytesTypeNode() }), - structFieldTypeNode({ name: 'id', type: numberTypeNode('u32') }), - ]), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - - const input = { - id: 999, - key: new Uint8Array([1, 2]), - value: new Uint8Array([3, 4, 5]), - }; - - expect(transformer(input)).toEqual({ - id: 999, - key: ['base16', '0102'], - value: ['base16', '030405'], - }); - }); - - test('should handle struct with missing optional fields', () => { - const structNode = structTypeNode([ - structFieldTypeNode({ name: 'required', type: numberTypeNode('u32') }), - structFieldTypeNode({ name: 'optional', type: optionTypeNode(bytesTypeNode()) }), - ]); - const transformer = createCodecInputTransformer(structNode, rootNodeMock, { bytesEncoding: 'base16' }); - - const input = { required: 100 }; - expect(transformer(input)).toEqual({ required: 100 }); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/tupleTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/tupleTypeNode.test.ts deleted file mode 100644 index 30659137a..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/tupleTypeNode.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { bytesTypeNode, numberTypeNode, tupleTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('tupleTypeNode', () => { - test('should transform each item by position', () => { - const transformer = createCodecInputTransformer( - tupleTypeNode([numberTypeNode('u8'), bytesTypeNode()]), - rootNodeMock, - { bytesEncoding: 'base16' }, - ); - const input = [42, new Uint8Array([0xff])]; - expect(transformer(input)).toEqual([42, ['base16', 'ff']]); - }); - - test('should throw for non-array input', () => { - const transformer = createCodecInputTransformer(tupleTypeNode([numberTypeNode('u8')]), rootNodeMock); - expect(() => transformer('not an array')).toThrow(/Expected \[array\] for \[tupleTypeNode\]/); - }); - - test('should throw for wrong length', () => { - const transformer = createCodecInputTransformer( - tupleTypeNode([numberTypeNode('u8'), numberTypeNode('u16')]), - rootNodeMock, - ); - expect(() => transformer([1])).toThrow(/Expected \[array\(length:2\)\] for \[tupleTypeNode\]/); - expect(() => transformer([1, 2, 3])).toThrow(/Expected \[array\(length:2\)\] for \[tupleTypeNode\]/); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/zeroableOptionTypeNode.test.ts b/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/zeroableOptionTypeNode.test.ts deleted file mode 100644 index fe0405d8a..000000000 --- a/packages/dynamic-address-resolution/test/visitors/codec-input-transformer/zeroableOptionTypeNode.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { bytesTypeNode, zeroableOptionTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { createCodecInputTransformer } from '../../../src/visitors/codec-input-transformer'; -import { rootNodeMock } from './codec-input-transformer-test-utils'; - -describe('zeroableOptionTypeNode', () => { - test('should pass through null and undefined', () => { - const transformer = createCodecInputTransformer(zeroableOptionTypeNode(bytesTypeNode()), rootNodeMock); - expect(transformer(null)).toBe(null); - expect(transformer(undefined)).toBe(undefined); - }); - - test('should transform non-null inner value', () => { - const transformer = createCodecInputTransformer(zeroableOptionTypeNode(bytesTypeNode()), rootNodeMock, { - bytesEncoding: 'base16', - }); - expect(transformer(new Uint8Array([0xcd]))).toEqual(['base16', 'cd']); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/condition-node-value/accountValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/condition-node-value/accountValueNode.test.ts deleted file mode 100644 index fb8655671..000000000 --- a/packages/dynamic-address-resolution/test/visitors/condition-node-value/accountValueNode.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { accountValueNode, instructionAccountNode, instructionNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeVisitor } from './condition-node-value-test-utils'; - -describe('condition-node-value: visitAccountValue', () => { - const ixNodeWithAccount = instructionNode({ - accounts: [ - instructionAccountNode({ - isOptional: true, - isSigner: false, - isWritable: false, - name: 'myAccount', - }), - ], - name: 'testInstruction', - }); - - test('should return null when user provides null', async () => { - const visitor = makeVisitor({ - accountsInput: { myAccount: null }, - ixNode: ixNodeWithAccount, - }); - const result = await visitor.visitAccountValue(accountValueNode('myAccount')); - expect(result).toBeNull(); - }); - - test('should return address when user provides address', async () => { - const accAddress = await generateAddress(); - const visitor = makeVisitor({ - accountsInput: { myAccount: accAddress }, - ixNode: ixNodeWithAccount, - }); - const result = await visitor.visitAccountValue(accountValueNode('myAccount')); - expect(result).toBe(accAddress); - }); - - test('should throw for unknown account reference', async () => { - const visitor = makeVisitor(); - await expect(visitor.visitAccountValue(accountValueNode('unknown'))).rejects.toThrow( - /Referenced node \[unknown\] not found in \[testInstruction\]/, - ); - }); - - test('should throw on circular dependency', async () => { - const visitor = makeVisitor({ - ixNode: ixNodeWithAccount, - resolutionPath: ['myAccount'], - }); - await expect(visitor.visitAccountValue(accountValueNode('myAccount'))).rejects.toThrow( - /Circular dependency detected: \[/, - ); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/condition-node-value/argumentValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/condition-node-value/argumentValueNode.test.ts deleted file mode 100644 index ddb8bfaa3..000000000 --- a/packages/dynamic-address-resolution/test/visitors/condition-node-value/argumentValueNode.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { argumentValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './condition-node-value-test-utils'; - -describe('condition-node-value: visitArgumentValue', () => { - test('should return argument value', async () => { - const visitor = makeVisitor({ argumentsInput: { amount: 42 } }); - const result = await visitor.visitArgumentValue(argumentValueNode('amount')); - expect(result).toBe(42); - }); - - test('should return undefined for missing argument', async () => { - const visitor = makeVisitor({ argumentsInput: {} }); - const result = await visitor.visitArgumentValue(argumentValueNode('amount')); - expect(result).toBeUndefined(); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/condition-node-value/condition-node-value-test-utils.ts b/packages/dynamic-address-resolution/test/visitors/condition-node-value/condition-node-value-test-utils.ts deleted file mode 100644 index e61413f67..000000000 --- a/packages/dynamic-address-resolution/test/visitors/condition-node-value/condition-node-value-test-utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { instructionNode, programNode, rootNode } from 'codama'; - -import { createConditionNodeValueVisitor } from '../../../src/visitors/condition-node-value'; - -const rootNodeMock = rootNode(programNode({ name: 'test', publicKey: '11111111111111111111111111111111' })); -const ixNodeStub = instructionNode({ name: 'testInstruction' }); - -export function makeVisitor(overrides?: Partial[0]>) { - return createConditionNodeValueVisitor({ - accountsInput: undefined, - argumentsInput: undefined, - ixNode: ixNodeStub, - resolutionPath: [], - resolversInput: undefined, - root: rootNodeMock, - ...overrides, - }); -} diff --git a/packages/dynamic-address-resolution/test/visitors/condition-node-value/resolverValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/condition-node-value/resolverValueNode.test.ts deleted file mode 100644 index 456224e50..000000000 --- a/packages/dynamic-address-resolution/test/visitors/condition-node-value/resolverValueNode.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { resolverValueNode } from 'codama'; -import { describe, expect, test, vi } from 'vitest'; - -import { makeVisitor } from './condition-node-value-test-utils'; - -describe('condition-node-value: visitResolverValue', () => { - test('should return undefined when resolversInput is undefined', async () => { - const visitor = makeVisitor(); - const result = await visitor.visitResolverValue(resolverValueNode('myResolver')); - expect(result).toBeUndefined(); - }); - - test('should return undefined when resolver not in the resolversInput', async () => { - const visitor = makeVisitor({ resolversInput: {} }); - const result = await visitor.visitResolverValue(resolverValueNode('myResolver')); - expect(result).toBeUndefined(); - }); - - test('should call resolver and return result', async () => { - const visitor = makeVisitor({ - resolversInput: { myResolver: async () => await Promise.resolve(999) }, - }); - const result = await visitor.visitResolverValue(resolverValueNode('myResolver')); - expect(result).toBe(999); - }); - - test('should throw ResolverError when resolver throws', async () => { - const visitor = makeVisitor({ - resolversInput: { - myResolver: () => { - throw new Error('some error'); - }, - }, - }); - await expect(visitor.visitResolverValue(resolverValueNode('myResolver'))).rejects.toThrow( - /Resolver \[myResolver\] threw an error while resolving \[conditionalValueNode\] \[myResolver\]/, - ); - }); - - test('should pass arguments and accounts to resolver', async () => { - const resolverSpy = vi.fn().mockReturnValue('resolved'); - const argumentsInput = { amount: 100 }; - const accountsInput = { treasury: null }; - - const visitor = makeVisitor({ - accountsInput, - argumentsInput, - resolversInput: { myResolver: resolverSpy }, - }); - const result = await visitor.visitResolverValue(resolverValueNode('myResolver')); - - expect(result).toBe('resolved'); - expect(resolverSpy).toHaveBeenCalledWith(argumentsInput, accountsInput); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/accountValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/accountValueNode.test.ts deleted file mode 100644 index 11da00b33..000000000 --- a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/accountValueNode.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { accountValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { CONSTANT_PDA_SEED_VALUE_SUPPORTED_NODE_KINDS } from '../../../src/visitors/pda-seed-value'; -import { makeConstantVisitor } from './constant-pda-seed-value-test-utils'; - -describe('constant-pda-seed-value: visitAccountValue', () => { - test('should throw UNEXPECTED_NODE_KIND — accounts are not resolvable in a constant context', async () => { - const visitor = makeConstantVisitor(); - await expect(visitor.visitAccountValue(accountValueNode('anything'))).rejects.toThrow( - `Expected node of kind [${CONSTANT_PDA_SEED_VALUE_SUPPORTED_NODE_KINDS.join(',')}], got [accountValueNode]`, - ); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/argumentValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/argumentValueNode.test.ts deleted file mode 100644 index 4f94072c9..000000000 --- a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/argumentValueNode.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { argumentValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { CONSTANT_PDA_SEED_VALUE_SUPPORTED_NODE_KINDS } from '../../../src/visitors/pda-seed-value'; -import { makeConstantVisitor } from './constant-pda-seed-value-test-utils'; - -describe('constant-pda-seed-value: visitArgumentValue', () => { - test('should throw UNEXPECTED_NODE_KIND — arguments are not resolvable in a constant context', async () => { - const visitor = makeConstantVisitor(); - await expect(visitor.visitArgumentValue(argumentValueNode('anything'))).rejects.toThrow( - `Expected node of kind [${CONSTANT_PDA_SEED_VALUE_SUPPORTED_NODE_KINDS.join(',')}], got [argumentValueNode]`, - ); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/booleanValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/booleanValueNode.test.ts deleted file mode 100644 index d1e71a094..000000000 --- a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/booleanValueNode.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { getBooleanCodec } from '@solana/codecs'; -import { booleanValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeConstantVisitor } from './constant-pda-seed-value-test-utils'; - -describe('constant-pda-seed-value: visitBooleanValue', () => { - test('should encode true', async () => { - const result = await makeConstantVisitor().visitBooleanValue(booleanValueNode(true)); - expect(result).toEqual(getBooleanCodec().encode(true)); - }); - - test('should encode false', async () => { - const result = await makeConstantVisitor().visitBooleanValue(booleanValueNode(false)); - expect(result).toEqual(getBooleanCodec().encode(false)); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/bytesValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/bytesValueNode.test.ts deleted file mode 100644 index 9d6b02011..000000000 --- a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/bytesValueNode.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { getBase16Codec, getBase58Codec, getUtf8Codec } from '@solana/codecs'; -import { bytesValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeConstantVisitor } from './constant-pda-seed-value-test-utils'; - -describe('constant-pda-seed-value: visitBytesValue', () => { - test('should encode base16 data', async () => { - const hex = '48656c6c6f'; - const result = await makeConstantVisitor().visitBytesValue(bytesValueNode('base16', hex)); - expect(result).toEqual(getBase16Codec().encode(hex)); - }); - - test('should encode base58 data', async () => { - const b58 = await generateAddress(); - const result = await makeConstantVisitor().visitBytesValue(bytesValueNode('base58', b58)); - expect(result).toEqual(getBase58Codec().encode(b58)); - }); - - test('should encode utf8 data', async () => { - const text = 'Hello'; - const result = await makeConstantVisitor().visitBytesValue(bytesValueNode('utf8', text)); - expect(result).toEqual(getUtf8Codec().encode(text)); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/constant-pda-seed-value-test-utils.ts b/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/constant-pda-seed-value-test-utils.ts deleted file mode 100644 index b090930f7..000000000 --- a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/constant-pda-seed-value-test-utils.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { address } from '@solana/addresses'; - -import { createConstantPdaSeedValueVisitor } from '../../../src/visitors/pda-seed-value'; - -const PROGRAM_PUBLIC_KEY = '11111111111111111111111111111111'; - -export function makeConstantVisitor(overrides?: Partial[0]>) { - return createConstantPdaSeedValueVisitor({ - programId: address(PROGRAM_PUBLIC_KEY), - ...overrides, - }); -} diff --git a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/constantValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/constantValueNode.test.ts deleted file mode 100644 index 5d390fc81..000000000 --- a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/constantValueNode.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { getUtf8Codec } from '@solana/codecs'; -import { constantValueNode, mapValueNode, numberTypeNode, stringTypeNode, stringValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { CONSTANT_PDA_SEED_VALUE_SUPPORTED_NODE_KINDS } from '../../../src/visitors/pda-seed-value'; -import { makeConstantVisitor } from './constant-pda-seed-value-test-utils'; - -describe('constant-pda-seed-value: visitConstantValue', () => { - test('should delegate to inner stringValueNode', async () => { - const node = constantValueNode(stringTypeNode('utf8'), stringValueNode('Hello world')); - const result = await makeConstantVisitor().visitConstantValue(node); - expect(result).toEqual(getUtf8Codec().encode('Hello world')); - }); - - test('should recurse through nested constantValueNodes', async () => { - const node = constantValueNode( - stringTypeNode('utf8'), - constantValueNode(stringTypeNode('utf8'), stringValueNode('Nested hello world')), - ); - const result = await makeConstantVisitor().visitConstantValue(node); - expect(result).toEqual(getUtf8Codec().encode('Nested hello world')); - }); - - test('should throw for unsupported inner node kind via fallback', async () => { - const node = constantValueNode(numberTypeNode('u8'), mapValueNode([])); - await expect(makeConstantVisitor().visitConstantValue(node)).rejects.toThrow( - `Expected node of kind [${CONSTANT_PDA_SEED_VALUE_SUPPORTED_NODE_KINDS.join(',')}], got [mapValueNode]`, - ); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/noneValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/noneValueNode.test.ts deleted file mode 100644 index 0990d1bda..000000000 --- a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/noneValueNode.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { noneValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeConstantVisitor } from './constant-pda-seed-value-test-utils'; - -describe('constant-pda-seed-value: visitNoneValue', () => { - test('should return empty Uint8Array', async () => { - const result = await makeConstantVisitor().visitNoneValue(noneValueNode()); - expect(result).toEqual(new Uint8Array(0)); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/numberValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/numberValueNode.test.ts deleted file mode 100644 index af06fadc2..000000000 --- a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/numberValueNode.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { numberValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeConstantVisitor } from './constant-pda-seed-value-test-utils'; - -describe('constant-pda-seed-value: visitNumberValue', () => { - test('should encode valid u8', async () => { - const result = await makeConstantVisitor().visitNumberValue(numberValueNode(1)); - expect(result).toEqual(new Uint8Array([1])); - }); - - test('should throw for value out of u8 range', async () => { - await expect(makeConstantVisitor().visitNumberValue(numberValueNode(256))).rejects.toThrow(/out of range/); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/programIdValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/programIdValueNode.test.ts deleted file mode 100644 index 83be58b59..000000000 --- a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/programIdValueNode.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { getAddressEncoder } from '@solana/addresses'; -import { programIdValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeConstantVisitor } from './constant-pda-seed-value-test-utils'; - -describe('constant-pda-seed-value: visitProgramIdValue', () => { - test('should encode the context programId as 32-byte address', async () => { - const randomAddress = await generateAddress(); - const result = await makeConstantVisitor({ programId: randomAddress }).visitProgramIdValue( - programIdValueNode(), - ); - expect(result).toEqual(getAddressEncoder().encode(randomAddress)); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/publicKeyValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/publicKeyValueNode.test.ts deleted file mode 100644 index d3328b21c..000000000 --- a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/publicKeyValueNode.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { address, getAddressEncoder } from '@solana/addresses'; -import { publicKeyValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeConstantVisitor } from './constant-pda-seed-value-test-utils'; - -describe('constant-pda-seed-value: visitPublicKeyValue', () => { - test('should encode the provided public key as 32-byte address', async () => { - const randomAddress = await generateAddress(); - const result = await makeConstantVisitor().visitPublicKeyValue(publicKeyValueNode(randomAddress)); - expect(result).toEqual(getAddressEncoder().encode(address(randomAddress))); - }); - - test('should throw for invalid public key', async () => { - await expect(makeConstantVisitor().visitPublicKeyValue(publicKeyValueNode('not-a-key'))).rejects.toThrow( - /Cannot convert value to Address/, - ); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/someValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/someValueNode.test.ts deleted file mode 100644 index 0d4c86887..000000000 --- a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/someValueNode.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { mapValueNode, numberValueNode, someValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { CONSTANT_PDA_SEED_VALUE_SUPPORTED_NODE_KINDS } from '../../../src/visitors/pda-seed-value'; -import { makeConstantVisitor } from './constant-pda-seed-value-test-utils'; - -describe('constant-pda-seed-value: visitSomeValue', () => { - test('should delegate to inner numberValueNode', async () => { - const node = someValueNode(numberValueNode(42)); - const result = await makeConstantVisitor().visitSomeValue(node); - expect(result).toEqual(new Uint8Array([42])); - }); - - test('should throw for unsupported inner node kind', async () => { - const node = someValueNode(mapValueNode([])); - await expect(makeConstantVisitor().visitSomeValue(node)).rejects.toThrow( - `Expected node of kind [${CONSTANT_PDA_SEED_VALUE_SUPPORTED_NODE_KINDS.join(',')}], got [mapValueNode]`, - ); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/stringValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/stringValueNode.test.ts deleted file mode 100644 index 832104939..000000000 --- a/packages/dynamic-address-resolution/test/visitors/constant-pda-seed-value/stringValueNode.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { getUtf8Codec } from '@solana/codecs'; -import { stringValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeConstantVisitor } from './constant-pda-seed-value-test-utils'; - -describe('constant-pda-seed-value: visitStringValue', () => { - test('should encode non-empty string as UTF-8 bytes', async () => { - const result = await makeConstantVisitor().visitStringValue(stringValueNode('hello')); - expect(result).toEqual(getUtf8Codec().encode('hello')); - }); - - test('should encode empty string as empty bytes', async () => { - const result = await makeConstantVisitor().visitStringValue(stringValueNode('')); - expect(result).toEqual(getUtf8Codec().encode('')); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/booleanValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/default-value-encoder/booleanValueNode.test.ts deleted file mode 100644 index 0b118edf4..000000000 --- a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/booleanValueNode.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { getBooleanCodec } from '@solana/codecs'; -import { booleanTypeNode, booleanValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './default-value-encoder-test-utils'; - -describe('default-value-encoder: visitBooleanValue', () => { - test('should encode true', () => { - const visitor = makeVisitor(booleanTypeNode()); - const result = visitor.visitBooleanValue(booleanValueNode(true)); - expect(result).toEqual(getBooleanCodec().encode(true)); - }); - - test('should encode false', () => { - const visitor = makeVisitor(booleanTypeNode()); - const result = visitor.visitBooleanValue(booleanValueNode(false)); - expect(result).toEqual(getBooleanCodec().encode(false)); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/bytesValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/default-value-encoder/bytesValueNode.test.ts deleted file mode 100644 index d2ae05557..000000000 --- a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/bytesValueNode.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { getBase16Codec, getBase58Codec, getUtf8Codec } from '@solana/codecs'; -import { bytesTypeNode, bytesValueNode, fixedSizeTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeVisitor } from './default-value-encoder-test-utils'; - -describe('default-value-encoder: visitBytesValue', () => { - test('should encode base16 bytes', () => { - const visitor = makeVisitor(fixedSizeTypeNode(bytesTypeNode(), 4)); - const result = visitor.visitBytesValue(bytesValueNode('base16', 'deadbeef')); - expect(result).toEqual(getBase16Codec().encode('deadbeef')); - }); - - test('should encode base58 bytes', async () => { - const bs58 = await generateAddress(); - const visitor = makeVisitor(fixedSizeTypeNode(bytesTypeNode(), 32)); - const result = visitor.visitBytesValue(bytesValueNode('base58', bs58)); - expect(result).toEqual(getBase58Codec().encode(bs58)); - }); - - test('should encode utf8 bytes', () => { - const visitor = makeVisitor(fixedSizeTypeNode(bytesTypeNode(), 5)); - const result = visitor.visitBytesValue(bytesValueNode('utf8', 'hello')); - expect(result).toEqual(getUtf8Codec().encode('hello')); - }); - - test('should encode empty bytes', () => { - const visitor = makeVisitor(fixedSizeTypeNode(bytesTypeNode(), 0)); - const result = visitor.visitBytesValue(bytesValueNode('base16', '')); - expect(result).toEqual(new Uint8Array(0)); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/default-value-encoder-test-utils.ts b/packages/dynamic-address-resolution/test/visitors/default-value-encoder/default-value-encoder-test-utils.ts deleted file mode 100644 index 97c4e5cb6..000000000 --- a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/default-value-encoder-test-utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getNodeCodec } from '@codama/dynamic-codecs'; -import type { InstructionArgumentNode, TypeNode } from 'codama'; -import { instructionArgumentNode, instructionNode, programNode, rootNode } from 'codama'; - -import { createDefaultValueEncoderVisitor } from '../../../src/visitors/default-value-encoder'; - -const root = rootNode(programNode({ name: 'test_program', publicKey: '11111111111111111111111111111111' })); - -export function makeVisitor(argType: TypeNode) { - const ixArgNode: InstructionArgumentNode = instructionArgumentNode({ - name: 'testArg', - type: argType, - }); - const ixNode = instructionNode({ - arguments: [ixArgNode], - name: 'testInstruction', - }); - const codec = getNodeCodec([root, root.program, ixNode, ixArgNode]); - return createDefaultValueEncoderVisitor(codec); -} diff --git a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/enumValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/default-value-encoder/enumValueNode.test.ts deleted file mode 100644 index d48683e44..000000000 --- a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/enumValueNode.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { enumEmptyVariantTypeNode, enumTypeNode, enumValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './default-value-encoder-test-utils'; - -describe('default-value-encoder: visitEnumValue', () => { - test('should encode variants', () => { - const visitor = makeVisitor( - enumTypeNode([enumEmptyVariantTypeNode('variantA'), enumEmptyVariantTypeNode('variantB')]), - ); - const variantA = visitor.visitEnumValue(enumValueNode('testEnum', 'variantA')); - expect(variantA).toEqual(new Uint8Array([0])); - - const variantB = visitor.visitEnumValue(enumValueNode('testEnum', 'variantB')); - expect(variantB).toEqual(new Uint8Array([1])); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/noneValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/default-value-encoder/noneValueNode.test.ts deleted file mode 100644 index e27533673..000000000 --- a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/noneValueNode.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { noneValueNode, numberTypeNode, optionTypeNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './default-value-encoder-test-utils'; - -describe('default-value-encoder: visitNoneValue', () => { - test('should encode none as option prefix byte 0', () => { - const visitor = makeVisitor(optionTypeNode(numberTypeNode('u8'))); - const result = visitor.visitNoneValue(noneValueNode()); - expect(result).toEqual(new Uint8Array([0])); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/numberValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/default-value-encoder/numberValueNode.test.ts deleted file mode 100644 index e025e4371..000000000 --- a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/numberValueNode.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { getU8Codec, getU32Codec } from '@solana/codecs'; -import { numberTypeNode, numberValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './default-value-encoder-test-utils'; - -describe('default-value-encoder: visitNumberValue', () => { - test('should encode u8 value', () => { - const visitor = makeVisitor(numberTypeNode('u8')); - const result = visitor.visitNumberValue(numberValueNode(42)); - expect(result).toEqual(getU8Codec().encode(42)); - }); - - test('should encode zero', () => { - const visitor = makeVisitor(numberTypeNode('u8')); - const result = visitor.visitNumberValue(numberValueNode(0)); - expect(result).toEqual(getU8Codec().encode(0)); - }); - - test('should encode u32 value', () => { - const visitor = makeVisitor(numberTypeNode('u32')); - const result = visitor.visitNumberValue(numberValueNode(100_000)); - expect(result).toEqual(getU32Codec().encode(100_000)); - }); - - test('should encode max u8 value', () => { - const visitor = makeVisitor(numberTypeNode('u8')); - const result = visitor.visitNumberValue(numberValueNode(255)); - expect(result).toEqual(getU8Codec().encode(255)); - }); - - test('should throw on overflow for u8 value', () => { - const visitor = makeVisitor(numberTypeNode('u8')); - expect(() => visitor.visitNumberValue(numberValueNode(1000))).toThrow(); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/publicKeyValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/default-value-encoder/publicKeyValueNode.test.ts deleted file mode 100644 index e1d81f9b0..000000000 --- a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/publicKeyValueNode.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { getAddressCodec } from '@solana/addresses'; -import { publicKeyTypeNode, publicKeyValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeVisitor } from './default-value-encoder-test-utils'; - -describe('default-value-encoder: visitPublicKeyValue', () => { - test('should encode public key as 32 bytes', async () => { - const pubkey = await generateAddress(); - const visitor = makeVisitor(publicKeyTypeNode()); - const result = visitor.visitPublicKeyValue(publicKeyValueNode(pubkey)); - expect(result).toEqual(getAddressCodec().encode(pubkey)); - expect(result.length).toBe(32); - }); - - test('should throw for invalid public key', () => { - const invalidPubkey = 'not-a-valid-pubkey'; - const visitor = makeVisitor(publicKeyTypeNode()); - expect(() => visitor.visitPublicKeyValue(publicKeyValueNode(invalidPubkey))).toThrow(); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/stringValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/default-value-encoder/stringValueNode.test.ts deleted file mode 100644 index ef53e0f42..000000000 --- a/packages/dynamic-address-resolution/test/visitors/default-value-encoder/stringValueNode.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getBase16Codec, getBase58Codec, getUtf8Codec } from '@solana/codecs'; -import { fixedSizeTypeNode, stringTypeNode, stringValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './default-value-encoder-test-utils'; - -describe('default-value-encoder: visitStringValue', () => { - test('should encode utf8 string', () => { - const visitor = makeVisitor(fixedSizeTypeNode(stringTypeNode('utf8'), 5)); - const result = visitor.visitStringValue(stringValueNode('hello')); - expect(result).toEqual(getUtf8Codec().encode('hello')); - }); - - test('should encode base16 string', () => { - const visitor = makeVisitor(fixedSizeTypeNode(stringTypeNode('base16'), 4)); - const result = visitor.visitStringValue(stringValueNode('deadbeef')); - expect(result).toEqual(getBase16Codec().encode('deadbeef')); - }); - - test('should encode base58 string', () => { - const visitor = makeVisitor(fixedSizeTypeNode(stringTypeNode('base58'), 3)); - const result = visitor.visitStringValue(stringValueNode('abc')); - expect(result).toEqual(getBase58Codec().encode('abc')); - }); - - test('should encode empty string', () => { - const visitor = makeVisitor(fixedSizeTypeNode(stringTypeNode('utf8'), 0)); - const result = visitor.visitStringValue(stringValueNode('')); - expect(result).toEqual(getUtf8Codec().encode('')); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/accountValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/pda-seed-value/accountValueNode.test.ts deleted file mode 100644 index 6452cf13f..000000000 --- a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/accountValueNode.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { address, getAddressEncoder } from '@solana/addresses'; -import { accountValueNode, instructionAccountNode, instructionNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeVisitor } from './pda-seed-value-test-utils'; - -describe('pda-seed-value: visitAccountValue', () => { - const ixNodeWithAccount = instructionNode({ - accounts: [ - instructionAccountNode({ - isSigner: false, - isWritable: false, - name: 'authority', - }), - ], - name: 'testInstruction', - }); - - test('should encode provided account address', async () => { - const randomAddress = await generateAddress(); - const visitor = makeVisitor({ - accountsInput: { authority: randomAddress }, - ixNode: ixNodeWithAccount, - }); - const result = await visitor.visitAccountValue(accountValueNode('authority')); - expect(result).toEqual(getAddressEncoder().encode(address(randomAddress))); - }); - - test('should fall through to resolution when provided address is null', async () => { - const visitor = makeVisitor({ - accountsInput: { authority: null }, - ixNode: ixNodeWithAccount, - }); - // null is not treated as a provided address — it falls through to resolveAccountAddress, - // which throws because the account has no default value - await expect(visitor.visitAccountValue(accountValueNode('authority'))).rejects.toThrow( - /Missing account \[authority\]/, - ); - }); - - test('should throw when resolved address is null', async () => { - const ixNodeWithOptionalAccount = instructionNode({ - accounts: [ - instructionAccountNode({ - isOptional: true, - isSigner: false, - isWritable: false, - name: 'authority', - }), - ], - name: 'testInstruction', - optionalAccountStrategy: 'omitted', - }); - const visitor = makeVisitor({ - accountsInput: { authority: null }, - ixNode: ixNodeWithOptionalAccount, - }); - await expect(visitor.visitAccountValue(accountValueNode('authority'))).rejects.toThrow( - /Failed to derive PDA for account \[authority\]/, - ); - }); - - test('should throw for unknown account reference', async () => { - const visitor = makeVisitor({ ixNode: ixNodeWithAccount }); - await expect(visitor.visitAccountValue(accountValueNode('nonexistent'))).rejects.toThrow( - /Referenced node \[nonexistent\] not found in \[testInstruction\]/, - ); - }); - - test('should throw on circular dependency', async () => { - const visitor = makeVisitor({ - ixNode: ixNodeWithAccount, - resolutionPath: ['authority'], - }); - await expect(visitor.visitAccountValue(accountValueNode('authority'))).rejects.toThrow( - /Circular dependency detected: \[/, - ); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/argumentValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/pda-seed-value/argumentValueNode.test.ts deleted file mode 100644 index f62b9be99..000000000 --- a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/argumentValueNode.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { getUtf8Codec } from '@solana/codecs'; -import { - argumentValueNode, - instructionArgumentNode, - instructionNode, - numberTypeNode, - publicKeyTypeNode, - remainderOptionTypeNode, - sizePrefixTypeNode, - stringTypeNode, -} from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeVisitor } from './pda-seed-value-test-utils'; - -describe('pda-seed-value: visitArgumentValue', () => { - const ixNodeWithArg = instructionNode({ - arguments: [instructionArgumentNode({ name: 'title', type: stringTypeNode('utf8') })], - name: 'testInstruction', - }); - - test('should encode argument value using its codec', async () => { - const visitor = makeVisitor({ - argumentsInput: { title: 'hello' }, - ixNode: ixNodeWithArg, - }); - const result = await visitor.visitArgumentValue(argumentValueNode('title')); - expect(result).toEqual(getUtf8Codec().encode('hello')); - }); - - test('should use seedTypeNode override instead of argument type', async () => { - // Argument type is sizePrefixTypeNode (u32 length prefix + utf8 string), - // but seedTypeNode overrides it to plain stringTypeNode (raw utf8 bytes only). - // This mirrors how on-chain PDA derivation uses raw string bytes as seeds, - // even when the instruction argument is serialized with a length prefix. - const sizePrefixedArgType = sizePrefixTypeNode(stringTypeNode('utf8'), numberTypeNode('u32')); - const ixNodeWithSizePrefixedArg = instructionNode({ - arguments: [instructionArgumentNode({ name: 'name', type: sizePrefixedArgType })], - name: 'testInstruction', - }); - - const visitor = makeVisitor({ - argumentsInput: { name: 'hello' }, - ixNode: ixNodeWithSizePrefixedArg, - seedTypeNode: stringTypeNode('utf8'), - }); - const result = await visitor.visitArgumentValue(argumentValueNode('name')); - - // Should produce raw utf8 bytes (5 bytes), NOT size-prefixed bytes (4 + 5 = 9 bytes) - const rawUtf8Bytes = getUtf8Codec().encode('hello'); - expect(result).toEqual(rawUtf8Bytes); - expect(result.length).toBe(5); - }); - - test('should throw for unknown argument name', async () => { - const visitor = makeVisitor({ ixNode: ixNodeWithArg }); - await expect(visitor.visitArgumentValue(argumentValueNode('unknown'))).rejects.toThrow( - /Referenced node \[unknown\] not found in \[testInstruction\]/, - ); - }); - - test('should throw when required argument value is undefined', async () => { - const visitor = makeVisitor({ - argumentsInput: {}, - ixNode: ixNodeWithArg, - }); - await expect(visitor.visitArgumentValue(argumentValueNode('title'))).rejects.toThrow( - /Missing argument \[title\]/, - ); - }); - - test('should throw when required argument value is null', async () => { - const visitor = makeVisitor({ - argumentsInput: { title: null }, - ixNode: ixNodeWithArg, - }); - await expect(visitor.visitArgumentValue(argumentValueNode('title'))).rejects.toThrow( - /Missing argument \[title\]/, - ); - }); - - describe('remainderOptionTypeNode seeds', () => { - // Mirrors the pmp IDL's metadata PDA: - // the "authority" seed is remainderOptionTypeNode(publicKeyTypeNode) — null is canonical. - const ixNodeWithOptionalSeed = instructionNode({ - arguments: [ - instructionArgumentNode({ - name: 'authority', - type: remainderOptionTypeNode(publicKeyTypeNode()), - }), - ], - name: 'testInstruction', - }); - - test('should return empty bytes when argument is undefined', async () => { - const visitor = makeVisitor({ - argumentsInput: {}, - ixNode: ixNodeWithOptionalSeed, - }); - const result = await visitor.visitArgumentValue(argumentValueNode('authority')); - expect(result).toEqual(new Uint8Array(0)); - }); - - test('should return empty bytes when argument is null', async () => { - const visitor = makeVisitor({ - argumentsInput: { authority: null }, - ixNode: ixNodeWithOptionalSeed, - }); - const result = await visitor.visitArgumentValue(argumentValueNode('authority')); - expect(result).toEqual(new Uint8Array(0)); - }); - - test('should encode value when argument is provided', async () => { - const authority = await generateAddress(); - const visitor = makeVisitor({ - argumentsInput: { authority }, - ixNode: ixNodeWithOptionalSeed, - }); - const result = await visitor.visitArgumentValue(argumentValueNode('authority')); - expect(result.length).toBe(32); - }); - - test('should return empty bytes when seedTypeNode override is remainderOptionTypeNode and argument is null', async () => { - const ixNodeWithRequiredArg = instructionNode({ - arguments: [instructionArgumentNode({ name: 'authority', type: publicKeyTypeNode() })], - name: 'testInstruction', - }); - const visitor = makeVisitor({ - argumentsInput: { authority: null }, - ixNode: ixNodeWithRequiredArg, - seedTypeNode: remainderOptionTypeNode(publicKeyTypeNode()), - }); - const result = await visitor.visitArgumentValue(argumentValueNode('authority')); - expect(result).toEqual(new Uint8Array(0)); - }); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/booleanValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/pda-seed-value/booleanValueNode.test.ts deleted file mode 100644 index 779e865de..000000000 --- a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/booleanValueNode.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { getBooleanCodec } from '@solana/codecs'; -import { booleanValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './pda-seed-value-test-utils'; - -describe('pda-seed-value: visitBooleanValue', () => { - test('should encode true', async () => { - const result = await makeVisitor().visitBooleanValue(booleanValueNode(true)); - expect(result).toEqual(getBooleanCodec().encode(true)); - }); - - test('should encode false', async () => { - const result = await makeVisitor().visitBooleanValue(booleanValueNode(false)); - expect(result).toEqual(getBooleanCodec().encode(false)); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/bytesValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/pda-seed-value/bytesValueNode.test.ts deleted file mode 100644 index 903a840e4..000000000 --- a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/bytesValueNode.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { getBase16Codec, getBase58Codec, getUtf8Codec } from '@solana/codecs'; -import { bytesValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeVisitor } from './pda-seed-value-test-utils'; - -describe('pda-seed-value: visitBytesValue', () => { - test('should encode base16 data', async () => { - // const hex = Buffer.from('Hello', 'utf8').toString('hex'); - const hex = '48656c6c6f'; - const result = await makeVisitor().visitBytesValue(bytesValueNode('base16', hex)); - expect(result).toEqual(getBase16Codec().encode(hex)); - }); - - test('should encode base58 data', async () => { - const b58 = await generateAddress(); - const result = await makeVisitor().visitBytesValue(bytesValueNode('base58', b58)); - expect(result).toEqual(getBase58Codec().encode(b58)); - }); - - test('should encode utf8 data', async () => { - const text = 'Hello'; - const result = await makeVisitor().visitBytesValue(bytesValueNode('utf8', text)); - expect(result).toEqual(getUtf8Codec().encode(text)); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/constantValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/pda-seed-value/constantValueNode.test.ts deleted file mode 100644 index 630852ed6..000000000 --- a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/constantValueNode.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { getUtf8Codec } from '@solana/codecs'; -import { - argumentValueNode, - constantValueNode, - instructionArgumentNode, - instructionNode, - mapValueNode, - numberTypeNode, - stringTypeNode, - stringValueNode, -} from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { PDA_SEED_VALUE_SUPPORTED_NODE_KINDS } from '../../../src/visitors/pda-seed-value'; -import { makeVisitor } from './pda-seed-value-test-utils'; - -describe('pda-seed-value: visitConstantValue', () => { - test('should delegate to inner stringValueNode', async () => { - const node = constantValueNode(stringTypeNode('utf8'), stringValueNode('Hello world')); - const result = await makeVisitor().visitConstantValue(node); - expect(result).toEqual(getUtf8Codec().encode('Hello world')); - }); - - test('should delegate to nested inner constantValueNode', async () => { - const node = constantValueNode( - stringTypeNode('utf8'), - constantValueNode(stringTypeNode('utf8'), stringValueNode('Nested hello world')), - ); - const result = await makeVisitor().visitConstantValue(node); - expect(result).toEqual(getUtf8Codec().encode('Nested hello world')); - }); - - test('should throw for unsupported inner node kind', async () => { - const node = constantValueNode(numberTypeNode('u8'), mapValueNode([])); - await expect(makeVisitor().visitConstantValue(node)).rejects.toThrow( - `Expected node of kind [${PDA_SEED_VALUE_SUPPORTED_NODE_KINDS.join(',')}], got [mapValueNode]`, - ); - }); - - test('should resolve the argument when wrapping an argumentValueNode', async () => { - const visitor = makeVisitor({ - argumentsInput: { title: 'hello' }, - ixNode: instructionNode({ - arguments: [instructionArgumentNode({ name: 'title', type: stringTypeNode('utf8') })], - name: 'testInstruction', - }), - }); - // @ts-expect-error Deliberate constraint violation for testing extended recursion through constantValueNode - const node = constantValueNode(stringTypeNode('utf8'), argumentValueNode('title')); - const result = await visitor.visitConstantValue(node); - expect(result).toEqual(getUtf8Codec().encode('hello')); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/noneValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/pda-seed-value/noneValueNode.test.ts deleted file mode 100644 index 7f7180eec..000000000 --- a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/noneValueNode.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { noneValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './pda-seed-value-test-utils'; - -describe('pda-seed-value: visitNoneValue', () => { - test('should return empty Uint8Array', async () => { - const result = await makeVisitor().visitNoneValue(noneValueNode()); - expect(result).toEqual(new Uint8Array(0)); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/numberValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/pda-seed-value/numberValueNode.test.ts deleted file mode 100644 index d5909f931..000000000 --- a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/numberValueNode.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { CodamaError } from '@codama/errors'; -import { numberValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './pda-seed-value-test-utils'; - -describe('pda-seed-value: visitNumberValue', () => { - test('should encode 0 as single byte', async () => { - const result = await makeVisitor().visitNumberValue(numberValueNode(0)); - expect(result).toEqual(new Uint8Array([0])); - }); - - test('should encode 255 as single byte', async () => { - const result = await makeVisitor().visitNumberValue(numberValueNode(255)); - expect(result).toEqual(new Uint8Array([255])); - }); - - test('should throw for value > 255', async () => { - await expect(makeVisitor().visitNumberValue(numberValueNode(256))).rejects.toThrow(/out of range/); - }); - - test('should throw for negative value', async () => { - await expect(makeVisitor().visitNumberValue(numberValueNode(-1))).rejects.toThrow(CodamaError); - }); - - test('should throw for non-integer value', async () => { - await expect(makeVisitor().visitNumberValue(numberValueNode(1.5))).rejects.toThrow(/out of range/); - }); - - test('should throw for large value', async () => { - await expect(makeVisitor().visitNumberValue(numberValueNode(70000))).rejects.toThrow(/out of range/); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/pda-seed-value-test-utils.ts b/packages/dynamic-address-resolution/test/visitors/pda-seed-value/pda-seed-value-test-utils.ts deleted file mode 100644 index df2ef7056..000000000 --- a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/pda-seed-value-test-utils.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { address } from '@solana/addresses'; -import { instructionNode, programNode, rootNode } from 'codama'; - -import { createPdaSeedValueVisitor } from '../../../src/visitors/pda-seed-value'; - -const PROGRAM_PUBLIC_KEY = '11111111111111111111111111111111'; - -export const rootNodeMock = rootNode(programNode({ name: 'test', publicKey: PROGRAM_PUBLIC_KEY })); - -export const ixNodeStub = instructionNode({ name: 'testInstruction' }); - -export function makeVisitor(overrides?: Partial[0]>) { - return createPdaSeedValueVisitor({ - accountsInput: undefined, - argumentsInput: undefined, - ixNode: ixNodeStub, - programId: address(PROGRAM_PUBLIC_KEY), - resolutionPath: [], - resolversInput: undefined, - root: rootNodeMock, - ...overrides, - }); -} diff --git a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/programIdValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/pda-seed-value/programIdValueNode.test.ts deleted file mode 100644 index a66ea42de..000000000 --- a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/programIdValueNode.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { getAddressEncoder } from '@solana/addresses'; -import { programIdValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeVisitor } from './pda-seed-value-test-utils'; - -describe('pda-seed-value: visitProgramIdValue', () => { - test('should encode the context programId as 32-byte address', async () => { - const randomAddress = await generateAddress(); - const result = await makeVisitor({ - programId: randomAddress, - }).visitProgramIdValue(programIdValueNode()); - expect(result).toEqual(getAddressEncoder().encode(randomAddress)); - }); - - test('should throw an error for non-string programId', async () => { - const invalidValues = [42, [1, 2, 3], null]; - for (const value of invalidValues) { - // @ts-expect-error testing invalid programId value - const visitor = makeVisitor({ programId: value }); - await expect(visitor.visitProgramIdValue(programIdValueNode())).rejects.toThrow( - `Cannot convert value to Address: [${JSON.stringify(value)}].`, - ); - } - }); - - test('should throw an error for invalid string programId', async () => { - const invalidValues = ['not-a-key', '123', '', ' ']; - for (const value of invalidValues) { - // @ts-expect-error testing invalid programId value - const visitor = makeVisitor({ programId: value }); - await expect(visitor.visitProgramIdValue(programIdValueNode())).rejects.toThrow( - `Cannot convert value to Address: [${JSON.stringify(value)}].`, - ); - } - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/publicKeyValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/pda-seed-value/publicKeyValueNode.test.ts deleted file mode 100644 index fc39dd4e6..000000000 --- a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/publicKeyValueNode.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { address, getAddressEncoder } from '@solana/addresses'; -import { publicKeyValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeVisitor } from './pda-seed-value-test-utils'; - -describe('pda-seed-value: visitPublicKeyValue', () => { - test('should encode the provided public key as 32-byte address', async () => { - const randomAddress = await generateAddress(); - const result = await makeVisitor().visitPublicKeyValue(publicKeyValueNode(randomAddress)); - expect(result).toEqual(getAddressEncoder().encode(address(randomAddress))); - }); - - test('should throw for invalid public key', async () => { - const invalidPublicKeys = [123, 'not-a-key', [1, 2, 3], null]; - const visitor = makeVisitor(); - - for (const invalidPublicKey of invalidPublicKeys) { - // @ts-expect-error testing invalid inputs - await expect(visitor.visitPublicKeyValue(publicKeyValueNode(invalidPublicKey))).rejects.toThrow( - `Cannot convert value to Address: [${JSON.stringify(invalidPublicKey)}].`, - ); - } - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/someValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/pda-seed-value/someValueNode.test.ts deleted file mode 100644 index 55db74712..000000000 --- a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/someValueNode.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { getUtf8Codec } from '@solana/codecs'; -import { - argumentValueNode, - constantValueNode, - instructionArgumentNode, - instructionNode, - mapValueNode, - numberValueNode, - publicKeyTypeNode, - publicKeyValueNode, - someValueNode, - stringTypeNode, -} from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { PDA_SEED_VALUE_SUPPORTED_NODE_KINDS } from '../../../src/visitors/pda-seed-value'; -import { makeVisitor } from './pda-seed-value-test-utils'; - -describe('pda-seed-value: visitSomeValue', () => { - test('should delegate to inner numberValueNode', async () => { - const node = someValueNode(numberValueNode(42)); - const result = await makeVisitor().visitSomeValue(node); - expect(result).toEqual(new Uint8Array([42])); - }); - - test('should throw for unsupported inner node kind', async () => { - const node = someValueNode(mapValueNode([])); - await expect(makeVisitor().visitSomeValue(node)).rejects.toThrow( - `Expected node of kind [${PDA_SEED_VALUE_SUPPORTED_NODE_KINDS.join(',')}], got [mapValueNode]`, - ); - }); - - test('should throw for unsupported nested inner node kind', async () => { - const node = someValueNode(constantValueNode(publicKeyTypeNode(), publicKeyValueNode('invalid-key'))); - await expect(makeVisitor().visitSomeValue(node)).rejects.toThrow( - 'Cannot convert value to Address: ["invalid-key"].', - ); - }); - - test('someValueNode wrapping argumentValueNode resolves the argument', async () => { - const visitor = makeVisitor({ - argumentsInput: { title: 'hello' }, - ixNode: instructionNode({ - arguments: [instructionArgumentNode({ name: 'title', type: stringTypeNode('utf8') })], - name: 'testInstruction', - }), - }); - // @ts-expect-error Deliberate constraint violation for testing extended recursion through someValueNode. - const node = someValueNode(argumentValueNode('title')); - const result = await visitor.visitSomeValue(node); - expect(result).toEqual(getUtf8Codec().encode('hello')); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/stringValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/pda-seed-value/stringValueNode.test.ts deleted file mode 100644 index 98447239c..000000000 --- a/packages/dynamic-address-resolution/test/visitors/pda-seed-value/stringValueNode.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { getUtf8Codec } from '@solana/codecs'; -import { stringValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './pda-seed-value-test-utils'; - -describe('pda-seed-value: visitStringValue', () => { - test('should encode non-empty string as UTF-8 bytes', async () => { - const result = await makeVisitor().visitStringValue(stringValueNode('hello')); - expect(result).toEqual(getUtf8Codec().encode('hello')); - }); - - test('should encode empty string as empty bytes', async () => { - const result = await makeVisitor().visitStringValue(stringValueNode('')); - expect(result).toEqual(getUtf8Codec().encode('')); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/value-node-value/arrayValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/value-node-value/arrayValueNode.test.ts deleted file mode 100644 index 58caca02f..000000000 --- a/packages/dynamic-address-resolution/test/visitors/value-node-value/arrayValueNode.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { accountValueNode, arrayValueNode, numberValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { VALUE_NODE_SUPPORTED_NODE_KINDS } from '../../../src/visitors/value-node-value'; -import { makeVisitor } from './value-node-value-test-utils'; - -describe('value-node-value: visitArrayValue', () => { - test('should resolve empty array', () => { - const result = makeVisitor().visitArrayValue(arrayValueNode([])); - expect(result).toEqual({ kind: 'arrayValueNode', value: [] }); - }); - - test('should resolve array items recursively', () => { - const result = makeVisitor().visitArrayValue(arrayValueNode([numberValueNode(1), numberValueNode(2)])); - expect(result).toEqual({ - kind: 'arrayValueNode', - value: [ - { kind: 'numberValueNode', value: 1 }, - { kind: 'numberValueNode', value: 2 }, - ], - }); - }); - - test('should throw for unsupported inner node', () => { - expect(() => - makeVisitor().visitArrayValue( - // @ts-expect-error - accountValueNode is invalid - arrayValueNode([accountValueNode('test')]), - ), - ).toThrow(`Expected node of kind [${VALUE_NODE_SUPPORTED_NODE_KINDS.join(',')}], got [accountValueNode]`); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/value-node-value/booleanValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/value-node-value/booleanValueNode.test.ts deleted file mode 100644 index 5d4f05411..000000000 --- a/packages/dynamic-address-resolution/test/visitors/value-node-value/booleanValueNode.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { booleanValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './value-node-value-test-utils'; - -describe('value-node-value: visitBooleanValue', () => { - test('should resolve true', () => { - const result = makeVisitor().visitBooleanValue(booleanValueNode(true)); - expect(result).toEqual({ kind: 'booleanValueNode', value: true }); - }); - - test('should resolve false', () => { - const result = makeVisitor().visitBooleanValue(booleanValueNode(false)); - expect(result).toEqual({ kind: 'booleanValueNode', value: false }); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/value-node-value/bytesValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/value-node-value/bytesValueNode.test.ts deleted file mode 100644 index 92dea386c..000000000 --- a/packages/dynamic-address-resolution/test/visitors/value-node-value/bytesValueNode.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { bytesValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './value-node-value-test-utils'; - -describe('value-node-value: visitBytesValue', () => { - test('should resolve with base16 encoding', () => { - const result = makeVisitor().visitBytesValue(bytesValueNode('base16', 'deadbeef')); - expect(result).toEqual({ encoding: 'base16', kind: 'bytesValueNode', value: 'deadbeef' }); - }); - - test('should resolve with base64 encoding', () => { - const result = makeVisitor().visitBytesValue(bytesValueNode('base64', 'AQID')); - expect(result).toEqual({ encoding: 'base64', kind: 'bytesValueNode', value: 'AQID' }); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/value-node-value/constantValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/value-node-value/constantValueNode.test.ts deleted file mode 100644 index 0c6cf5f42..000000000 --- a/packages/dynamic-address-resolution/test/visitors/value-node-value/constantValueNode.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { accountValueNode, constantValueNode, numberTypeNode, numberValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { VALUE_NODE_SUPPORTED_NODE_KINDS } from '../../../src/visitors/value-node-value'; -import { makeVisitor } from './value-node-value-test-utils'; - -describe('value-node-value: visitConstantValue', () => { - test('should delegate to inner value node', () => { - const result = makeVisitor().visitConstantValue(constantValueNode(numberTypeNode('u8'), numberValueNode(255))); - expect(result).toEqual({ kind: 'numberValueNode', value: 255 }); - }); - - test('should throw for unsupported inner node', () => { - expect(() => - makeVisitor().visitConstantValue( - // @ts-expect-error - accountValueNode is invalid inside constantValueNode - constantValueNode(numberTypeNode('u8'), accountValueNode('test')), - ), - ).toThrow(`Expected node of kind [${VALUE_NODE_SUPPORTED_NODE_KINDS.join(',')}], got [accountValueNode]`); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/value-node-value/enumValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/value-node-value/enumValueNode.test.ts deleted file mode 100644 index beae1b49c..000000000 --- a/packages/dynamic-address-resolution/test/visitors/value-node-value/enumValueNode.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { enumValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './value-node-value-test-utils'; - -describe('value-node-value: visitEnumValue', () => { - test('should resolve enum variant', () => { - const result = makeVisitor().visitEnumValue(enumValueNode('TokenStandard', 'nonFungible')); - expect(result).toEqual({ kind: 'enumValueNode', value: 'nonFungible' }); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/value-node-value/mapValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/value-node-value/mapValueNode.test.ts deleted file mode 100644 index 2525df069..000000000 --- a/packages/dynamic-address-resolution/test/visitors/value-node-value/mapValueNode.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { accountValueNode, mapEntryValueNode, mapValueNode, numberValueNode, stringValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { VALUE_NODE_SUPPORTED_NODE_KINDS } from '../../../src/visitors/value-node-value'; -import { makeVisitor } from './value-node-value-test-utils'; - -describe('value-node-value: visitMapValue', () => { - test('should resolve empty map', () => { - const result = makeVisitor().visitMapValue(mapValueNode([])); - expect(result).toEqual({ kind: 'mapValueNode', value: [] }); - }); - - test('should resolve key/value pairs recursively', () => { - const result = makeVisitor().visitMapValue( - mapValueNode([mapEntryValueNode(stringValueNode('key1'), numberValueNode(100))]), - ); - expect(result).toEqual({ - kind: 'mapValueNode', - value: [ - { - key: { kind: 'stringValueNode', value: 'key1' }, - value: { kind: 'numberValueNode', value: 100 }, - }, - ], - }); - }); - - test('should throw for unsupported map key', () => { - expect(() => - makeVisitor().visitMapValue( - mapValueNode([ - mapEntryValueNode( - // @ts-expect-error - accountValueNode is invalid StandaloneValueNode - accountValueNode('test'), - numberValueNode(1), - ), - ]), - ), - ).toThrow(`Expected node of kind [${VALUE_NODE_SUPPORTED_NODE_KINDS.join(',')}], got [accountValueNode]`); - }); - - test('should throw for unsupported map value', () => { - expect(() => - makeVisitor().visitMapValue( - mapValueNode([ - mapEntryValueNode( - stringValueNode('ok'), - // @ts-expect-error - accountValueNode is invalid StandaloneValueNode - accountValueNode('test'), - ), - ]), - ), - ).toThrow(`Expected node of kind [${VALUE_NODE_SUPPORTED_NODE_KINDS.join(',')}], got [accountValueNode]`); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/value-node-value/noneValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/value-node-value/noneValueNode.test.ts deleted file mode 100644 index ce25a298b..000000000 --- a/packages/dynamic-address-resolution/test/visitors/value-node-value/noneValueNode.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { noneValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './value-node-value-test-utils'; - -describe('value-node-value: visitNoneValue', () => { - test('should resolve none to null', () => { - const result = makeVisitor().visitNoneValue(noneValueNode()); - expect(result).toEqual({ kind: 'noneValueNode', value: null }); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/value-node-value/numberValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/value-node-value/numberValueNode.test.ts deleted file mode 100644 index 8d1622440..000000000 --- a/packages/dynamic-address-resolution/test/visitors/value-node-value/numberValueNode.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { numberValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './value-node-value-test-utils'; - -describe('value-node-value: visitNumberValue', () => { - test('should resolve positive number', () => { - const result = makeVisitor().visitNumberValue(numberValueNode(42)); - expect(result).toEqual({ kind: 'numberValueNode', value: 42 }); - }); - - test('should resolve zero', () => { - const result = makeVisitor().visitNumberValue(numberValueNode(0)); - expect(result).toEqual({ kind: 'numberValueNode', value: 0 }); - }); - - test('should resolve negative number', () => { - const result = makeVisitor().visitNumberValue(numberValueNode(-7)); - expect(result).toEqual({ kind: 'numberValueNode', value: -7 }); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/value-node-value/publicKeyValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/value-node-value/publicKeyValueNode.test.ts deleted file mode 100644 index 7ddd2f8a9..000000000 --- a/packages/dynamic-address-resolution/test/visitors/value-node-value/publicKeyValueNode.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { address } from '@solana/addresses'; -import { publicKeyValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { generateAddress } from '../../test-utils'; -import { makeVisitor } from './value-node-value-test-utils'; - -describe('value-node-value: visitPublicKeyValue', () => { - test('should resolve to Address', async () => { - const key = await generateAddress(); - const result = makeVisitor().visitPublicKeyValue(publicKeyValueNode(key)); - expect(result).toEqual({ kind: 'publicKeyValueNode', value: address(key) }); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/value-node-value/setValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/value-node-value/setValueNode.test.ts deleted file mode 100644 index ff51f4844..000000000 --- a/packages/dynamic-address-resolution/test/visitors/value-node-value/setValueNode.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { accountValueNode, numberValueNode, setValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { VALUE_NODE_SUPPORTED_NODE_KINDS } from '../../../src/visitors/value-node-value'; -import { makeVisitor } from './value-node-value-test-utils'; - -describe('value-node-value: visitSetValue', () => { - test('should resolve empty set', () => { - const result = makeVisitor().visitSetValue(setValueNode([])); - expect(result).toEqual({ kind: 'setValueNode', value: [] }); - }); - - test('should resolve set items', () => { - const result = makeVisitor().visitSetValue(setValueNode([numberValueNode(10), numberValueNode(20)])); - expect(result).toEqual({ - kind: 'setValueNode', - value: [ - { kind: 'numberValueNode', value: 10 }, - { kind: 'numberValueNode', value: 20 }, - ], - }); - }); - - test('should throw for unsupported inner node', () => { - expect(() => - makeVisitor().visitSetValue( - // @ts-expect-error - accountValueNode is invalid as StandaloneValueNode - setValueNode([accountValueNode('test')]), - ), - ).toThrow(`Expected node of kind [${VALUE_NODE_SUPPORTED_NODE_KINDS.join(',')}], got [accountValueNode]`); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/value-node-value/someValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/value-node-value/someValueNode.test.ts deleted file mode 100644 index b64151b5c..000000000 --- a/packages/dynamic-address-resolution/test/visitors/value-node-value/someValueNode.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { accountValueNode, numberValueNode, someValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { VALUE_NODE_SUPPORTED_NODE_KINDS } from '../../../src/visitors/value-node-value'; -import { makeVisitor } from './value-node-value-test-utils'; - -describe('value-node-value: visitSomeValue', () => { - test('should delegate to inner value node', () => { - const result = makeVisitor().visitSomeValue(someValueNode(numberValueNode(42))); - expect(result).toEqual({ kind: 'numberValueNode', value: 42 }); - }); - - test('should throw for unsupported inner node', () => { - expect(() => - makeVisitor().visitSomeValue( - // @ts-expect-error - accountValueNode is invalid - someValueNode(accountValueNode('test')), - ), - ).toThrow(`Expected node of kind [${VALUE_NODE_SUPPORTED_NODE_KINDS.join(',')}], got [accountValueNode]`); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/value-node-value/stringValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/value-node-value/stringValueNode.test.ts deleted file mode 100644 index 43d3c87fb..000000000 --- a/packages/dynamic-address-resolution/test/visitors/value-node-value/stringValueNode.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { stringValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { makeVisitor } from './value-node-value-test-utils'; - -describe('value-node-value: visitStringValue', () => { - test('should resolve string', () => { - const result = makeVisitor().visitStringValue(stringValueNode('hello')); - expect(result).toEqual({ kind: 'stringValueNode', value: 'hello' }); - }); - - test('should resolve empty string', () => { - const result = makeVisitor().visitStringValue(stringValueNode('')); - expect(result).toEqual({ kind: 'stringValueNode', value: '' }); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/value-node-value/structValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/value-node-value/structValueNode.test.ts deleted file mode 100644 index e531423b5..000000000 --- a/packages/dynamic-address-resolution/test/visitors/value-node-value/structValueNode.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { accountValueNode, numberValueNode, stringValueNode, structFieldValueNode, structValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { VALUE_NODE_SUPPORTED_NODE_KINDS } from '../../../src/visitors/value-node-value'; -import { makeVisitor } from './value-node-value-test-utils'; - -describe('value-node-value: visitStructValue', () => { - test('should resolve struct fields to object entries', () => { - const result = makeVisitor().visitStructValue( - structValueNode([ - structFieldValueNode('name', stringValueNode('Alice')), - structFieldValueNode('age', numberValueNode(30)), - ]), - ); - expect(result).toEqual({ - kind: 'structValueNode', - value: { - age: { kind: 'numberValueNode', value: 30 }, - name: { kind: 'stringValueNode', value: 'Alice' }, - }, - }); - }); - - test('should throw for unsupported field value', () => { - expect(() => - makeVisitor().visitStructValue( - // @ts-expect-error - accountValueNode is invalid as a StandaloneValueNode - structValueNode([structFieldValueNode('invalid_field', accountValueNode('test'))]), - ), - ).toThrow(`Expected node of kind [${VALUE_NODE_SUPPORTED_NODE_KINDS.join(',')}], got [accountValueNode]`); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/value-node-value/tupleValueNode.test.ts b/packages/dynamic-address-resolution/test/visitors/value-node-value/tupleValueNode.test.ts deleted file mode 100644 index deae081ec..000000000 --- a/packages/dynamic-address-resolution/test/visitors/value-node-value/tupleValueNode.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { accountValueNode, booleanValueNode, numberValueNode, stringValueNode, tupleValueNode } from 'codama'; -import { describe, expect, test } from 'vitest'; - -import { VALUE_NODE_SUPPORTED_NODE_KINDS } from '../../../src/visitors/value-node-value'; -import { makeVisitor } from './value-node-value-test-utils'; - -describe('value-node-value: visitTupleValue', () => { - test('should resolve mixed-type tuple items', () => { - const result = makeVisitor().visitTupleValue( - tupleValueNode([numberValueNode(1), stringValueNode('a'), booleanValueNode(true)]), - ); - expect(result).toEqual({ - kind: 'tupleValueNode', - value: [ - { kind: 'numberValueNode', value: 1 }, - { kind: 'stringValueNode', value: 'a' }, - { kind: 'booleanValueNode', value: true }, - ], - }); - }); - - test('should throw for unsupported inner node', () => { - expect(() => - makeVisitor().visitTupleValue( - // @ts-expect-error - accountValueNode is invalid as a StandaloneValueNode - tupleValueNode([accountValueNode('test')]), - ), - ).toThrow(`Expected node of kind [${VALUE_NODE_SUPPORTED_NODE_KINDS.join(',')}], got [accountValueNode]`); - }); -}); diff --git a/packages/dynamic-address-resolution/test/visitors/value-node-value/value-node-value-test-utils.ts b/packages/dynamic-address-resolution/test/visitors/value-node-value/value-node-value-test-utils.ts deleted file mode 100644 index fa5cd746c..000000000 --- a/packages/dynamic-address-resolution/test/visitors/value-node-value/value-node-value-test-utils.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createValueNodeVisitor } from '../../../src/visitors/value-node-value'; - -export function makeVisitor() { - return createValueNodeVisitor(); -} diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index 0c4b7ab81..e1d4638ac 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -85,19 +85,22 @@ export const CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING = 2500003; export const CODAMA_ERROR__DYNAMIC_CLIENT__INVALID_ACCOUNT_ADDRESS = 2500004; export const CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ADDRESS_TYPE = 2500005; export const CODAMA_ERROR__DYNAMIC_CLIENT__CANNOT_CONVERT_TO_ADDRESS = 2500006; +/** @deprecated No longer thrown as of Codama v2, which removed `resolverValueNode`. */ export const CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_RESOLVER_MISSING = 2500007; export const CODAMA_ERROR__DYNAMIC_CLIENT__CIRCULAR_ACCOUNT_DEPENDENCY = 2500008; export const CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_OPTIONAL_ACCOUNT_STRATEGY = 2500009; export const CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_DERIVE_PDA = 2500010; -export const CODAMA_ERROR__DYNAMIC_CLIENT__ARGUMENT_MISSING = 2500011; +export const CODAMA_ERROR__DYNAMIC_CLIENT__DATA_MISSING = 2500011; export const CODAMA_ERROR__DYNAMIC_CLIENT__DEFAULT_VALUE_MISSING = 2500012; export const CODAMA_ERROR__DYNAMIC_CLIENT__INVALID_ARGUMENT_INPUT = 2500013; export const CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE = 2500014; export const CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_ENCODE_ARGUMENT = 2500015; +/** @deprecated No longer thrown as of Codama v2, which removed `resolverValueNode`. */ export const CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_EXECUTE_RESOLVER = 2500016; export const CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_VALIDATE_INPUT = 2500017; export const CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_NODE = 2500018; export const CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION = 2500019; +export const CODAMA_ERROR__DYNAMIC_CLIENT__PDA_SEED_MISSING = 2500020; // Renderers-related errors. // Reserve error codes in the range [2800000-2800999]. @@ -133,9 +136,9 @@ export type CodamaErrorCode = | typeof CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND | typeof CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING | typeof CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_RESOLVER_MISSING - | typeof CODAMA_ERROR__DYNAMIC_CLIENT__ARGUMENT_MISSING | typeof CODAMA_ERROR__DYNAMIC_CLIENT__CANNOT_CONVERT_TO_ADDRESS | typeof CODAMA_ERROR__DYNAMIC_CLIENT__CIRCULAR_ACCOUNT_DEPENDENCY + | typeof CODAMA_ERROR__DYNAMIC_CLIENT__DATA_MISSING | typeof CODAMA_ERROR__DYNAMIC_CLIENT__DEFAULT_VALUE_MISSING | typeof CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_DERIVE_PDA | typeof CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_ENCODE_ARGUMENT @@ -147,6 +150,7 @@ export type CodamaErrorCode = | typeof CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION | typeof CODAMA_ERROR__DYNAMIC_CLIENT__NODE_REFERENCE_NOT_FOUND | typeof CODAMA_ERROR__DYNAMIC_CLIENT__PDA_NOT_FOUND + | typeof CODAMA_ERROR__DYNAMIC_CLIENT__PDA_SEED_MISSING | typeof CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ADDRESS_TYPE | typeof CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE | typeof CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_NODE diff --git a/packages/errors/src/context.ts b/packages/errors/src/context.ts index c0207ed12..6493a18df 100644 --- a/packages/errors/src/context.ts +++ b/packages/errors/src/context.ts @@ -38,9 +38,9 @@ import { CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND, CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING, CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_RESOLVER_MISSING, - CODAMA_ERROR__DYNAMIC_CLIENT__ARGUMENT_MISSING, CODAMA_ERROR__DYNAMIC_CLIENT__CANNOT_CONVERT_TO_ADDRESS, CODAMA_ERROR__DYNAMIC_CLIENT__CIRCULAR_ACCOUNT_DEPENDENCY, + CODAMA_ERROR__DYNAMIC_CLIENT__DATA_MISSING, CODAMA_ERROR__DYNAMIC_CLIENT__DEFAULT_VALUE_MISSING, CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_DERIVE_PDA, CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_ENCODE_ARGUMENT, @@ -52,6 +52,7 @@ import { CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, CODAMA_ERROR__DYNAMIC_CLIENT__NODE_REFERENCE_NOT_FOUND, CODAMA_ERROR__DYNAMIC_CLIENT__PDA_NOT_FOUND, + CODAMA_ERROR__DYNAMIC_CLIENT__PDA_SEED_MISSING, CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ADDRESS_TYPE, CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE, CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_NODE, @@ -149,16 +150,16 @@ export type CodamaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{ accountName: IdentifierString; resolverName: IdentifierString; }; - [CODAMA_ERROR__DYNAMIC_CLIENT__ARGUMENT_MISSING]: { - argumentName: IdentifierString; - instructionName: IdentifierString; - }; [CODAMA_ERROR__DYNAMIC_CLIENT__CANNOT_CONVERT_TO_ADDRESS]: { value: string; }; [CODAMA_ERROR__DYNAMIC_CLIENT__CIRCULAR_ACCOUNT_DEPENDENCY]: { chain: string; }; + [CODAMA_ERROR__DYNAMIC_CLIENT__DATA_MISSING]: { + instructionName: IdentifierString; + path: PathString; + }; [CODAMA_ERROR__DYNAMIC_CLIENT__DEFAULT_VALUE_MISSING]: { argumentName: IdentifierString; instructionName: IdentifierString; @@ -202,6 +203,10 @@ export type CodamaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{ available: string; pdaName: string; }; + [CODAMA_ERROR__DYNAMIC_CLIENT__PDA_SEED_MISSING]: { + pdaName: IdentifierString; + seedName: IdentifierString; + }; [CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ADDRESS_TYPE]: { accountName: string; diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index e60c98713..b3abdb7fb 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -17,9 +17,9 @@ import { CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND, CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_MISSING, CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_RESOLVER_MISSING, - CODAMA_ERROR__DYNAMIC_CLIENT__ARGUMENT_MISSING, CODAMA_ERROR__DYNAMIC_CLIENT__CANNOT_CONVERT_TO_ADDRESS, CODAMA_ERROR__DYNAMIC_CLIENT__CIRCULAR_ACCOUNT_DEPENDENCY, + CODAMA_ERROR__DYNAMIC_CLIENT__DATA_MISSING, CODAMA_ERROR__DYNAMIC_CLIENT__DEFAULT_VALUE_MISSING, CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_DERIVE_PDA, CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_ENCODE_ARGUMENT, @@ -31,6 +31,7 @@ import { CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, CODAMA_ERROR__DYNAMIC_CLIENT__NODE_REFERENCE_NOT_FOUND, CODAMA_ERROR__DYNAMIC_CLIENT__PDA_NOT_FOUND, + CODAMA_ERROR__DYNAMIC_CLIENT__PDA_SEED_MISSING, CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ADDRESS_TYPE, CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ARGUMENT_TYPE, CODAMA_ERROR__DYNAMIC_CLIENT__UNSUPPORTED_NODE, @@ -96,9 +97,9 @@ export const CodamaErrorMessages: Readonly<{ 'Missing account [$accountName] in [$instructionName] instruction.', [CODAMA_ERROR__DYNAMIC_CLIENT__ACCOUNT_RESOLVER_MISSING]: 'Resolver [$resolverName] not provided for account [$accountName].', - [CODAMA_ERROR__DYNAMIC_CLIENT__ARGUMENT_MISSING]: 'Missing argument [$argumentName] in [$instructionName].', [CODAMA_ERROR__DYNAMIC_CLIENT__CANNOT_CONVERT_TO_ADDRESS]: 'Cannot convert value to Address: [$value].', [CODAMA_ERROR__DYNAMIC_CLIENT__CIRCULAR_ACCOUNT_DEPENDENCY]: 'Circular dependency detected: [$chain].', + [CODAMA_ERROR__DYNAMIC_CLIENT__DATA_MISSING]: 'Missing data [$path] in [$instructionName].', [CODAMA_ERROR__DYNAMIC_CLIENT__DEFAULT_VALUE_MISSING]: 'Default value is missing for argument [$argumentName] in [$instructionName].', [CODAMA_ERROR__DYNAMIC_CLIENT__FAILED_TO_DERIVE_PDA]: 'Failed to derive PDA for account [$accountName].', @@ -116,6 +117,7 @@ export const CodamaErrorMessages: Readonly<{ [CODAMA_ERROR__DYNAMIC_CLIENT__NODE_REFERENCE_NOT_FOUND]: 'Referenced node [$referencedName] not found in [$instructionName].', [CODAMA_ERROR__DYNAMIC_CLIENT__PDA_NOT_FOUND]: 'PDA [$pdaName] not found in IDL. Available: [$available].', + [CODAMA_ERROR__DYNAMIC_CLIENT__PDA_SEED_MISSING]: 'Missing seed [$seedName] to derive PDA [$pdaName].', [CODAMA_ERROR__DYNAMIC_CLIENT__UNEXPECTED_ADDRESS_TYPE]: 'Expected [$expectedType] for account [$accountName], but received [$actualType].',