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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion packages/dynamic-codecs/src/values.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND, CodamaError } from '@codama/errors';
import {
CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION,
CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND,
CodamaError,
} from '@codama/errors';
import { assertIsNode, bytesTypeNode, isNode, isScalarEnum, pascalCase, ValueNode } from '@codama/nodes';
import { LinkableDictionary, NodeStack, pipe, recordNodeStackVisitor, visit, Visitor } from '@codama/visitors-core';

Expand Down Expand Up @@ -60,6 +64,16 @@ export function getValueNodeVisitor(
}
return kind;
},
visitInjectedValue(node) {
// `injectedValueNode` is a placeholder resolved by the provide/inject graph
// at instruction-build / presentation time. By codec time the resolution
// pass should have replaced it with a concrete value; reaching this layer
// unresolved indicates an upstream pipeline bug, not a value this visitor
// can decode without ancestor context.
throw new CodamaError(CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, {
message: `injectedValueNode (key "${node.key}") reached codec evaluation; the provide/inject resolution pass must run first.`,
});
},
visitMapValue(node) {
return Object.fromEntries(
node.entries.map(entry => {
Expand Down
22 changes: 22 additions & 0 deletions packages/dynamic-codecs/test/values/InjectedValueNode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION, isCodamaError } from '@codama/errors';
import { injectedValueNode } from '@codama/nodes';
import { LinkableDictionary, visit } from '@codama/visitors-core';
import { expect, test } from 'vitest';

import { getValueNodeVisitor } from '../../src';

test('it throws when an injectedValueNode reaches codec evaluation unresolved', () => {
// Given an injectedValueNode that the provide/inject pass never replaced with a concrete value.
const node = injectedValueNode({ key: 'decimals' });

// When we visit it with the value-node codec visitor.
const visitInjectedValue = () => visit(node, getValueNodeVisitor(new LinkableDictionary()));

// Then it throws a pipeline invariant-violation error naming the unresolved node.
expect(visitInjectedValue).toThrow(/injectedValueNode \(key "decimals"\) reached codec evaluation/);
try {
visitInjectedValue();
} catch (error) {
expect(isCodamaError(error, CODAMA_ERROR__DYNAMIC_CLIENT__INVARIANT_VIOLATION)).toBe(true);
}
});
11 changes: 11 additions & 0 deletions packages/node-types/src/generated/InstructionAccountNode.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type { CamelCaseString } from '../brands';
import type { Docs } from '../Docs';
import type { InstructionInputValueNode } from './contextualValueNodes/InstructionInputValueNode';
import type { InstructionAccountDisplayNode } from './displayNodes/InstructionAccountDisplayNode';
import type { AccountLinkNode } from './linkNodes/AccountLinkNode';

/** An account participating in an instruction, with its name, signing/writability flags, and an optional default value. */
export interface InstructionAccountNode<
TDefaultValue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined,
TAccountLink extends AccountLinkNode | undefined = AccountLinkNode | undefined,
TDisplay extends InstructionAccountDisplayNode | undefined = InstructionAccountDisplayNode | undefined,
> {
readonly kind: 'instructionAccountNode';

Expand All @@ -26,4 +30,11 @@ export interface InstructionAccountNode<
// Children.
/** A default value used to fill the slot when the caller does not provide one. */
readonly defaultValue?: TDefaultValue;
/**
* A reference to the account's data layout. Required for consumers (e.g. `accountFieldValueNode`) to read fields from the account.
* The link's optional `program` allows cross-program references via the root's `additionalPrograms`.
*/
readonly accountLink?: TAccountLink;
/** Display metadata describing how the account is presented. */
readonly display?: TDisplay;
}
4 changes: 4 additions & 0 deletions packages/node-types/src/generated/InstructionArgumentNode.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { CamelCaseString } from '../brands';
import type { Docs } from '../Docs';
import type { InstructionInputValueNode } from './contextualValueNodes/InstructionInputValueNode';
import type { StructFieldDisplayNode } from './displayNodes/StructFieldDisplayNode';
import type { DefaultValueStrategy } from './shared/defaultValueStrategy';
import type { TypeNode } from './typeNodes/TypeNode';

/** A named argument of an instruction, with its type and an optional default value. */
export interface InstructionArgumentNode<
TDefaultValue extends InstructionInputValueNode | undefined = InstructionInputValueNode | undefined,
TType extends TypeNode = TypeNode,
TDisplay extends StructFieldDisplayNode | undefined = StructFieldDisplayNode | undefined,
> {
readonly kind: 'instructionArgumentNode';

Expand All @@ -24,4 +26,6 @@ export interface InstructionArgumentNode<
readonly type: TType;
/** A default value used when the argument is omitted by callers. */
readonly defaultValue?: TDefaultValue;
/** Display metadata describing how the argument is presented. */
readonly display?: TDisplay;
}
11 changes: 11 additions & 0 deletions packages/node-types/src/generated/InstructionNode.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { CamelCaseString } from '../brands';
import type { Docs } from '../Docs';
import type { DiscriminatorNode } from './discriminatorNodes/DiscriminatorNode';
import type { InstructionDisplayNode } from './displayNodes/InstructionDisplayNode';
import type { InstructionAccountNode } from './InstructionAccountNode';
import type { InstructionArgumentNode } from './InstructionArgumentNode';
import type { InstructionByteDeltaNode } from './InstructionByteDeltaNode';
import type { InstructionRemainingAccountsNode } from './InstructionRemainingAccountsNode';
import type { InstructionStatusNode } from './InstructionStatusNode';
import type { ProvidedNode } from './ProvidedNode';
import type { OptionalAccountStrategy } from './shared/optionalAccountStrategy';

type SelfInstructionNode = InstructionNode;
Expand All @@ -22,6 +24,8 @@ export interface InstructionNode<
TDiscriminators extends Array<DiscriminatorNode> | undefined = Array<DiscriminatorNode> | undefined,
TSubInstructions extends Array<SelfInstructionNode> | undefined = Array<SelfInstructionNode> | undefined,
TStatus extends InstructionStatusNode | undefined = InstructionStatusNode | undefined,
TProvides extends Array<ProvidedNode> | undefined = Array<ProvidedNode> | undefined,
TDisplay extends InstructionDisplayNode | undefined = InstructionDisplayNode | undefined,
> {
readonly kind: 'instructionNode';

Expand Down Expand Up @@ -53,4 +57,11 @@ export interface InstructionNode<
readonly status?: TStatus;
/** Inner instructions invoked through CPI as part of executing this instruction. */
readonly subInstructions?: TSubInstructions;
/**
* Named nodes exposed to consumers in the surrounding scope.
* Each entry pairs with an `injectedValueNode` that references it by key, so reusable types can pull contextual values without naming siblings directly.
*/
readonly provides?: TProvides;
/** Display metadata describing how the instruction is presented. */
readonly display?: TDisplay;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { Docs } from '../Docs';
import type { InstructionAccountDisplayNode } from './displayNodes/InstructionAccountDisplayNode';
import type { InstructionRemainingAccountsValue } from './InstructionRemainingAccountsValue';

/** A "remaining accounts" slot in an instruction — a variable-length tail of accounts derived from a value. */
export interface InstructionRemainingAccountsNode<
TValue extends InstructionRemainingAccountsValue = InstructionRemainingAccountsValue,
TDisplay extends InstructionAccountDisplayNode | undefined = InstructionAccountDisplayNode | undefined,
> {
readonly kind: 'instructionRemainingAccountsNode';

Expand All @@ -23,4 +25,6 @@ export interface InstructionRemainingAccountsNode<
// Children.
/** The source of the remaining-accounts list — a referenced argument or a resolver. */
readonly value: TValue;
/** Display metadata describing how the remaining-accounts group is presented as a whole. */
readonly display?: TDisplay;
}
4 changes: 4 additions & 0 deletions packages/node-types/src/generated/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { RegisteredContextualValueNode } from './contextualValueNodes/Regis
import type { RegisteredCountNode } from './countNodes/RegisteredCountNode';
import type { DefinedTypeNode } from './DefinedTypeNode';
import type { RegisteredDiscriminatorNode } from './discriminatorNodes/RegisteredDiscriminatorNode';
import type { RegisteredDisplayNode } from './displayNodes/RegisteredDisplayNode';
import type { ErrorNode } from './ErrorNode';
import type { EventNode } from './EventNode';
import type { InstructionAccountNode } from './InstructionAccountNode';
Expand All @@ -16,6 +17,7 @@ import type { RegisteredLinkNode } from './linkNodes/RegisteredLinkNode';
import type { PdaNode } from './PdaNode';
import type { RegisteredPdaSeedNode } from './pdaSeedNodes/RegisteredPdaSeedNode';
import type { ProgramNode } from './ProgramNode';
import type { ProvidedNode } from './ProvidedNode';
import type { RootNode } from './RootNode';
import type { RegisteredTypeNode } from './typeNodes/RegisteredTypeNode';
import type { RegisteredValueNode } from './valueNodes/RegisteredValueNode';
Expand All @@ -36,9 +38,11 @@ export type Node =
| InstructionStatusNode
| PdaNode
| ProgramNode
| ProvidedNode
| RegisteredContextualValueNode
| RegisteredCountNode
| RegisteredDiscriminatorNode
| RegisteredDisplayNode
| RegisteredLinkNode
| RegisteredPdaSeedNode
| RegisteredTypeNode
Expand Down
18 changes: 18 additions & 0 deletions packages/node-types/src/generated/ProvidedNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { CamelCaseString } from '../brands';
import type { Node } from './Node';

/**
* Exposes a node under a name so consumers in the surrounding scope can resolve it by that key.
* Sits inside a host's `provides` list and pairs with `injectedValueNode` on the consumer side: an injection with the matching key resolves to this entry's `node`.
*/
export interface ProvidedNode<TNode extends Node = Node> {
readonly kind: 'providedNode';

// Data.
/** The key under which the node is exposed to consumers. */
readonly name: CamelCaseString;

// Children.
/** The exposed node. The provider is a transparent pipe — any node may be supplied; the family check happens at the injection point against the consumer's expected family. */
readonly node: TNode;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { CamelCaseString } from '../../brands';

/**
* Refers to a field of a named account's decoded data.
* The referenced account must carry an `accountLink` so the account's layout is known.
* Resolving the value requires reading the account state at presentation time.
*/
export interface AccountFieldValueNode {
readonly kind: 'accountFieldValueNode';

// Data.
/** The name of the referenced account in the surrounding instruction. */
readonly account: CamelCaseString;
/**
* The name of the field within the account's decoded data.
* When absent, the value is the whole decoded account data.
*/
readonly path?: CamelCaseString;
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AccountBumpValueNode } from './AccountBumpValueNode';
import type { AccountFieldValueNode } from './AccountFieldValueNode';
import type { AccountValueNode } from './AccountValueNode';
import type { ArgumentValueNode } from './ArgumentValueNode';
import type { ConditionalValueNode } from './ConditionalValueNode';
Expand All @@ -11,6 +12,7 @@ import type { ResolverValueNode } from './ResolverValueNode';
/** Every contextual-value node usable as a top-level value. */
export type StandaloneContextualValueNode =
| AccountBumpValueNode
| AccountFieldValueNode
| AccountValueNode
| ArgumentValueNode
| ConditionalValueNode
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './AccountBumpValueNode';
export * from './AccountFieldValueNode';
export * from './AccountValueNode';
export * from './ArgumentValueNode';
export * from './ConditionalValueCondition';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { InjectableNumberValueNode } from '../valueNodes/InjectableNumberValueNode';
import type { InjectableStringValueNode } from '../valueNodes/InjectableStringValueNode';

/**
* Display metadata that presents a number as a scaled amount with an optional unit.
* The value is divided by `10 ^ decimals` and rendered alongside `unit` (e.g. `"USDC"`, `"%"`, `"bps"`).
*/
export interface AmountNumberDisplayNode<
TDecimals extends InjectableNumberValueNode | undefined = InjectableNumberValueNode | undefined,
TUnit extends InjectableStringValueNode | undefined = InjectableStringValueNode | undefined,
> {
readonly kind: 'amountNumberDisplayNode';

// Children.
/**
* How many decimal places scale the underlying integer. Resolved as a number value: either a literal `numberValueNode` or a key resolved from a surrounding provider.
* A value of `1000000` with `decimals` resolving to `6` renders as `1`.
* When this input cannot resolve, renderers should fall back to presenting the raw value rather than guess the scale.
*/
readonly decimals?: TDecimals;
/**
* A label appended after the scaled value (e.g. `"USDC"`, `"%"`, `"bps"`). Resolved as a string value: either a literal `stringValueNode` or a key resolved from a surrounding provider.
* When this input cannot resolve, renderers should present the scaled value without a unit.
*/
readonly unit?: TUnit;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Display metadata that presents a number as a point in time.
* The underlying value counts ticks since the Unix epoch; `ticksPerSecond` is the divisor that converts those ticks back to seconds.
*/
export interface DateTimeNumberDisplayNode {
readonly kind: 'dateTimeNumberDisplayNode';

// Data.
/**
* How many ticks make one second. Defaults to `1` (the value is already in seconds).
* Common choices are `1000` (milliseconds), `1000000` (microseconds), and `1000000000` (nanoseconds).
*/
readonly ticksPerSecond?: number;
}
4 changes: 4 additions & 0 deletions packages/node-types/src/generated/displayNodes/DisplayNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { RegisteredDisplayNode } from './RegisteredDisplayNode';

/** The composable form: any registered display node. */
export type DisplayNode = RegisteredDisplayNode;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Display metadata that presents a number as an elapsed duration.
* The underlying value counts ticks; `ticksPerSecond` is the divisor that converts those ticks back to seconds.
* Renderers typically format the result as `HH:mm:ss` or a coarser human-readable form.
*/
export interface DurationNumberDisplayNode {
readonly kind: 'durationNumberDisplayNode';

// Data.
/**
* How many ticks make one second. Defaults to `1` (the value is already in seconds).
* Common choices are `1000` (milliseconds), `1000000` (microseconds), and `1000000000` (nanoseconds).
*/
readonly ticksPerSecond?: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/** Display metadata for an enum variant: its label and whether to hide its inner payload. */
export interface EnumVariantDisplayNode {
readonly kind: 'enumVariantDisplayNode';

// Data.
/**
* An override label shown for the variant (e.g. `"Buy"`).
* When absent, renderers derive a label from the variant `name`.
*/
readonly label?: string;
/**
* When `true`, the variant's payload is hidden — only the label is rendered.
* Useful for tuple payloads that have no per-field handle, or when the payload is purely structural.
*/
readonly skipInnerData?: boolean;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { DisplaySkip } from '../shared/displaySkip';

/** Display metadata for an instruction account: its label in the fallback list and whether it is shown. */
export interface InstructionAccountDisplayNode {
readonly kind: 'instructionAccountDisplayNode';

// Data.
/**
* An override label shown in the fallback list (e.g. `"To"`).
* When absent, renderers derive a label from the account `name`.
*/
readonly label?: string;
/** Whether the account is shown in the fallback list. Defaults to `"never"` (always shown). */
readonly skip?: DisplaySkip;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Display metadata for an instruction: a short intent label and an interpolated sentence template.
* Either form may be absent; presentation strategy is left to the renderer.
*/
export interface InstructionDisplayNode {
readonly kind: 'instructionDisplayNode';

// Data.
/** A short imperative label describing what the instruction does (e.g. `"Transfer"`). */
readonly intent?: string;
/**
* A sentence template that composes the instruction into prose with `${root.path}` placeholders.
* Roots are `data.` (an instruction argument) and `accounts.` (an instruction account); the path is flat after the root (e.g. `${data.amount}`, `${accounts.destination}`).
* A placeholder renders through its referent's own presentation; the `skip` rule governs the fallback list only and never the sentence.
*/
readonly interpolatedIntent?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { AmountNumberDisplayNode } from './AmountNumberDisplayNode';
import type { DateTimeNumberDisplayNode } from './DateTimeNumberDisplayNode';
import type { DurationNumberDisplayNode } from './DurationNumberDisplayNode';

/** The presentation forms a number may take. Raw rendering is expressed by the absence of a display attribute. */
export type NumberDisplayNode = AmountNumberDisplayNode | DateTimeNumberDisplayNode | DurationNumberDisplayNode;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { AmountNumberDisplayNode } from './AmountNumberDisplayNode';
import type { DateTimeNumberDisplayNode } from './DateTimeNumberDisplayNode';
import type { DurationNumberDisplayNode } from './DurationNumberDisplayNode';
import type { EnumVariantDisplayNode } from './EnumVariantDisplayNode';
import type { InstructionAccountDisplayNode } from './InstructionAccountDisplayNode';
import type { InstructionDisplayNode } from './InstructionDisplayNode';
import type { StringDisplayNode } from './StringDisplayNode';
import type { StructFieldDisplayNode } from './StructFieldDisplayNode';

/** Every node tagged as display metadata. */
export type RegisteredDisplayNode =
| AmountNumberDisplayNode
| DateTimeNumberDisplayNode
| DurationNumberDisplayNode
| EnumVariantDisplayNode
| InstructionAccountDisplayNode
| InstructionDisplayNode
| StringDisplayNode
| StructFieldDisplayNode;
Loading
Loading