codama-renderers-dart 0.4.3 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.browser.cjs +634 -159
  3. package/dist/index.browser.cjs.map +1 -1
  4. package/dist/index.browser.js +635 -161
  5. package/dist/index.browser.js.map +1 -1
  6. package/dist/index.node.cjs +634 -159
  7. package/dist/index.node.cjs.map +1 -1
  8. package/dist/index.node.js +635 -161
  9. package/dist/index.node.js.map +1 -1
  10. package/dist/index.react-native.js +635 -161
  11. package/dist/index.react-native.js.map +1 -1
  12. package/dist/types/fragments/accountPage.d.ts.map +1 -1
  13. package/dist/types/fragments/instructionPage.d.ts.map +1 -1
  14. package/dist/types/fragments/programPage.d.ts +1 -1
  15. package/dist/types/fragments/programPage.d.ts.map +1 -1
  16. package/dist/types/fragments/typePage.d.ts.map +1 -1
  17. package/dist/types/index.d.ts +1 -0
  18. package/dist/types/index.d.ts.map +1 -1
  19. package/dist/types/utils/discriminators.d.ts +10 -0
  20. package/dist/types/utils/discriminators.d.ts.map +1 -0
  21. package/dist/types/utils/exactDecoder.d.ts +17 -0
  22. package/dist/types/utils/exactDecoder.d.ts.map +1 -0
  23. package/dist/types/utils/formatCode.d.ts +1 -1
  24. package/dist/types/utils/formatCode.d.ts.map +1 -1
  25. package/dist/types/utils/importMap.d.ts.map +1 -1
  26. package/dist/types/utils/index.d.ts +4 -0
  27. package/dist/types/utils/index.d.ts.map +1 -1
  28. package/dist/types/utils/nameTransformers.d.ts +1 -1
  29. package/dist/types/utils/nameTransformers.d.ts.map +1 -1
  30. package/dist/types/utils/normalizeRootNode.d.ts +11 -0
  31. package/dist/types/utils/normalizeRootNode.d.ts.map +1 -0
  32. package/dist/types/utils/valueNodes.d.ts +16 -0
  33. package/dist/types/utils/valueNodes.d.ts.map +1 -0
  34. package/dist/types/visitors/getTypeManifestVisitor.d.ts +1 -1
  35. package/dist/types/visitors/getTypeManifestVisitor.d.ts.map +1 -1
  36. package/dist/types/visitors/renderVisitor.d.ts.map +1 -1
  37. package/package.json +31 -23
  38. package/readme.md +30 -3
  39. package/src/fragments/accountPage.ts +90 -17
  40. package/src/fragments/instructionPage.ts +109 -72
  41. package/src/fragments/programPage.ts +165 -6
  42. package/src/fragments/typePage.ts +36 -15
  43. package/src/index.ts +1 -0
  44. package/src/utils/discriminators.ts +121 -0
  45. package/src/utils/exactDecoder.ts +82 -0
  46. package/src/utils/formatCode.ts +7 -11
  47. package/src/utils/importMap.ts +16 -3
  48. package/src/utils/index.ts +4 -0
  49. package/src/utils/nameTransformers.ts +3 -2
  50. package/src/utils/normalizeRootNode.ts +76 -0
  51. package/src/utils/valueNodes.ts +87 -0
  52. package/src/utils/wellKnownAddresses.ts +4 -4
  53. package/src/visitors/getRenderMapVisitor.ts +1 -1
  54. package/src/visitors/getTypeManifestVisitor.ts +48 -29
  55. package/src/visitors/renderVisitor.ts +11 -4
@@ -7,16 +7,20 @@ import { visit } from "@codama/visitors-core";
7
7
 
8
8
  import type { Fragment } from "../utils/fragment.js";
9
9
  import {
10
- emptyFragment,
11
10
  fragment,
12
11
  fragmentFromString,
13
12
  mergeFragments,
14
13
  use,
15
14
  } from "../utils/fragment.js";
16
15
  import type { RenderScope } from "../utils/options.js";
17
- import { camelCase, pascalCase } from "../utils/nameTransformers.js";
16
+ import { camelCase } from "../utils/nameTransformers.js";
17
+ import { getDiscriminatorValidationFragment } from "../utils/discriminators.js";
18
+ import { getTopLevelDecoderFragment } from "../utils/exactDecoder.js";
19
+ import {
20
+ getDartValueFragment,
21
+ isConstDartValueNode,
22
+ } from "../utils/valueNodes.js";
18
23
  import { getDiscriminatorConstantsFragment } from "./discriminatorConstants.js";
19
- import { WELL_KNOWN_ADDRESSES } from "../utils/wellKnownAddresses.js";
20
24
 
21
25
  /**
22
26
  * Generate a full Dart file for an instruction.
@@ -31,19 +35,55 @@ export function getInstructionPageFragment(
31
35
  const parseFnName = scope.nameApi.instructionParseFunction(name);
32
36
 
33
37
  const accounts = node.accounts ?? [];
34
- const args = (node.arguments ?? []).filter(
35
- (arg) => !isDiscriminatorArg(arg, node),
36
- );
37
38
  const allArgs = node.arguments ?? [];
39
+ const optionalAccountStrategy: string =
40
+ node.optionalAccountStrategy ?? "programId";
41
+ if (
42
+ optionalAccountStrategy !== "programId" &&
43
+ optionalAccountStrategy !== "omitted"
44
+ ) {
45
+ throw new Error(
46
+ `Unsupported optional account strategy on instruction "${node.name}": ${optionalAccountStrategy}`,
47
+ );
48
+ }
49
+ const args = allArgs.filter(
50
+ (arg) =>
51
+ !isDiscriminatorArg(arg, node) && !isOmittedDefaultArgument(arg),
52
+ );
38
53
 
39
54
  // Build the instruction data class
40
55
  const dataClassName = `${typeName}InstructionData`;
41
56
 
42
57
  // Visit each arg type once and collect manifests for reuse
43
- const allArgManifests = allArgs.map((arg) => ({
58
+ const allArgManifests = (node.arguments ?? []).map((arg) => ({
44
59
  arg,
45
60
  manifest: visit(arg.type, scope.typeManifestVisitor),
46
61
  }));
62
+ const argDefaultValues = new Map(
63
+ allArgManifests.flatMap(({ arg, manifest }) => {
64
+ if (arg.defaultValue == null) return [];
65
+ const needsRenderedDefault =
66
+ isOmittedDefaultArgument(arg) ||
67
+ isDiscriminatorArg(arg, node) ||
68
+ arg.defaultValue.kind !== "accountBumpValueNode";
69
+ if (!needsRenderedDefault) return [];
70
+ return [
71
+ [
72
+ arg,
73
+ getDartValueFragment(arg.defaultValue, manifest.type.content),
74
+ ] as const,
75
+ ];
76
+ }),
77
+ );
78
+ for (const arg of allArgs.filter((candidate) =>
79
+ isDiscriminatorArg(candidate, node),
80
+ )) {
81
+ if (!argDefaultValues.has(arg)) {
82
+ throw new Error(
83
+ `Field discriminator "${arg.name}" on instruction "${node.name}" must reference a field with a default value.`,
84
+ );
85
+ }
86
+ }
47
87
 
48
88
  // Data fields (non-discriminator arguments)
49
89
  const dataFieldDecls = allArgManifests
@@ -56,20 +96,25 @@ export function getInstructionPageFragment(
56
96
  const dataCtorParams = allArgManifests
57
97
  .map(({ arg, manifest }) => {
58
98
  const fieldName = camelCase(arg.name as string);
99
+ if (isOmittedDefaultArgument(arg)) return "";
59
100
  if (isDiscriminatorArg(arg, node)) {
60
101
  return isConstDefaultValue(arg.defaultValue)
61
- ? ` this.${fieldName} = ${getDiscriminatorDefault(arg, node)},`
102
+ ? ` this.${fieldName} = ${argDefaultValues.get(arg)!.content},`
62
103
  : ` ${manifest.type.content}? ${fieldName},`;
63
104
  }
64
105
  return ` required this.${fieldName},`;
65
106
  })
107
+ .filter(Boolean)
66
108
  .join("\n");
67
109
 
68
110
  const dataCtorInitializers = allArgManifests
69
111
  .map(({ arg }) => {
70
112
  const fieldName = camelCase(arg.name as string);
113
+ if (isOmittedDefaultArgument(arg)) {
114
+ return ` ${fieldName} = ${argDefaultValues.get(arg)!.content}`;
115
+ }
71
116
  if (isDiscriminatorArg(arg, node) && !isConstDefaultValue(arg.defaultValue)) {
72
- return ` ${fieldName} = ${fieldName} ?? ${getDiscriminatorDefault(arg, node)}`;
117
+ return ` ${fieldName} = ${fieldName} ?? ${argDefaultValues.get(arg)!.content}`;
73
118
  }
74
119
  return null;
75
120
  })
@@ -90,13 +135,17 @@ export function getInstructionPageFragment(
90
135
  .join("\n");
91
136
 
92
137
  const toMapFields = allArgs
93
- .map(
94
- (arg) =>
95
- ` '${arg.name as string}': value.${camelCase(arg.name as string)},`,
96
- )
138
+ .map((arg) => {
139
+ const value =
140
+ isOmittedDefaultArgument(arg) || isDiscriminatorArg(arg, node)
141
+ ? argDefaultValues.get(arg)!.content
142
+ : `value.${camelCase(arg.name as string)}`;
143
+ return ` '${arg.name as string}': ${value},`;
144
+ })
97
145
  .join("\n");
98
146
 
99
147
  const fromMapFields = allArgManifests
148
+ .filter(({ arg }) => !isOmittedDefaultArgument(arg))
100
149
  .map(({ arg, manifest }) => {
101
150
  const typeStr = manifest.type.content;
102
151
  const isNullable = typeStr.endsWith("?");
@@ -105,7 +154,20 @@ export function getInstructionPageFragment(
105
154
  })
106
155
  .join("\n");
107
156
 
108
- const dataCtorKeyword = dataCtorInitializers ? "" : "const ";
157
+ const dataCtorKeyword = allArgs.every(
158
+ (arg) =>
159
+ !isOmittedDefaultArgument(arg) ||
160
+ isConstDartValueNode(arg.defaultValue!),
161
+ ) && allArgs.every(
162
+ (arg) =>
163
+ !isDiscriminatorArg(arg, node) ||
164
+ isConstDefaultValue(arg.defaultValue),
165
+ )
166
+ ? "const "
167
+ : "";
168
+ const dataCtorSignature = dataCtorParams
169
+ ? `${dataCtorKeyword}${dataClassName}({\n${dataCtorParams}\n })${dataCtorInitializers ? ` :\n${dataCtorInitializers}` : ""};`
170
+ : `${dataCtorKeyword}${dataClassName}()${dataCtorInitializers ? ` :\n${dataCtorInitializers}` : ""};`;
109
171
  const dataEncoderName = `get${typeName}InstructionDataEncoder`;
110
172
  const dataDecoderName = `get${typeName}InstructionDataDecoder`;
111
173
  const dataCodecName = `get${typeName}InstructionDataCodec`;
@@ -134,7 +196,11 @@ export function getInstructionPageFragment(
134
196
  const manifest = argManifestMap.get(arg)!;
135
197
  const fieldName = camelCase(arg.name as string);
136
198
  const typeStr = manifest.type.content;
137
- const hasDefault = arg.defaultValue != null;
199
+ // Account bumps require asynchronous PDA derivation, so callers must
200
+ // provide them instead of relying on a synchronous generated default.
201
+ const hasDefault =
202
+ arg.defaultValue != null &&
203
+ arg.defaultValue.kind !== "accountBumpValueNode";
138
204
  if (hasDefault) {
139
205
  // Don't add `?` if the type is already nullable.
140
206
  const nullableType = typeStr.endsWith("?") ? typeStr : `${typeStr}?`;
@@ -151,7 +217,10 @@ export function getInstructionPageFragment(
151
217
  const role = getAccountRole(acc);
152
218
  const isOptional = acc.isOptional ?? false;
153
219
  if (isOptional) {
154
- return ` if (${fieldName} != null) AccountMeta(address: ${fieldName}, role: ${role}),`;
220
+ if (optionalAccountStrategy === "omitted") {
221
+ return ` if (${fieldName} != null) AccountMeta(address: ${fieldName}, role: ${role}),`;
222
+ }
223
+ return ` if (${fieldName} != null) AccountMeta(address: ${fieldName}, role: ${role}) else AccountMeta(address: ${instrProgramParam}, role: AccountRole.readonly),`;
155
224
  }
156
225
  return ` AccountMeta(address: ${fieldName}, role: ${role}),`;
157
226
  })
@@ -161,17 +230,31 @@ export function getInstructionPageFragment(
161
230
  const dataConstruction = allArgs
162
231
  .map((arg) => {
163
232
  const fieldName = camelCase(arg.name as string);
164
- if (isDiscriminatorArg(arg, node)) {
233
+ if (
234
+ isOmittedDefaultArgument(arg) ||
235
+ isDiscriminatorArg(arg, node)
236
+ ) {
165
237
  return ""; // Use default
166
238
  }
167
- // Use 'arg_<name>' prefix to reference builder params without shadowing.
168
- return ` ${fieldName}: ${fieldName}${arg.defaultValue != null ? ` ?? ${getDefaultValue(arg)}` : ""},`;
239
+ const skipDefault = arg.defaultValue?.kind === "accountBumpValueNode";
240
+ return ` ${fieldName}: ${fieldName}${arg.defaultValue != null && !skipDefault ? ` ?? ${argDefaultValues.get(arg)!.content}` : ""},`;
169
241
  })
170
242
  .filter(Boolean)
171
243
  .join("\n");
172
244
 
173
245
  // Discriminator
174
246
  const discFragment = getDiscriminatorConstantsFragment(node, scope);
247
+ const discriminatorValidation = getDiscriminatorValidationFragment(
248
+ node,
249
+ scope,
250
+ );
251
+ const topLevelDecoder = getTopLevelDecoderFragment({
252
+ typeName: dataClassName,
253
+ description: `${name} instruction decoder`,
254
+ discriminatorValidation,
255
+ fromMapFields,
256
+ requireExactConsumption: true,
257
+ });
175
258
 
176
259
  const parts: Fragment[] = [
177
260
  fragment`// Auto-generated. Do not edit.
@@ -199,9 +282,7 @@ ${use("AccountRole", "solanaInstructions")}`,
199
282
  parts.push(fragment`
200
283
  @immutable
201
284
  class ${fragmentFromString(dataClassName)} {
202
- ${fragmentFromString(dataCtorKeyword)}${fragmentFromString(dataClassName)}({
203
- ${fragmentFromString(dataCtorParams)}
204
- })${fragmentFromString(dataCtorInitializers ? ` :\n${dataCtorInitializers}` : "")};
285
+ ${fragmentFromString(dataCtorSignature)}
205
286
 
206
287
  ${fragmentFromString(dataFieldDecls)}
207
288
  }`);
@@ -226,12 +307,7 @@ Decoder<${fragmentFromString(dataClassName)}> ${fragmentFromString(dataDecoderNa
226
307
  ${fragmentFromString(decFields)}
227
308
  ]);
228
309
 
229
- return transformDecoder(
230
- structDecoder,
231
- (Map<String, Object?> map, Uint8List bytes, int offset) => ${fragmentFromString(dataClassName)}(
232
- ${fragmentFromString(fromMapFields)}
233
- ),
234
- );
310
+ ${topLevelDecoder}
235
311
  }
236
312
 
237
313
  Codec<${fragmentFromString(dataClassName)}, ${fragmentFromString(dataClassName)}> ${fragmentFromString(dataCodecName)}() {
@@ -274,6 +350,9 @@ ${fragmentFromString(dataClassName)} ${fragmentFromString(parseFnName)}(Instruct
274
350
  result.imports.mergeWith(manifest.decoder.imports);
275
351
  result.imports.mergeWith(manifest.type.imports);
276
352
  }
353
+ for (const defaultValue of argDefaultValues.values()) {
354
+ result.imports.mergeWith(defaultValue.imports);
355
+ }
277
356
 
278
357
  return result;
279
358
  }
@@ -299,23 +378,6 @@ function isDiscriminatorArg(
299
378
  );
300
379
  }
301
380
 
302
- function getDiscriminatorDefault(
303
- arg: InstructionArgumentNode,
304
- _node: InstructionNode,
305
- ): string {
306
- if (arg.defaultValue) {
307
- if (arg.defaultValue.kind === "numberValueNode") {
308
- return String(arg.defaultValue.number);
309
- }
310
- if (arg.defaultValue.kind === "bytesValueNode") {
311
- const data = arg.defaultValue.data as string;
312
- const bytes = data.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) ?? [];
313
- return `Uint8List.fromList([${bytes.join(", ")}])`;
314
- }
315
- }
316
- return "0";
317
- }
318
-
319
381
  function isConstDefaultValue(defaultValue: InstructionArgumentNode["defaultValue"]): boolean {
320
382
  if (!defaultValue) return false;
321
383
  switch (defaultValue.kind) {
@@ -329,31 +391,6 @@ function isConstDefaultValue(defaultValue: InstructionArgumentNode["defaultValue
329
391
  }
330
392
  }
331
393
 
332
- function getDefaultValue(arg: InstructionArgumentNode): string {
333
- if (!arg.defaultValue) return "null";
334
- const dv = arg.defaultValue;
335
- switch (dv.kind) {
336
- case "numberValueNode":
337
- return String(dv.number);
338
- case "booleanValueNode":
339
- return String(dv.boolean);
340
- case "stringValueNode":
341
- return `'${dv.string}'`;
342
- case "publicKeyValueNode": {
343
- const wellKnownName = WELL_KNOWN_ADDRESSES.get(dv.publicKey);
344
- if (wellKnownName) {
345
- use(wellKnownName, "solanaAddresses");
346
- return wellKnownName;
347
- }
348
- return `Address('${dv.publicKey}')`;
349
- }
350
- case "noneValueNode":
351
- return "null";
352
- case "bytesValueNode": {
353
- const bytes = dv.data.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) ?? [];
354
- return `Uint8List.fromList([${bytes.join(", ")}])`;
355
- }
356
- default:
357
- return "null";
358
- }
394
+ function isOmittedDefaultArgument(arg: InstructionArgumentNode): boolean {
395
+ return arg.defaultValue != null && arg.defaultValueStrategy === "omitted";
359
396
  }
@@ -1,4 +1,10 @@
1
- import type { ProgramNode } from "@codama/nodes";
1
+ import {
2
+ getAllInstructionsWithSubs,
3
+ type InstructionNode,
4
+ type ProgramNode,
5
+ type ValueNode,
6
+ } from "@codama/nodes";
7
+ import { visit } from "@codama/visitors-core";
2
8
 
3
9
  import type { Fragment } from "../utils/fragment.js";
4
10
  import {
@@ -67,19 +73,172 @@ ${fragmentFromString(accountVariants)}
67
73
  }`);
68
74
  }
69
75
 
70
- // Instruction identifier enum
71
- const instructions = node.instructions ?? [];
76
+ // Instruction identification and parsing helpers.
77
+ const instructions = getAllInstructionsWithSubs(node);
72
78
  if (instructions.length > 0) {
79
+ const programName = pascalCase(name);
80
+ const instructionEnum = `${programName}Instruction`;
73
81
  const instrVariants = instructions
74
82
  .map((instr) => ` ${camelCase(instr.name as string)},`)
75
83
  .join("\n");
76
84
 
77
85
  parts.push(fragment`
78
- /// Known instructions for the ${fragmentFromString(pascalCase(name))} program.
79
- enum ${fragmentFromString(pascalCase(name))}Instruction {
86
+ /// Known instructions for the ${fragmentFromString(programName)} program.
87
+ enum ${fragmentFromString(instructionEnum)} {
80
88
  ${fragmentFromString(instrVariants)}
81
89
  }`);
90
+
91
+ const identifiableInstructions = instructions
92
+ .map((instruction) => ({
93
+ instruction,
94
+ condition: getInstructionDiscriminatorCondition(instruction, scope),
95
+ }))
96
+ .filter(({ condition }) => condition !== null) as {
97
+ instruction: InstructionNode;
98
+ condition: Fragment;
99
+ }[];
100
+
101
+ if (identifiableInstructions.length > 0) {
102
+ const identifyBranches = identifiableInstructions.map(
103
+ ({ instruction, condition }) => fragment` if (${condition}) {
104
+ return ${fragmentFromString(instructionEnum)}.${fragmentFromString(camelCase(instruction.name as string))};
105
+ }`,
106
+ );
107
+ const parsedBase = `Parsed${programName}Instruction`;
108
+ const parsedClasses = instructions.map((instruction) => {
109
+ const instructionName = pascalCase(instruction.name as string);
110
+ const variant = camelCase(instruction.name as string);
111
+ const parsedClass = `Parsed${instructionName}`;
112
+ const dataClass = `${instructionName}InstructionData`;
113
+ return fragment`/// A parsed ${fragmentFromString(instructionName)} instruction.
114
+ final class ${fragmentFromString(parsedClass)} extends ${fragmentFromString(parsedBase)} {
115
+ const ${fragmentFromString(parsedClass)}({required this.data})
116
+ : super(${fragmentFromString(instructionEnum)}.${fragmentFromString(variant)});
117
+
118
+ final ${use(dataClass, "../instructions/instructions.dart")} data;
119
+ }`;
120
+ });
121
+ const parseBranches = instructions.map((instruction) => {
122
+ const instructionName = pascalCase(instruction.name as string);
123
+ const variant = camelCase(instruction.name as string);
124
+ const parsedClass = `Parsed${instructionName}`;
125
+ const parseFunction = scope.nameApi.instructionParseFunction(
126
+ instruction.name as string,
127
+ );
128
+ return fragment` ${fragmentFromString(instructionEnum)}.${fragmentFromString(variant)} => ${fragmentFromString(parsedClass)}(
129
+ data: ${use(parseFunction, "../instructions/instructions.dart")}(instruction),
130
+ ),`;
131
+ });
132
+
133
+ parts.push(fragment`
134
+ /// Identifies the type of a ${fragmentFromString(programName)} instruction.
135
+ ${fragmentFromString(instructionEnum)} identify${fragmentFromString(programName)}Instruction(
136
+ ${use("Uint8List", "dartTypedData")} data,
137
+ ) {
138
+ ${mergeFragments(identifyBranches, (cs) => cs.join("\n"))}
139
+
140
+ throw ${use("SolanaError", "solanaErrors")}(
141
+ ${use("SolanaErrorCode", "solanaErrors")}.programClientsFailedToIdentifyInstruction,
142
+ {
143
+ 'instructionData': data,
144
+ 'programName': '${fragmentFromString(name)}',
145
+ },
146
+ );
147
+ }
148
+
149
+ /// A parsed instruction from the ${fragmentFromString(programName)} program.
150
+ sealed class ${fragmentFromString(parsedBase)} {
151
+ const ${fragmentFromString(parsedBase)}(this.instructionType);
152
+
153
+ final ${fragmentFromString(instructionEnum)} instructionType;
154
+ }
155
+
156
+ ${mergeFragments(parsedClasses, (cs) => cs.join("\n\n"))}
157
+
158
+ /// Parses a ${fragmentFromString(programName)} instruction.
159
+ ${fragmentFromString(parsedBase)} parse${fragmentFromString(programName)}Instruction(
160
+ ${use("Instruction", "solanaInstructions")} instruction,
161
+ ) {
162
+ return switch (identify${fragmentFromString(programName)}Instruction(
163
+ instruction.data ?? Uint8List(0),
164
+ )) {
165
+ ${mergeFragments(parseBranches, (cs) => cs.join("\n"))}
166
+ };
167
+ }`);
168
+ }
82
169
  }
83
170
 
84
171
  return mergeFragments(parts, (cs) => cs.join("\n"));
85
- }
172
+ }
173
+
174
+ function getInstructionDiscriminatorCondition(
175
+ instruction: InstructionNode,
176
+ scope: RenderScope,
177
+ ): Fragment | null {
178
+ const discriminators = instruction.discriminators ?? [];
179
+ if (discriminators.length === 0) return null;
180
+
181
+ const conditions = discriminators.map((discriminator): Fragment | null => {
182
+ switch (discriminator.kind) {
183
+ case "sizeDiscriminatorNode":
184
+ return fragment`data.length == ${discriminator.size}`;
185
+ case "constantDiscriminatorNode": {
186
+ const manifest = visit(
187
+ discriminator.constant.type,
188
+ scope.typeManifestVisitor,
189
+ );
190
+ const value = getValueFragment(
191
+ discriminator.constant.value,
192
+ manifest.type.content,
193
+ );
194
+ if (value === null) return null;
195
+ return fragment`${use("containsBytes", "solanaCodecsCore")}(data, ${manifest.encoder}.encode(${value}), ${discriminator.offset})`;
196
+ }
197
+ case "fieldDiscriminatorNode": {
198
+ const argument = (instruction.arguments ?? []).find(
199
+ (candidate) => candidate.name === discriminator.name,
200
+ );
201
+ if (argument?.defaultValue == null) return null;
202
+ const manifest = visit(argument.type, scope.typeManifestVisitor);
203
+ const value = getValueFragment(
204
+ argument.defaultValue,
205
+ manifest.type.content,
206
+ );
207
+ if (value === null) return null;
208
+ return fragment`${use("containsBytes", "solanaCodecsCore")}(data, ${manifest.encoder}.encode(${value}), ${discriminator.offset})`;
209
+ }
210
+ }
211
+ });
212
+
213
+ if (conditions.some((condition) => condition === null)) return null;
214
+ return mergeFragments(conditions as Fragment[], (cs) => cs.join(" && "));
215
+ }
216
+
217
+ function getValueFragment(
218
+ value:
219
+ | ValueNode
220
+ | NonNullable<
221
+ NonNullable<InstructionNode["arguments"]>[number]["defaultValue"]
222
+ >,
223
+ dartType?: string,
224
+ ): Fragment | null {
225
+ switch (value.kind) {
226
+ case "numberValueNode":
227
+ return fragmentFromString(
228
+ dartType === "BigInt"
229
+ ? `BigInt.from(${value.number})`
230
+ : String(value.number),
231
+ );
232
+ case "booleanValueNode":
233
+ return fragmentFromString(String(value.boolean));
234
+ case "stringValueNode":
235
+ return fragmentFromString(`'${value.string.replaceAll("'", "\\'")}'`);
236
+ case "bytesValueNode": {
237
+ const clean = value.data.replace(/^0x/, "");
238
+ const bytes = clean.match(/.{1,2}/g)?.map((byte) => parseInt(byte, 16)) ?? [];
239
+ return fragment`${use("Uint8List", "dartTypedData")}.fromList([${fragmentFromString(bytes.join(", "))}])`;
240
+ }
241
+ default:
242
+ return null;
243
+ }
244
+ }
@@ -1,4 +1,9 @@
1
- import type { DefinedTypeNode, StructFieldTypeNode, TypeNode } from "@codama/nodes";
1
+ import type {
2
+ DefinedTypeNode,
3
+ EnumVariantTypeNode,
4
+ StructFieldTypeNode,
5
+ TypeNode,
6
+ } from "@codama/nodes";
2
7
  import { resolveNestedTypeNode } from "@codama/nodes";
3
8
  import { visit } from "@codama/visitors-core";
4
9
 
@@ -30,13 +35,14 @@ export function getTypePageFragment(
30
35
 
31
36
  // Check what kind of type this is
32
37
  if (typeNode.kind === "enumTypeNode") {
33
- const isScalar = typeNode.variants.every(
38
+ const variants = typeNode.variants ?? [];
39
+ const isScalar = variants.every(
34
40
  (v) => v.kind === "enumEmptyVariantTypeNode",
35
41
  );
36
42
  if (isScalar) {
37
- return getScalarEnumPageFragment(node, scope);
43
+ return getScalarEnumPageFragment(node, variants, scope);
38
44
  }
39
- return getDataEnumPageFragment(node, scope);
45
+ return getDataEnumPageFragment(node, variants, scope);
40
46
  }
41
47
 
42
48
  if (typeNode.kind === "structTypeNode") {
@@ -52,6 +58,7 @@ export function getTypePageFragment(
52
58
  */
53
59
  function getScalarEnumPageFragment(
54
60
  node: DefinedTypeNode,
61
+ enumVariants: EnumVariantTypeNode[],
55
62
  scope: RenderScope,
56
63
  ): Fragment {
57
64
  const name = node.name as string;
@@ -59,7 +66,7 @@ function getScalarEnumPageFragment(
59
66
  const enumNode = node.type;
60
67
  if (enumNode.kind !== "enumTypeNode") return emptyFragment();
61
68
 
62
- const variants = enumNode.variants.map((v) => {
69
+ const variants = enumVariants.map((v) => {
63
70
  const variantName = scope.nameApi.enumVariant(v.name as string);
64
71
  return variantName;
65
72
  });
@@ -73,6 +80,18 @@ function getScalarEnumPageFragment(
73
80
  const resolvedSize = resolveNestedTypeNode(enumNode.size);
74
81
  const sizeType = resolvedSize.format ?? "u8";
75
82
  const sizeCodecName = getNumberCodecName(sizeType);
83
+ const isU64 = sizeType === "u64";
84
+ const encodedVariantIndex = isU64
85
+ ? "BigInt.from(value.index)"
86
+ : "value.index";
87
+ const decodedVariant = isU64
88
+ ? `(BigInt value, Uint8List bytes, int offset) {
89
+ if (value.isNegative || value >= BigInt.from(${typeName}.values.length)) {
90
+ throw RangeError('Invalid ${typeName} discriminator: ' + value.toString());
91
+ }
92
+ return ${typeName}.values[value.toInt()];
93
+ }`
94
+ : `(int value, Uint8List bytes, int offset) => ${typeName}.values[value]`;
76
95
 
77
96
  return fragment`// Auto-generated. Do not edit.
78
97
  // ignore_for_file: type=lint
@@ -94,14 +113,14 @@ ${fragmentFromString(variantLines)}
94
113
  Encoder<${fragmentFromString(typeName)}> ${fragmentFromString(encoderName)}() {
95
114
  return transformEncoder(
96
115
  get${fragmentFromString(sizeCodecName)}Encoder(),
97
- (${fragmentFromString(typeName)} value) => value.index,
116
+ (${fragmentFromString(typeName)} value) => ${fragmentFromString(encodedVariantIndex)},
98
117
  );
99
118
  }
100
119
 
101
120
  Decoder<${fragmentFromString(typeName)}> ${fragmentFromString(decoderName)}() {
102
121
  return transformDecoder(
103
122
  get${fragmentFromString(sizeCodecName)}Decoder(),
104
- (int value, Uint8List bytes, int offset) => ${fragmentFromString(typeName)}.values[value],
123
+ ${fragmentFromString(decodedVariant)},
105
124
  );
106
125
  }
107
126
 
@@ -115,6 +134,7 @@ Codec<${fragmentFromString(typeName)}, ${fragmentFromString(typeName)}> ${fragme
115
134
  */
116
135
  function getDataEnumPageFragment(
117
136
  node: DefinedTypeNode,
137
+ variants: EnumVariantTypeNode[],
118
138
  scope: RenderScope,
119
139
  ): Fragment {
120
140
  const name = node.name as string;
@@ -130,8 +150,9 @@ function getDataEnumPageFragment(
130
150
  // Collect all field type manifests from variants so we can merge their imports
131
151
  const allVariantManifests: { encoder: Fragment; decoder: Fragment; type: Fragment }[] = [];
132
152
 
133
- for (let i = 0; i < enumNode.variants.length; i++) {
134
- const variant = enumNode.variants[i];
153
+ for (let i = 0; i < variants.length; i++) {
154
+ const variant = variants[i];
155
+ const variantName = pascalCase(variant.name as string);
135
156
  const variantClassName = scope.nameApi.sealedClassVariant(
136
157
  name,
137
158
  variant.name as string,
@@ -149,7 +170,7 @@ function getDataEnumPageFragment(
149
170
  int get hashCode => runtimeType.hashCode;
150
171
 
151
172
  @override
152
- String toString() => '${typeName}.${variantClassName}()';
173
+ String toString() => '${typeName}.${variantName}()';
153
174
  }`);
154
175
 
155
176
  encoderVariants.push(
@@ -162,7 +183,7 @@ function getDataEnumPageFragment(
162
183
  decodeCases.push(`case ${i}: return const ${variantClassName}();`);
163
184
  } else if (variant.kind === "enumStructVariantTypeNode") {
164
185
  const resolvedStruct = resolveNestedTypeNode(variant.struct);
165
- const fields = resolvedStruct.fields;
186
+ const fields = resolvedStruct.fields ?? [];
166
187
 
167
188
  // Visit each field type once and collect manifests
168
189
  const fieldManifests = fields.map((f: StructFieldTypeNode) => ({
@@ -217,7 +238,7 @@ ${fieldDecls}
217
238
  int get hashCode => ${hashExpression};
218
239
 
219
240
  @override
220
- String toString() => '${typeName}.${variantClassName}(${toStringFields})';
241
+ String toString() => '${typeName}.${variantName}(${toStringFields})';
221
242
  }`);
222
243
 
223
244
  const encFields = fieldManifests
@@ -270,7 +291,7 @@ ${fieldDecls}
270
291
  decodeCases.push(`case ${i}: return ${variantClassName}(${fromMapFields.replace(/\n/g, ' ').replace(/,$/, '')});`);
271
292
  } else if (variant.kind === "enumTupleVariantTypeNode") {
272
293
  const resolvedTuple = resolveNestedTypeNode(variant.tuple);
273
- const items = resolvedTuple.items;
294
+ const items = resolvedTuple.items ?? [];
274
295
  if (items.length === 1) {
275
296
  const manifest = visit(items[0], scope.typeManifestVisitor);
276
297
  allVariantManifests.push(manifest);
@@ -288,7 +309,7 @@ ${fieldDecls}
288
309
  int get hashCode => value.hashCode;
289
310
 
290
311
  @override
291
- String toString() => '${typeName}.${variantClassName}(\$value)';
312
+ String toString() => '${typeName}.${variantName}(\$value)';
292
313
  }`);
293
314
  encoderVariants.push(
294
315
  `(${i}, transformEncoder<${manifest.type.content}, Map<String, Object?>>(${manifest.encoder.content}, (Map<String, Object?> map) => map['value']! as ${manifest.type.content}))`,
@@ -383,7 +404,7 @@ function getStructPageFragment(
383
404
  const structNode = node.type;
384
405
  if (structNode.kind !== "structTypeNode") return emptyFragment();
385
406
 
386
- const fields = structNode.fields;
407
+ const fields = structNode.fields ?? [];
387
408
 
388
409
  // Visit each field type once and collect manifests for reuse
389
410
  const fieldManifests = fields.map((f: StructFieldTypeNode) => ({
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export { getRenderMapVisitor } from "./visitors/getRenderMapVisitor.js";
3
3
  export { getTypeManifestVisitor } from "./visitors/getTypeManifestVisitor.js";
4
4
  export { DartImportMap, DART_EXTERNAL_PACKAGE_MAP } from "./utils/importMap.js";
5
5
  export { createDartNameApi } from "./utils/nameTransformers.js";
6
+ export { normalizeRootNode } from "./utils/normalizeRootNode.js";
6
7
  export type { DartNameApi } from "./utils/nameTransformers.js";
7
8
  export type { RenderOptions, GetRenderMapOptions, RenderScope } from "./utils/options.js";
8
9
  export type { TypeManifest } from "./utils/typeManifest.js";