Skip to content

Commit f0cba6f

Browse files
committed
Adapt update visitors to Codama v2
1 parent ae160c2 commit f0cba6f

26 files changed

Lines changed: 3354 additions & 852 deletions

‎packages/cli/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ In the example below, we rename the `vault` account to `safe` and update the `au
242242
"args": [
243243
{
244244
"vault": {
245-
"name": "safe"
245+
"identifier": "safe"
246246
},
247247
"bank": {
248248
"data": { "authority": "treasury" }

‎packages/errors/src/codes.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ export const CODAMA_ERROR__VISITORS__RENDER_MAP_KEY_NOT_FOUND = 1200011;
5757
export const CODAMA_ERROR__VISITORS__CANNOT_REMOVE_LAST_PATH_IN_NODE_STACK = 1200012;
5858
export const CODAMA_ERROR__VISITORS__INVALID_PROVIDED_VALUE = 1200013;
5959
export const CODAMA_ERROR__VISITORS__CANNOT_FLATTEN_STRUCT_WITH_PLUGINS = 1200014;
60+
export const CODAMA_ERROR__VISITORS__UNRECOGNIZED_UPDATE_KEYS = 1200015;
61+
export const CODAMA_ERROR__VISITORS__INSTRUCTION_DATA_FIELD_NOT_FOUND = 1200016;
62+
export const CODAMA_ERROR__VISITORS__INSTRUCTION_ACCOUNT_NOT_FOUND = 1200017;
63+
export const CODAMA_ERROR__VISITORS__DEFINED_TYPE_MEMBER_NOT_FOUND = 1200018;
6064

6165
// Anchor-related errors.
6266
// Reserve error codes in the range [2100000-2100999].
@@ -164,10 +168,14 @@ export type CodamaErrorCode =
164168
| typeof CODAMA_ERROR__VISITORS__CANNOT_REMOVE_LAST_PATH_IN_NODE_STACK
165169
| typeof CODAMA_ERROR__VISITORS__CANNOT_USE_OPTIONAL_ACCOUNT_AS_PDA_SEED_VALUE
166170
| typeof CODAMA_ERROR__VISITORS__CYCLIC_DEPENDENCY_DETECTED_WHEN_RESOLVING_INSTRUCTION_DEFAULT_VALUES
171+
| typeof CODAMA_ERROR__VISITORS__DEFINED_TYPE_MEMBER_NOT_FOUND
167172
| typeof CODAMA_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE
173+
| typeof CODAMA_ERROR__VISITORS__INSTRUCTION_ACCOUNT_NOT_FOUND
174+
| typeof CODAMA_ERROR__VISITORS__INSTRUCTION_DATA_FIELD_NOT_FOUND
168175
| typeof CODAMA_ERROR__VISITORS__INSTRUCTION_ENUM_DATA_FIELD_NOT_FOUND
169176
| typeof CODAMA_ERROR__VISITORS__INVALID_INSTRUCTION_DEFAULT_VALUE_DEPENDENCY
170177
| typeof CODAMA_ERROR__VISITORS__INVALID_NUMBER_WRAPPER
171178
| typeof CODAMA_ERROR__VISITORS__INVALID_PDA_SEED_VALUES
172179
| typeof CODAMA_ERROR__VISITORS__INVALID_PROVIDED_VALUE
173-
| typeof CODAMA_ERROR__VISITORS__RENDER_MAP_KEY_NOT_FOUND;
180+
| typeof CODAMA_ERROR__VISITORS__RENDER_MAP_KEY_NOT_FOUND
181+
| typeof CODAMA_ERROR__VISITORS__UNRECOGNIZED_UPDATE_KEYS;

‎packages/errors/src/context.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
AccountNode,
88
AccountValueNode,
99
DataValueNode,
10+
DefinedTypeNode,
1011
EnumTypeNode,
1112
IdentifierString,
1213
InstructionAccountNode,
@@ -74,13 +75,17 @@ import {
7475
CODAMA_ERROR__VISITORS__CANNOT_REMOVE_LAST_PATH_IN_NODE_STACK,
7576
CODAMA_ERROR__VISITORS__CANNOT_USE_OPTIONAL_ACCOUNT_AS_PDA_SEED_VALUE,
7677
CODAMA_ERROR__VISITORS__CYCLIC_DEPENDENCY_DETECTED_WHEN_RESOLVING_INSTRUCTION_DEFAULT_VALUES,
78+
CODAMA_ERROR__VISITORS__DEFINED_TYPE_MEMBER_NOT_FOUND,
7779
CODAMA_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE,
80+
CODAMA_ERROR__VISITORS__INSTRUCTION_ACCOUNT_NOT_FOUND,
81+
CODAMA_ERROR__VISITORS__INSTRUCTION_DATA_FIELD_NOT_FOUND,
7882
CODAMA_ERROR__VISITORS__INSTRUCTION_ENUM_DATA_FIELD_NOT_FOUND,
7983
CODAMA_ERROR__VISITORS__INVALID_INSTRUCTION_DEFAULT_VALUE_DEPENDENCY,
8084
CODAMA_ERROR__VISITORS__INVALID_NUMBER_WRAPPER,
8185
CODAMA_ERROR__VISITORS__INVALID_PDA_SEED_VALUES,
8286
CODAMA_ERROR__VISITORS__INVALID_PROVIDED_VALUE,
8387
CODAMA_ERROR__VISITORS__RENDER_MAP_KEY_NOT_FOUND,
88+
CODAMA_ERROR__VISITORS__UNRECOGNIZED_UPDATE_KEYS,
8489
CodamaErrorCode,
8590
} from './codes';
8691

@@ -294,10 +299,25 @@ export type CodamaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
294299
instruction: InstructionNode;
295300
instructionName: IdentifierString;
296301
};
302+
[CODAMA_ERROR__VISITORS__DEFINED_TYPE_MEMBER_NOT_FOUND]: {
303+
definedType: DefinedTypeNode;
304+
missingMember: string;
305+
name: IdentifierString;
306+
};
297307
[CODAMA_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE]: {
298308
formattedHistogram: string;
299309
validationItems: readonly ValidationItem[];
300310
};
311+
[CODAMA_ERROR__VISITORS__INSTRUCTION_ACCOUNT_NOT_FOUND]: {
312+
accountName: string;
313+
instruction: InstructionNode;
314+
instructionName: IdentifierString;
315+
};
316+
[CODAMA_ERROR__VISITORS__INSTRUCTION_DATA_FIELD_NOT_FOUND]: {
317+
instruction: InstructionNode;
318+
instructionName: IdentifierString;
319+
path: string;
320+
};
301321
[CODAMA_ERROR__VISITORS__INSTRUCTION_ENUM_DATA_FIELD_NOT_FOUND]: {
302322
fieldName: IdentifierString;
303323
instruction: InstructionNode;
@@ -331,6 +351,11 @@ export type CodamaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
331351
[CODAMA_ERROR__VISITORS__RENDER_MAP_KEY_NOT_FOUND]: {
332352
key: string;
333353
};
354+
[CODAMA_ERROR__VISITORS__UNRECOGNIZED_UPDATE_KEYS]: {
355+
allowedKeys: string[];
356+
selector: string;
357+
unrecognizedKeys: string[];
358+
};
334359
}>;
335360

336361
type ValidationItem = {

‎packages/errors/src/messages.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,17 @@ import {
5555
CODAMA_ERROR__VISITORS__CANNOT_REMOVE_LAST_PATH_IN_NODE_STACK,
5656
CODAMA_ERROR__VISITORS__CANNOT_USE_OPTIONAL_ACCOUNT_AS_PDA_SEED_VALUE,
5757
CODAMA_ERROR__VISITORS__CYCLIC_DEPENDENCY_DETECTED_WHEN_RESOLVING_INSTRUCTION_DEFAULT_VALUES,
58+
CODAMA_ERROR__VISITORS__DEFINED_TYPE_MEMBER_NOT_FOUND,
5859
CODAMA_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE,
60+
CODAMA_ERROR__VISITORS__INSTRUCTION_ACCOUNT_NOT_FOUND,
61+
CODAMA_ERROR__VISITORS__INSTRUCTION_DATA_FIELD_NOT_FOUND,
5962
CODAMA_ERROR__VISITORS__INSTRUCTION_ENUM_DATA_FIELD_NOT_FOUND,
6063
CODAMA_ERROR__VISITORS__INVALID_INSTRUCTION_DEFAULT_VALUE_DEPENDENCY,
6164
CODAMA_ERROR__VISITORS__INVALID_NUMBER_WRAPPER,
6265
CODAMA_ERROR__VISITORS__INVALID_PDA_SEED_VALUES,
6366
CODAMA_ERROR__VISITORS__INVALID_PROVIDED_VALUE,
6467
CODAMA_ERROR__VISITORS__RENDER_MAP_KEY_NOT_FOUND,
68+
CODAMA_ERROR__VISITORS__UNRECOGNIZED_UPDATE_KEYS,
6569
CodamaErrorCode,
6670
} from './codes';
6771

@@ -147,7 +151,13 @@ export const CodamaErrorMessages: Readonly<{
147151
'Cannot use optional account [$seedValueName] as the [$seedName] PDA seed for the [$instructionAccountName] account of the [$instructionName] instruction.',
148152
[CODAMA_ERROR__VISITORS__CYCLIC_DEPENDENCY_DETECTED_WHEN_RESOLVING_INSTRUCTION_DEFAULT_VALUES]:
149153
'Circular dependency detected when resolving the default values of the [$instructionName] instruction. Got the following dependency cycle [$formattedCycle].',
154+
[CODAMA_ERROR__VISITORS__DEFINED_TYPE_MEMBER_NOT_FOUND]:
155+
'Defined type [$name] does not have a field or variant named [$missingMember].',
150156
[CODAMA_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE]: 'Failed to validate the given node [$formattedHistogram].',
157+
[CODAMA_ERROR__VISITORS__INSTRUCTION_ACCOUNT_NOT_FOUND]:
158+
'Cannot find account [$accountName] in instruction [$instructionName].',
159+
[CODAMA_ERROR__VISITORS__INSTRUCTION_DATA_FIELD_NOT_FOUND]:
160+
'Cannot find the data field [$path] of instruction [$instructionName]. Note that fields behind a defined type link cannot be updated directly: unwrap the defined type first.',
151161
[CODAMA_ERROR__VISITORS__INSTRUCTION_ENUM_DATA_FIELD_NOT_FOUND]:
152162
'Could not find an enum data field named [$fieldName] for instruction [$instructionName].',
153163
[CODAMA_ERROR__VISITORS__INVALID_INSTRUCTION_DEFAULT_VALUE_DEPENDENCY]:
@@ -158,4 +168,6 @@ export const CodamaErrorMessages: Readonly<{
158168
[CODAMA_ERROR__VISITORS__INVALID_PROVIDED_VALUE]:
159169
'The value provided for [$key] is a [$providedKind], expected one of [$expectedKinds].',
160170
[CODAMA_ERROR__VISITORS__RENDER_MAP_KEY_NOT_FOUND]: 'Cannot find key [$key] in RenderMap.',
171+
[CODAMA_ERROR__VISITORS__UNRECOGNIZED_UPDATE_KEYS]:
172+
'Unrecognized update keys [$unrecognizedKeys] for [$selector]. Expected any of [$allowedKeys].',
161173
};

‎packages/visitors-core/README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,9 @@ const accountPath: NodePath<AccountNode> | undefined = linkables.getPath([...som
750750

751751
// Get the path of a linkable node using the full path of a link node, or throw an error if it is not found.
752752
const accountPath: NodePath<AccountNode> = linkables.getPathOrThrow([...somePath, accountLinkNode]);
753+
754+
// List the paths of every recorded linkable node of a given kind, across all programs.
755+
const accountPaths: NodePath<AccountNode>[] = linkables.getRecordedPathsOfKind('accountNode');
753756
```
754757

755758
Note that:

‎packages/visitors-core/src/LinkableDictionary.ts‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CODAMA_ERROR__LINKED_NODE_NOT_FOUND, CodamaError } from '@codama/errors';
1+
import { CODAMA_ERROR__LINKED_NODE_NOT_FOUND, CODAMA_ERROR__UNRECOGNIZED_NODE_KIND, CodamaError } from '@codama/errors';
22
import {
33
AccountNode,
44
DefinedTypeNode,
@@ -134,6 +134,50 @@ export class LinkableDictionary {
134134
return path ? getLastNodeFromPath(path) : undefined;
135135
}
136136

137+
/**
138+
* List the paths of every recorded linkable node of the given kind, in
139+
* recording order, across all programs.
140+
*
141+
* Only nodes recorded beforehand (via `recordPath` or the
142+
* `getRecordLinkablesVisitor`) are returned: the tree itself is not
143+
* traversed.
144+
*
145+
* @example
146+
* ```ts
147+
* linkables.getRecordedPathsOfKind('accountNode'); // [[root, programA, accountA], [root, programB, accountB]]
148+
* ```
149+
*/
150+
getRecordedPathsOfKind<TKind extends LinkableNode['kind']>(
151+
kind: TKind,
152+
): NodePath<Extract<LinkableNode, { kind: TKind }>>[] {
153+
type Paths = NodePath<Extract<LinkableNode, { kind: TKind }>>[];
154+
const programs = [...this.programs.values()];
155+
const linkableKind: LinkableNode['kind'] = kind;
156+
switch (linkableKind) {
157+
case 'programNode':
158+
return programs.map(program => program.program) as Paths;
159+
case 'accountNode':
160+
return programs.flatMap(program => [...program.accounts.values()]) as Paths;
161+
case 'definedTypeNode':
162+
return programs.flatMap(program => [...program.definedTypes.values()]) as Paths;
163+
case 'pdaNode':
164+
return programs.flatMap(program => [...program.pdas.values()]) as Paths;
165+
case 'instructionNode':
166+
return programs.flatMap(program =>
167+
[...program.instructions.values()].map(instruction => instruction.instruction),
168+
) as Paths;
169+
case 'instructionAccountNode':
170+
return programs.flatMap(program =>
171+
[...program.instructions.values()].flatMap(instruction => [...instruction.accounts.values()]),
172+
) as Paths;
173+
default: {
174+
// Fails to type-check if a linkable kind is not handled above.
175+
const unhandledKind: never = linkableKind;
176+
throw new CodamaError(CODAMA_ERROR__UNRECOGNIZED_NODE_KIND, { kind: unhandledKind });
177+
}
178+
}
179+
}
180+
137181
has(linkPath: NodePath<LinkNode>): boolean {
138182
const linkNode = getLastNodeFromPath(linkPath);
139183
const programDictionary = this.getProgramDictionary(linkPath);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { DefinedTypeNode, InstructionNode, isNode, PathString, StructFieldTypeNode, TypeNode } from '@codama/nodes';
2+
3+
import { LinkableDictionary } from './LinkableDictionary';
4+
import { getLastNodeFromPath, NodePath } from './NodePath';
5+
import { NodeStack } from './NodeStack';
6+
7+
/** A struct field within an instruction's data, with its full path. */
8+
export type InstructionDataField = {
9+
field: StructFieldTypeNode;
10+
/** The field's full path within `instructionNode.data`, e.g. `config.bump`. */
11+
path: PathString;
12+
};
13+
14+
/**
15+
* List every struct field addressable within an instruction's `data`, with
16+
* its full path, following `definedTypeLinkNode`s.
17+
*
18+
* Fields are only addressable where the data type resolves to a struct, so
19+
* nested fields are listed for struct-typed (or struct-linked) fields only.
20+
* Each defined type is followed at most once, which guards against cycles.
21+
*
22+
* @param instructionPath - The path to the instruction. It must contain the
23+
* instruction's `programNode` for linked data to be followed; otherwise a
24+
* linked type contributes no fields.
25+
* @param linkables - Used to follow `definedTypeLinkNode`s.
26+
*
27+
* @example
28+
* ```ts
29+
* // data: struct { amount: u64, config: struct { fee: u16 } }
30+
* getInstructionDataFields(instructionPath, linkables).map(({ path }) => path);
31+
* // ['amount', 'config', 'config.fee']
32+
* ```
33+
*/
34+
export function getInstructionDataFields(
35+
instructionPath: NodePath<InstructionNode>,
36+
linkables: LinkableDictionary,
37+
): InstructionDataField[] {
38+
const fields: InstructionDataField[] = [];
39+
const stack = new NodeStack(instructionPath);
40+
// Keyed by node rather than identifier: same-named types of different programs are distinct.
41+
const walkedDefinedTypes = new Set<DefinedTypeNode>();
42+
43+
const walk = (type: TypeNode | undefined, prefix: string): void => {
44+
if (!type) return;
45+
if (isNode(type, 'definedTypeLinkNode')) {
46+
const linkedPath = linkables.getPath([...stack.getPath(), type]);
47+
if (!linkedPath) return;
48+
const definedType = getLastNodeFromPath(linkedPath);
49+
if (walkedDefinedTypes.has(definedType)) return;
50+
walkedDefinedTypes.add(definedType);
51+
stack.pushPath(linkedPath);
52+
walk(definedType.type, prefix);
53+
stack.popPath();
54+
return;
55+
}
56+
if (!isNode(type, 'structTypeNode')) return;
57+
(type.fields ?? []).forEach(field => {
58+
const path = (prefix ? `${prefix}.${field.identifier}` : field.identifier) as PathString;
59+
fields.push({ field, path });
60+
if (isNode(field.type, ['structTypeNode', 'definedTypeLinkNode'])) {
61+
walk(field.type, path);
62+
}
63+
});
64+
};
65+
66+
walk(getLastNodeFromPath(instructionPath).data, '');
67+
return fields;
68+
}

‎packages/visitors-core/src/getResolvedInstructionInputsVisitor.ts‎

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import {
2121
VALUE_NODES,
2222
} from '@codama/nodes';
2323

24+
import { getInstructionDataFields } from './getInstructionDataFields';
2425
import { LinkableDictionary } from './LinkableDictionary';
25-
import { getLastNodeFromPath } from './NodePath';
2626
import { NodeStack } from './NodeStack';
2727
import { pipe } from './pipe';
2828
import { ProvidedScope } from './ProvidedScope';
@@ -99,40 +99,6 @@ export function getResolvedInstructionInputsVisitor(
9999
return scope.resolve(defaultValue, { kinds: INSTRUCTION_INPUT_VALUE_NODES });
100100
}
101101

102-
// Walk `instructionNode.data`, following defined-type links, and yield
103-
// every struct field with its full path. Fields are only addressable
104-
// where the data type resolves to a struct.
105-
function collectDataFields(instruction: InstructionNode): DataInput[] {
106-
const fields: DataInput[] = [];
107-
const walkedDefinedTypes = new Set<string>();
108-
109-
const walk = (type: InstructionNode['data'], prefix: string): void => {
110-
if (!type) return;
111-
if (isNode(type, 'definedTypeLinkNode')) {
112-
const linkedPath = linkables.getPath([...stack.getPath(), type]);
113-
if (!linkedPath) return;
114-
const definedType = getLastNodeFromPath(linkedPath);
115-
if (walkedDefinedTypes.has(definedType.identifier)) return;
116-
walkedDefinedTypes.add(definedType.identifier);
117-
stack.pushPath(linkedPath);
118-
walk(definedType.type, prefix);
119-
stack.popPath();
120-
return;
121-
}
122-
if (!isNode(type, 'structTypeNode')) return;
123-
(type.fields ?? []).forEach(field => {
124-
const path = (prefix ? `${prefix}.${field.identifier}` : field.identifier) as PathString;
125-
fields.push({ key: path, kind: 'data', node: field });
126-
if (isNode(field.type, 'structTypeNode') || isNode(field.type, 'definedTypeLinkNode')) {
127-
walk(field.type, path);
128-
}
129-
});
130-
};
131-
132-
walk(instruction.data, '');
133-
return fields;
134-
}
135-
136102
function resolveInstructionInput(instruction: InstructionNode, input: InstructionInput): void {
137103
// Ensure we don't visit the same input twice.
138104
if (input.kind === 'account' ? visitedAccounts.has(input.key) : visitedData.has(input.key)) {
@@ -303,7 +269,11 @@ export function getResolvedInstructionInputsVisitor(
303269

304270
// The data fields and their resolved defaults are fixed for the
305271
// duration of the visit, so compute them once.
306-
dataFields = collectDataFields(node);
272+
// The stack already contains the visited instruction since this visitor is
273+
// wrapped in `recordNodeStackVisitor` (see the `pipe` below).
274+
dataFields = getInstructionDataFields(stack.getPath('instructionNode'), linkables).map(
275+
({ field, path }): DataInput => ({ key: path, kind: 'data', node: field }),
276+
);
307277
dataDefaults = new Map(
308278
dataFields.map(field => [field.key, resolveDefaultValue(asInstructionInputValue(field.node.defaultValue))]),
309279
);

‎packages/visitors-core/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from './extendVisitor';
77
export * from './generated';
88
export * from './getByteSizeVisitor';
99
export * from './getDebugStringVisitor';
10+
export * from './getInstructionDataFields';
1011
export * from './getMaxByteSizeVisitor';
1112
export * from './getResolvedInstructionInputsVisitor';
1213
export * from './getUniqueHashStringVisitor';

0 commit comments

Comments
 (0)