codama-renderers-dart 0.5.0 → 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 (45) hide show
  1. package/dist/index.browser.cjs +456 -127
  2. package/dist/index.browser.cjs.map +1 -1
  3. package/dist/index.browser.js +456 -128
  4. package/dist/index.browser.js.map +1 -1
  5. package/dist/index.node.cjs +456 -127
  6. package/dist/index.node.cjs.map +1 -1
  7. package/dist/index.node.js +456 -128
  8. package/dist/index.node.js.map +1 -1
  9. package/dist/index.react-native.js +456 -128
  10. package/dist/index.react-native.js.map +1 -1
  11. package/dist/types/fragments/accountPage.d.ts.map +1 -1
  12. package/dist/types/fragments/instructionPage.d.ts.map +1 -1
  13. package/dist/types/fragments/typePage.d.ts.map +1 -1
  14. package/dist/types/index.d.ts +1 -0
  15. package/dist/types/index.d.ts.map +1 -1
  16. package/dist/types/utils/discriminators.d.ts +10 -0
  17. package/dist/types/utils/discriminators.d.ts.map +1 -0
  18. package/dist/types/utils/exactDecoder.d.ts +17 -0
  19. package/dist/types/utils/exactDecoder.d.ts.map +1 -0
  20. package/dist/types/utils/formatCode.d.ts +1 -1
  21. package/dist/types/utils/formatCode.d.ts.map +1 -1
  22. package/dist/types/utils/index.d.ts +4 -0
  23. package/dist/types/utils/index.d.ts.map +1 -1
  24. package/dist/types/utils/normalizeRootNode.d.ts +11 -0
  25. package/dist/types/utils/normalizeRootNode.d.ts.map +1 -0
  26. package/dist/types/utils/valueNodes.d.ts +16 -0
  27. package/dist/types/utils/valueNodes.d.ts.map +1 -0
  28. package/dist/types/visitors/getTypeManifestVisitor.d.ts +1 -1
  29. package/dist/types/visitors/getTypeManifestVisitor.d.ts.map +1 -1
  30. package/dist/types/visitors/renderVisitor.d.ts.map +1 -1
  31. package/package.json +21 -18
  32. package/readme.md +29 -0
  33. package/src/fragments/accountPage.ts +90 -17
  34. package/src/fragments/instructionPage.ts +103 -77
  35. package/src/fragments/typePage.ts +32 -12
  36. package/src/index.ts +1 -0
  37. package/src/utils/discriminators.ts +121 -0
  38. package/src/utils/exactDecoder.ts +82 -0
  39. package/src/utils/formatCode.ts +7 -11
  40. package/src/utils/index.ts +4 -0
  41. package/src/utils/normalizeRootNode.ts +76 -0
  42. package/src/utils/valueNodes.ts +87 -0
  43. package/src/visitors/getRenderMapVisitor.ts +1 -1
  44. package/src/visitors/getTypeManifestVisitor.ts +14 -8
  45. package/src/visitors/renderVisitor.ts +4 -1
@@ -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`;
@@ -155,7 +217,10 @@ export function getInstructionPageFragment(
155
217
  const role = getAccountRole(acc);
156
218
  const isOptional = acc.isOptional ?? false;
157
219
  if (isOptional) {
158
- 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),`;
159
224
  }
160
225
  return ` AccountMeta(address: ${fieldName}, role: ${role}),`;
161
226
  })
@@ -165,19 +230,31 @@ export function getInstructionPageFragment(
165
230
  const dataConstruction = allArgs
166
231
  .map((arg) => {
167
232
  const fieldName = camelCase(arg.name as string);
168
- if (isDiscriminatorArg(arg, node)) {
233
+ if (
234
+ isOmittedDefaultArgument(arg) ||
235
+ isDiscriminatorArg(arg, node)
236
+ ) {
169
237
  return ""; // Use default
170
238
  }
171
- // Use 'arg_<name>' prefix to reference builder params without shadowing.
172
- const typeStr2 = argManifestMap.get(arg)?.type.content;
173
239
  const skipDefault = arg.defaultValue?.kind === "accountBumpValueNode";
174
- return ` ${fieldName}: ${fieldName}${arg.defaultValue != null && !skipDefault ? ` ?? ${getDefaultValue(arg, typeStr2)}` : ""},`;
240
+ return ` ${fieldName}: ${fieldName}${arg.defaultValue != null && !skipDefault ? ` ?? ${argDefaultValues.get(arg)!.content}` : ""},`;
175
241
  })
176
242
  .filter(Boolean)
177
243
  .join("\n");
178
244
 
179
245
  // Discriminator
180
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
+ });
181
258
 
182
259
  const parts: Fragment[] = [
183
260
  fragment`// Auto-generated. Do not edit.
@@ -205,9 +282,7 @@ ${use("AccountRole", "solanaInstructions")}`,
205
282
  parts.push(fragment`
206
283
  @immutable
207
284
  class ${fragmentFromString(dataClassName)} {
208
- ${fragmentFromString(dataCtorKeyword)}${fragmentFromString(dataClassName)}({
209
- ${fragmentFromString(dataCtorParams)}
210
- })${fragmentFromString(dataCtorInitializers ? ` :\n${dataCtorInitializers}` : "")};
285
+ ${fragmentFromString(dataCtorSignature)}
211
286
 
212
287
  ${fragmentFromString(dataFieldDecls)}
213
288
  }`);
@@ -232,12 +307,7 @@ Decoder<${fragmentFromString(dataClassName)}> ${fragmentFromString(dataDecoderNa
232
307
  ${fragmentFromString(decFields)}
233
308
  ]);
234
309
 
235
- return transformDecoder(
236
- structDecoder,
237
- (Map<String, Object?> map, Uint8List bytes, int offset) => ${fragmentFromString(dataClassName)}(
238
- ${fragmentFromString(fromMapFields)}
239
- ),
240
- );
310
+ ${topLevelDecoder}
241
311
  }
242
312
 
243
313
  Codec<${fragmentFromString(dataClassName)}, ${fragmentFromString(dataClassName)}> ${fragmentFromString(dataCodecName)}() {
@@ -280,6 +350,9 @@ ${fragmentFromString(dataClassName)} ${fragmentFromString(parseFnName)}(Instruct
280
350
  result.imports.mergeWith(manifest.decoder.imports);
281
351
  result.imports.mergeWith(manifest.type.imports);
282
352
  }
353
+ for (const defaultValue of argDefaultValues.values()) {
354
+ result.imports.mergeWith(defaultValue.imports);
355
+ }
283
356
 
284
357
  return result;
285
358
  }
@@ -305,23 +378,6 @@ function isDiscriminatorArg(
305
378
  );
306
379
  }
307
380
 
308
- function getDiscriminatorDefault(
309
- arg: InstructionArgumentNode,
310
- _node: InstructionNode,
311
- ): string {
312
- if (arg.defaultValue) {
313
- if (arg.defaultValue.kind === "numberValueNode") {
314
- return String(arg.defaultValue.number);
315
- }
316
- if (arg.defaultValue.kind === "bytesValueNode") {
317
- const data = arg.defaultValue.data as string;
318
- const bytes = data.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) ?? [];
319
- return `Uint8List.fromList([${bytes.join(", ")}])`;
320
- }
321
- }
322
- return "0";
323
- }
324
-
325
381
  function isConstDefaultValue(defaultValue: InstructionArgumentNode["defaultValue"]): boolean {
326
382
  if (!defaultValue) return false;
327
383
  switch (defaultValue.kind) {
@@ -335,36 +391,6 @@ function isConstDefaultValue(defaultValue: InstructionArgumentNode["defaultValue
335
391
  }
336
392
  }
337
393
 
338
- function getDefaultValue(
339
- arg: InstructionArgumentNode,
340
- typeStr?: string,
341
- ): string {
342
- if (!arg.defaultValue) return "null";
343
- const isBigInt = typeStr === "BigInt";
344
- const dv = arg.defaultValue;
345
- switch (dv.kind) {
346
- case "numberValueNode":
347
- // Dart's `BigInt` requires a constructor; `0` is not assignable to `BigInt`.
348
- return isBigInt ? `BigInt.from(${dv.number})` : String(dv.number);
349
- case "booleanValueNode":
350
- return String(dv.boolean);
351
- case "stringValueNode":
352
- return `'${dv.string}'`;
353
- case "publicKeyValueNode": {
354
- const wellKnownName = WELL_KNOWN_ADDRESSES.get(dv.publicKey);
355
- if (wellKnownName) {
356
- use(wellKnownName, "solanaAddresses");
357
- return wellKnownName;
358
- }
359
- return `Address('${dv.publicKey}')`;
360
- }
361
- case "noneValueNode":
362
- return "null";
363
- case "bytesValueNode": {
364
- const bytes = dv.data.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) ?? [];
365
- return `Uint8List.fromList([${bytes.join(", ")}])`;
366
- }
367
- default:
368
- return "null";
369
- }
394
+ function isOmittedDefaultArgument(arg: InstructionArgumentNode): boolean {
395
+ return arg.defaultValue != null && arg.defaultValueStrategy === "omitted";
370
396
  }
@@ -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,8 @@ 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];
135
155
  const variantName = pascalCase(variant.name as string);
136
156
  const variantClassName = scope.nameApi.sealedClassVariant(
137
157
  name,
@@ -163,7 +183,7 @@ function getDataEnumPageFragment(
163
183
  decodeCases.push(`case ${i}: return const ${variantClassName}();`);
164
184
  } else if (variant.kind === "enumStructVariantTypeNode") {
165
185
  const resolvedStruct = resolveNestedTypeNode(variant.struct);
166
- const fields = resolvedStruct.fields;
186
+ const fields = resolvedStruct.fields ?? [];
167
187
 
168
188
  // Visit each field type once and collect manifests
169
189
  const fieldManifests = fields.map((f: StructFieldTypeNode) => ({
@@ -271,7 +291,7 @@ ${fieldDecls}
271
291
  decodeCases.push(`case ${i}: return ${variantClassName}(${fromMapFields.replace(/\n/g, ' ').replace(/,$/, '')});`);
272
292
  } else if (variant.kind === "enumTupleVariantTypeNode") {
273
293
  const resolvedTuple = resolveNestedTypeNode(variant.tuple);
274
- const items = resolvedTuple.items;
294
+ const items = resolvedTuple.items ?? [];
275
295
  if (items.length === 1) {
276
296
  const manifest = visit(items[0], scope.typeManifestVisitor);
277
297
  allVariantManifests.push(manifest);
@@ -384,7 +404,7 @@ function getStructPageFragment(
384
404
  const structNode = node.type;
385
405
  if (structNode.kind !== "structTypeNode") return emptyFragment();
386
406
 
387
- const fields = structNode.fields;
407
+ const fields = structNode.fields ?? [];
388
408
 
389
409
  // Visit each field type once and collect manifests for reuse
390
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";
@@ -0,0 +1,121 @@
1
+ import type {
2
+ AccountNode,
3
+ ConstantValueNode,
4
+ InstructionNode,
5
+ StructFieldTypeNode,
6
+ } from "@codama/nodes";
7
+ import { resolveNestedTypeNode } from "@codama/nodes";
8
+ import { visit } from "@codama/visitors-core";
9
+
10
+ import type { Fragment } from "./fragment.js";
11
+ import {
12
+ emptyFragment,
13
+ fragment,
14
+ fragmentFromString,
15
+ mergeFragments,
16
+ use,
17
+ } from "./fragment.js";
18
+ import type { RenderScope } from "./options.js";
19
+ import { getDartValueFragment } from "./valueNodes.js";
20
+
21
+ type DiscriminatedNode = AccountNode | InstructionNode;
22
+
23
+ /**
24
+ * Generate decoder guards for every discriminator declared by a Codama node.
25
+ */
26
+ export function getDiscriminatorValidationFragment(
27
+ node: DiscriminatedNode,
28
+ scope: RenderScope,
29
+ ): Fragment {
30
+ const discriminators = node.discriminators ?? [];
31
+ if (discriminators.length === 0) return emptyFragment();
32
+
33
+ const fields = getDiscriminatorFields(node);
34
+ const fragments = discriminators.map((discriminator): Fragment => {
35
+ switch (discriminator.kind) {
36
+ case "constantDiscriminatorNode":
37
+ return getConstantValidationFragment(
38
+ discriminator.constant,
39
+ discriminator.offset,
40
+ scope,
41
+ );
42
+ case "fieldDiscriminatorNode": {
43
+ const field = fields.find(
44
+ (candidate) => candidate.name === discriminator.name,
45
+ );
46
+ if (field?.defaultValue == null) {
47
+ throw new Error(
48
+ `Field discriminator "${discriminator.name}" on ${node.kind} "${node.name}" must reference a field with a default value.`,
49
+ );
50
+ }
51
+ return getConstantValidationFragment(
52
+ {
53
+ kind: "constantValueNode",
54
+ type: field.type,
55
+ value: field.defaultValue,
56
+ } as ConstantValueNode,
57
+ discriminator.offset,
58
+ scope,
59
+ );
60
+ }
61
+ case "sizeDiscriminatorNode": {
62
+ if (
63
+ !Number.isSafeInteger(discriminator.size) ||
64
+ discriminator.size < 0
65
+ ) {
66
+ throw new Error(
67
+ `Discriminator size must be a non-negative safe integer, got ${discriminator.size}.`,
68
+ );
69
+ }
70
+ return fragment`if (bytes.length - offset != ${fragmentFromString(String(discriminator.size))}) {
71
+ throw ${use("SolanaError", "solanaErrors")}(
72
+ ${use("SolanaErrorCode", "solanaErrors")}.codecsInvalidByteLength,
73
+ {
74
+ 'codecDescription': '${fragmentFromString(node.name as string)} discriminator',
75
+ 'expected': ${fragmentFromString(String(discriminator.size))},
76
+ 'bytesLength': bytes.length - offset,
77
+ },
78
+ );
79
+ }`;
80
+ }
81
+ }
82
+ });
83
+
84
+ return mergeFragments(fragments, (contents) => contents.join("\n"));
85
+ }
86
+
87
+ function getConstantValidationFragment(
88
+ constant: ConstantValueNode,
89
+ discriminatorOffset: number,
90
+ scope: RenderScope,
91
+ ): Fragment {
92
+ if (!Number.isSafeInteger(discriminatorOffset) || discriminatorOffset < 0) {
93
+ throw new Error(
94
+ `Discriminator offset must be a non-negative safe integer, got ${discriminatorOffset}.`,
95
+ );
96
+ }
97
+
98
+ const manifest = visit(constant.type, scope.typeManifestVisitor);
99
+ const value = getDartValueFragment(
100
+ constant.value,
101
+ manifest.type.content,
102
+ );
103
+ return fragment`${use("getConstantDecoder", "solanaCodecsDataStructures")}(
104
+ ${manifest.encoder}.encode(${value}),
105
+ ).read(bytes, offset + ${fragmentFromString(String(discriminatorOffset))});`;
106
+ }
107
+
108
+ function getDiscriminatorFields(
109
+ node: DiscriminatedNode,
110
+ ): readonly StructFieldTypeNode[] {
111
+ if (node.kind === "accountNode") {
112
+ return resolveNestedTypeNode(node.data).fields ?? [];
113
+ }
114
+ return (node.arguments ?? []).map(
115
+ (argument) =>
116
+ ({
117
+ ...argument,
118
+ kind: "structFieldTypeNode",
119
+ }) as StructFieldTypeNode,
120
+ );
121
+ }
@@ -0,0 +1,82 @@
1
+ import type { Fragment } from "./fragment.js";
2
+ import { fragment, fragmentFromString, use } from "./fragment.js";
3
+
4
+ export interface TopLevelDecoderFragmentOptions {
5
+ readonly typeName: string;
6
+ readonly description: string;
7
+ readonly discriminatorValidation: Fragment;
8
+ readonly fromMapFields: string;
9
+ readonly requireExactConsumption: boolean;
10
+ }
11
+
12
+ /**
13
+ * Wrap a generated top-level struct decoder with discriminator and length
14
+ * validation.
15
+ *
16
+ * The wrapper preserves fixed/variable decoder metadata so it remains
17
+ * compatible with the matching generated encoder when combined into a codec.
18
+ */
19
+ export function getTopLevelDecoderFragment({
20
+ typeName,
21
+ description,
22
+ discriminatorValidation,
23
+ fromMapFields,
24
+ requireExactConsumption,
25
+ }: TopLevelDecoderFragmentOptions): Fragment {
26
+ const validation = discriminatorValidation.content
27
+ ? ` ${discriminatorValidation.content.replaceAll("\n", "\n ")}\n`
28
+ : "";
29
+
30
+ const consumptionCheck = requireExactConsumption
31
+ ? ` if (newOffset != bytes.length) {
32
+ throwInvalidByteLength(newOffset - offset, bytes.length - offset);
33
+ }
34
+ `
35
+ : "";
36
+ const fixedSizeCheck = requireExactConsumption
37
+ ? "bytesLength != structDecoder.fixedSize"
38
+ : "bytesLength < structDecoder.fixedSize";
39
+
40
+ const result = fragment` Never throwInvalidByteLength(int expected, int bytesLength) {
41
+ throw ${use("SolanaError", "solanaErrors")}(
42
+ ${use("SolanaErrorCode", "solanaErrors")}.codecsInvalidByteLength,
43
+ {
44
+ 'codecDescription': '${fragmentFromString(description)}',
45
+ 'expected': expected,
46
+ 'bytesLength': bytesLength,
47
+ },
48
+ );
49
+ }
50
+
51
+ (${fragmentFromString(typeName)}, int) readTopLevel(Uint8List bytes, int offset) {
52
+ ${fragmentFromString(validation)} final (map, newOffset) = structDecoder.read(bytes, offset);
53
+ ${fragmentFromString(consumptionCheck)}
54
+ return (
55
+ ${fragmentFromString(typeName)}(
56
+ ${fragmentFromString(fromMapFields)}
57
+ ),
58
+ newOffset,
59
+ );
60
+ }
61
+
62
+ return switch (structDecoder) {
63
+ ${use("FixedSizeDecoder", "solanaCodecsCore")}<Map<String, Object?>>() =>
64
+ ${use("FixedSizeDecoder", "solanaCodecsCore")}<${fragmentFromString(typeName)}>(
65
+ fixedSize: structDecoder.fixedSize,
66
+ read: (bytes, offset) {
67
+ final bytesLength = bytes.length - offset;
68
+ if (${fragmentFromString(fixedSizeCheck)}) {
69
+ throwInvalidByteLength(structDecoder.fixedSize, bytesLength);
70
+ }
71
+ return readTopLevel(bytes, offset);
72
+ },
73
+ ),
74
+ ${use("VariableSizeDecoder", "solanaCodecsCore")}<Map<String, Object?>>() =>
75
+ ${use("VariableSizeDecoder", "solanaCodecsCore")}<${fragmentFromString(typeName)}>(
76
+ read: readTopLevel,
77
+ maxSize: structDecoder.maxSize,
78
+ ),
79
+ };`;
80
+ result.imports.mergeWith(discriminatorValidation.imports);
81
+ return result;
82
+ }
@@ -2,11 +2,11 @@ import { execSync } from "node:child_process";
2
2
 
3
3
  /**
4
4
  * Format Dart code using `dart format`.
5
- * Falls back to returning the unformatted code if dart is not available.
5
+ * Falls back to returning the unformatted code if Dart is not available.
6
6
  */
7
7
  export function formatDartCode(code: string): string {
8
8
  try {
9
- const result = execSync("dart format --fix -", {
9
+ const result = execSync("dart format --output=show", {
10
10
  input: code,
11
11
  encoding: "utf-8",
12
12
  timeout: 30_000,
@@ -23,13 +23,9 @@ export function formatDartCode(code: string): string {
23
23
  * Format all Dart files in a directory using `dart format`.
24
24
  */
25
25
  export function formatDartDirectory(dir: string): void {
26
- try {
27
- execSync(`dart format --fix "${dir}"`, {
28
- encoding: "utf-8",
29
- timeout: 60_000,
30
- stdio: ["pipe", "pipe", "pipe"],
31
- });
32
- } catch {
33
- // Silently ignore if dart format is not available
34
- }
26
+ execSync(`dart format "${dir}"`, {
27
+ encoding: "utf-8",
28
+ timeout: 60_000,
29
+ stdio: ["pipe", "pipe", "pipe"],
30
+ });
35
31
  }
@@ -1,8 +1,12 @@
1
1
  export * from "./codecs.js";
2
+ export * from "./discriminators.js";
3
+ export * from "./exactDecoder.js";
2
4
  export * from "./formatCode.js";
3
5
  export * from "./fragment.js";
4
6
  export * from "./importMap.js";
5
7
  export * from "./nameTransformers.js";
8
+ export * from "./normalizeRootNode.js";
6
9
  export * from "./options.js";
7
10
  export * from "./typeManifest.js";
11
+ export * from "./valueNodes.js";
8
12
  export * from "./wellKnownAddresses.js";