codama-renderers-dart 0.3.0 → 0.4.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codama-renderers-dart",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Codama renderer for generating Dart code targeting the solana_kit SDK",
5
5
  "exports": {
6
6
  "node": {
@@ -30,6 +30,11 @@
30
30
  "code-generator"
31
31
  ],
32
32
  "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/openbudgetfun/solana_kit.git",
36
+ "directory": "packages/codama-renderers-dart"
37
+ },
33
38
  "dependencies": {
34
39
  "@codama/errors": "^1.4.4",
35
40
  "@codama/nodes": "^1.4.4",
@@ -38,7 +43,8 @@
38
43
  },
39
44
  "devDependencies": {
40
45
  "@codama/nodes-from-anchor": "^1.3.9",
41
- "@codama/renderers-js": "^2.0.2"
46
+ "@codama/renderers-js": "^2.0.2",
47
+ "@vitest/coverage-v8": "3.2.4"
42
48
  },
43
49
  "engines": {
44
50
  "node": ">=18"
@@ -50,6 +56,7 @@
50
56
  "build": "pnpm exec rimraf dist && pnpm exec tsup && pnpm exec tsc -p tsconfig.declarations.json",
51
57
  "lint": "eslint --ext .ts src",
52
58
  "test": "pnpm exec vitest run",
59
+ "test:coverage": "pnpm exec vitest run --coverage.enabled --coverage.reporter=lcov",
53
60
  "test:watch": "pnpm exec vitest",
54
61
  "typecheck": "pnpm exec tsc --noEmit"
55
62
  }
package/readme.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # codama-renderers-dart
2
2
 
3
+ [![pub package](https://img.shields.io/pub/v/codama-renderers-dart.svg)](https://pub.dev/packages/codama-renderers-dart)
4
+ [![CI](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml)
5
+ [![Coverage](https://codecov.io/gh/openbudgetfun/solana_kit/branch/main/graph/badge.svg?flag=codama-renderers-dart)](https://codecov.io/gh/openbudgetfun/solana_kit?flag=codama-renderers-dart)
6
+
3
7
  A [Codama](https://github.com/codama-idl/codama) renderer that generates Dart code targeting the [solana_kit](https://github.com/openbudgetfun/solana_kit) SDK.
4
8
 
5
9
  Given a Codama IDL (Interface Description Language) describing a Solana program, this renderer produces a complete Dart package with typed account classes, instruction builders, codec functions, error definitions, PDA helpers, and barrel exports.
@@ -9,6 +13,10 @@ Given a Codama IDL (Interface Description Language) describing a Solana program,
9
13
  ```bash
10
14
  pnpm add codama-renderers-dart
11
15
  # or
16
+
17
+ [![pub package](https://img.shields.io/pub/v/codama-renderers-dart.svg)](https://pub.dev/packages/codama-renderers-dart)
18
+ [![CI](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml)
19
+ [![Coverage](https://codecov.io/gh/openbudgetfun/solana_kit/branch/main/graph/badge.svg?flag=codama-renderers-dart)](https://codecov.io/gh/openbudgetfun/solana_kit?flag=codama-renderers-dart)
12
20
  npm install codama-renderers-dart
13
21
  ```
14
22
 
@@ -277,18 +285,38 @@ Key source files:
277
285
 
278
286
  ```bash
279
287
  # Install dependencies
288
+
289
+ [![pub package](https://img.shields.io/pub/v/codama-renderers-dart.svg)](https://pub.dev/packages/codama-renderers-dart)
290
+ [![CI](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml)
291
+ [![Coverage](https://codecov.io/gh/openbudgetfun/solana_kit/branch/main/graph/badge.svg?flag=codama-renderers-dart)](https://codecov.io/gh/openbudgetfun/solana_kit?flag=codama-renderers-dart)
280
292
  pnpm install
281
293
 
282
294
  # Type check
295
+
296
+ [![pub package](https://img.shields.io/pub/v/codama-renderers-dart.svg)](https://pub.dev/packages/codama-renderers-dart)
297
+ [![CI](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml)
298
+ [![Coverage](https://codecov.io/gh/openbudgetfun/solana_kit/branch/main/graph/badge.svg?flag=codama-renderers-dart)](https://codecov.io/gh/openbudgetfun/solana_kit?flag=codama-renderers-dart)
283
299
  npx tsc --noEmit
284
300
 
285
301
  # Run tests
302
+
303
+ [![pub package](https://img.shields.io/pub/v/codama-renderers-dart.svg)](https://pub.dev/packages/codama-renderers-dart)
304
+ [![CI](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml)
305
+ [![Coverage](https://codecov.io/gh/openbudgetfun/solana_kit/branch/main/graph/badge.svg?flag=codama-renderers-dart)](https://codecov.io/gh/openbudgetfun/solana_kit?flag=codama-renderers-dart)
286
306
  pnpm test
287
307
 
288
308
  # Watch mode
309
+
310
+ [![pub package](https://img.shields.io/pub/v/codama-renderers-dart.svg)](https://pub.dev/packages/codama-renderers-dart)
311
+ [![CI](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml)
312
+ [![Coverage](https://codecov.io/gh/openbudgetfun/solana_kit/branch/main/graph/badge.svg?flag=codama-renderers-dart)](https://codecov.io/gh/openbudgetfun/solana_kit?flag=codama-renderers-dart)
289
313
  pnpm test:watch
290
314
 
291
315
  # Build
316
+
317
+ [![pub package](https://img.shields.io/pub/v/codama-renderers-dart.svg)](https://pub.dev/packages/codama-renderers-dart)
318
+ [![CI](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/openbudgetfun/solana_kit/actions/workflows/ci.yml)
319
+ [![Coverage](https://codecov.io/gh/openbudgetfun/solana_kit/branch/main/graph/badge.svg?flag=codama-renderers-dart)](https://codecov.io/gh/openbudgetfun/solana_kit?flag=codama-renderers-dart)
292
320
  pnpm build
293
321
  ```
294
322
 
@@ -53,15 +53,28 @@ export function getInstructionPageFragment(
53
53
  .join("\n");
54
54
 
55
55
  const dataCtorParams = allArgManifests
56
- .map(({ arg }) => {
56
+ .map(({ arg, manifest }) => {
57
57
  const fieldName = camelCase(arg.name as string);
58
58
  if (isDiscriminatorArg(arg, node)) {
59
- return ` this.${fieldName} = ${getDiscriminatorDefault(arg, node)},`;
59
+ return isConstDefaultValue(arg.defaultValue)
60
+ ? ` this.${fieldName} = ${getDiscriminatorDefault(arg, node)},`
61
+ : ` ${manifest.type.content}? ${fieldName},`;
60
62
  }
61
63
  return ` required this.${fieldName},`;
62
64
  })
63
65
  .join("\n");
64
66
 
67
+ const dataCtorInitializers = allArgManifests
68
+ .map(({ arg }) => {
69
+ const fieldName = camelCase(arg.name as string);
70
+ if (isDiscriminatorArg(arg, node) && !isConstDefaultValue(arg.defaultValue)) {
71
+ return ` ${fieldName} = ${fieldName} ?? ${getDiscriminatorDefault(arg, node)}`;
72
+ }
73
+ return null;
74
+ })
75
+ .filter((value): value is string => value !== null)
76
+ .join(",\n");
77
+
65
78
  // Encoder/decoder for instruction data
66
79
  const encFields = allArgManifests
67
80
  .map(({ arg, manifest }) => {
@@ -91,10 +104,17 @@ export function getInstructionPageFragment(
91
104
  })
92
105
  .join("\n");
93
106
 
107
+ const dataCtorKeyword = dataCtorInitializers ? "" : "const ";
94
108
  const dataEncoderName = `get${typeName}InstructionDataEncoder`;
95
109
  const dataDecoderName = `get${typeName}InstructionDataDecoder`;
96
110
  const dataCodecName = `get${typeName}InstructionDataCodec`;
97
111
 
112
+ // Determine whether any arg or account name collides with 'programAddress'
113
+ const argNames = new Set(args.map((a) => camelCase(a.name as string)));
114
+ const accountNames = new Set(accounts.map((a) => camelCase(a.name as string)));
115
+ const hasProgramAddressCollision = argNames.has('programAddress') || accountNames.has('programAddress');
116
+ const instrProgramParam = hasProgramAddressCollision ? 'instructionProgramAddress' : 'programAddress';
117
+
98
118
  // Build the instruction builder function
99
119
  const accountParams = accounts
100
120
  .map((acc) => {
@@ -112,11 +132,14 @@ export function getInstructionPageFragment(
112
132
  .map((arg) => {
113
133
  const manifest = argManifestMap.get(arg)!;
114
134
  const fieldName = camelCase(arg.name as string);
135
+ const typeStr = manifest.type.content;
115
136
  const hasDefault = arg.defaultValue != null;
116
137
  if (hasDefault) {
117
- return ` ${manifest.type.content}? ${fieldName},`;
138
+ // Don't add `?` if the type is already nullable.
139
+ const nullableType = typeStr.endsWith("?") ? typeStr : `${typeStr}?`;
140
+ return ` ${nullableType} ${fieldName},`;
118
141
  }
119
- return ` required ${manifest.type.content} ${fieldName},`;
142
+ return ` required ${typeStr} ${fieldName},`;
120
143
  })
121
144
  .join("\n");
122
145
 
@@ -140,6 +163,7 @@ export function getInstructionPageFragment(
140
163
  if (isDiscriminatorArg(arg, node)) {
141
164
  return ""; // Use default
142
165
  }
166
+ // Use 'arg_<name>' prefix to reference builder params without shadowing.
143
167
  return ` ${fieldName}: ${fieldName}${arg.defaultValue != null ? ` ?? ${getDefaultValue(arg)}` : ""},`;
144
168
  })
145
169
  .filter(Boolean)
@@ -174,9 +198,9 @@ ${use("AccountRole", "solanaInstructions")}`,
174
198
  parts.push(fragment`
175
199
  @immutable
176
200
  class ${fragmentFromString(dataClassName)} {
177
- const ${fragmentFromString(dataClassName)}({
201
+ ${fragmentFromString(dataCtorKeyword)}${fragmentFromString(dataClassName)}({
178
202
  ${fragmentFromString(dataCtorParams)}
179
- });
203
+ })${fragmentFromString(dataCtorInitializers ? ` :\n${dataCtorInitializers}` : "")};
180
204
 
181
205
  ${fragmentFromString(dataFieldDecls)}
182
206
  }`);
@@ -217,20 +241,20 @@ Codec<${fragmentFromString(dataClassName)}, ${fragmentFromString(dataClassName)}
217
241
  parts.push(fragment`
218
242
  /// Creates a [${fragmentFromString(typeName)}] instruction.
219
243
  Instruction ${fragmentFromString(instrFnName)}({
220
- required Address programAddress,
244
+ required Address ${fragmentFromString(instrProgramParam)},
221
245
  ${fragmentFromString(accountParams)}
222
246
  ${fragmentFromString(argParams)}
223
247
  }) {
224
- final data = ${fragmentFromString(dataClassName)}(
248
+ final instructionData = ${fragmentFromString(dataClassName)}(
225
249
  ${fragmentFromString(dataConstruction)}
226
250
  );
227
251
 
228
252
  return Instruction(
229
- programAddress: programAddress,
253
+ programAddress: ${fragmentFromString(instrProgramParam)},
230
254
  accounts: [
231
255
  ${fragmentFromString(accountMetas)}
232
256
  ],
233
- data: ${fragmentFromString(dataEncoderName)}().encode(data),
257
+ data: ${fragmentFromString(dataEncoderName)}().encode(instructionData),
234
258
  );
235
259
  }`);
236
260
 
@@ -282,10 +306,28 @@ function getDiscriminatorDefault(
282
306
  if (arg.defaultValue.kind === "numberValueNode") {
283
307
  return String(arg.defaultValue.number);
284
308
  }
309
+ if (arg.defaultValue.kind === "bytesValueNode") {
310
+ const data = arg.defaultValue.data as string;
311
+ const bytes = data.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) ?? [];
312
+ return `Uint8List.fromList([${bytes.join(", ")}])`;
313
+ }
285
314
  }
286
315
  return "0";
287
316
  }
288
317
 
318
+ function isConstDefaultValue(defaultValue: InstructionArgumentNode["defaultValue"]): boolean {
319
+ if (!defaultValue) return false;
320
+ switch (defaultValue.kind) {
321
+ case "numberValueNode":
322
+ case "booleanValueNode":
323
+ case "stringValueNode":
324
+ case "noneValueNode":
325
+ return true;
326
+ default:
327
+ return false;
328
+ }
329
+ }
330
+
289
331
  function getDefaultValue(arg: InstructionArgumentNode): string {
290
332
  if (!arg.defaultValue) return "null";
291
333
  const dv = arg.defaultValue;
@@ -300,6 +342,10 @@ function getDefaultValue(arg: InstructionArgumentNode): string {
300
342
  return `Address('${dv.publicKey}')`;
301
343
  case "noneValueNode":
302
344
  return "null";
345
+ case "bytesValueNode": {
346
+ const bytes = dv.data.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) ?? [];
347
+ return `Uint8List.fromList([${bytes.join(", ")}])`;
348
+ }
303
349
  default:
304
350
  return "null";
305
351
  }
@@ -23,6 +23,11 @@ export function getPdaPageFragment(
23
23
  const seedsClassName = `${pascalCase(name)}Seeds`;
24
24
 
25
25
  const seeds = node.seeds ?? [];
26
+ const hasByteSeeds = seeds.some(
27
+ (seed) =>
28
+ seed.kind === "constantPdaSeedNode" &&
29
+ seed.value.kind === "bytesValueNode",
30
+ );
26
31
 
27
32
  // Separate variable seeds from constant seeds
28
33
  const variableSeeds = seeds.filter(
@@ -79,11 +84,15 @@ export function getPdaPageFragment(
79
84
  ? `Address programAddress = const Address('${node.programId}')`
80
85
  : "required Address programAddress";
81
86
 
87
+ const typedDataImport = hasByteSeeds
88
+ ? use("Uint8List", "dartTypedData")
89
+ : fragmentFromString("");
90
+
82
91
  const parts: Fragment[] = [
83
92
  fragment`// Auto-generated. Do not edit.
84
93
  // ignore_for_file: type=lint
85
94
 
86
- ${use("Uint8List", "dartTypedData")}
95
+ ${typedDataImport}
87
96
  ${use("immutable", "meta")}
88
97
  ${use("Address", "solanaAddresses")}
89
98
  ${use("getAddressEncoder", "solanaAddresses")}
@@ -110,13 +119,13 @@ Future<(Address, int)> ${fragmentFromString(findFnName)}({
110
119
  ${fragmentFromString(seedsParam)}
111
120
  ${fragmentFromString(programIdParam)},
112
121
  }) async {
113
- final seeds = <Object>[
122
+ final seedValues = <Object>[
114
123
  ${fragmentFromString(seedValues.join("\n"))}
115
124
  ];
116
125
 
117
126
  return getProgramDerivedAddress(
118
127
  programAddress: programAddress,
119
- seeds: seeds,
128
+ seeds: seedValues,
120
129
  );
121
130
  }`);
122
131
 
@@ -125,6 +125,8 @@ function getDataEnumPageFragment(
125
125
  const variantClasses: string[] = [];
126
126
  const encoderVariants: string[] = [];
127
127
  const decoderVariants: string[] = [];
128
+ const encodeCases: string[] = [];
129
+ const decodeCases: string[] = [];
128
130
  // Collect all field type manifests from variants so we can merge their imports
129
131
  const allVariantManifests: { encoder: Fragment; decoder: Fragment; type: Fragment }[] = [];
130
132
 
@@ -154,8 +156,10 @@ function getDataEnumPageFragment(
154
156
  `(${i}, getStructEncoder(<(String, Encoder<Object?>)>[]))`,
155
157
  );
156
158
  decoderVariants.push(
157
- `(${i}, getStructDecoder(<(String, Decoder<Object?>)>[]).map((_) => const ${variantClassName}()))`,
159
+ `(${i}, transformDecoder<Map<String, Object?>, Map<String, Object?>>(getStructDecoder(<(String, Decoder<Object?>)>[]), (Map<String, Object?> map, Uint8List bytes, int offset) => <String, Object?>{}))`,
158
160
  );
161
+ encodeCases.push(`${variantClassName}() => <String, Object?>{'__kind': ${i}},`);
162
+ decodeCases.push(`case ${i}: return const ${variantClassName}();`);
159
163
  } else if (variant.kind === "enumStructVariantTypeNode") {
160
164
  const resolvedStruct = resolveNestedTypeNode(variant.struct);
161
165
  const fields = resolvedStruct.fields;
@@ -178,23 +182,28 @@ function getDataEnumPageFragment(
178
182
  const ctorParams = fields
179
183
  .map((f: StructFieldTypeNode) => ` required this.${camelCase(f.name as string)},`)
180
184
  .join("\n");
181
-
182
- const eqChecks = fields
183
- .map((f: StructFieldTypeNode) => `${camelCase(f.name as string)} == other.${camelCase(f.name as string)}`)
184
- .join(" &&\n ");
185
-
186
- const hashFields = fields
187
- .map((f: StructFieldTypeNode) => camelCase(f.name as string))
188
- .join(", ");
185
+ const ctorSignature =
186
+ fields.length === 0
187
+ ? ` const ${variantClassName}();`
188
+ : ` const ${variantClassName}({\n${ctorParams}\n });`;
189
+
190
+ const eqChecks =
191
+ fields.length === 0
192
+ ? "true"
193
+ : fields
194
+ .map((f: StructFieldTypeNode) => `${camelCase(f.name as string)} == other.${camelCase(f.name as string)}`)
195
+ .join(" &&\n ");
196
+
197
+ const hashExpression = getHashExpression(
198
+ fields.map((f: StructFieldTypeNode) => camelCase(f.name as string)),
199
+ );
189
200
 
190
201
  const toStringFields = fields
191
202
  .map((f: StructFieldTypeNode) => `${camelCase(f.name as string)}: $${camelCase(f.name as string)}`)
192
203
  .join(", ");
193
204
 
194
205
  variantClasses.push(`final class ${variantClassName} extends ${typeName} {
195
- const ${variantClassName}({
196
- ${ctorParams}
197
- });
206
+ ${ctorSignature}
198
207
 
199
208
  ${fieldDecls}
200
209
 
@@ -205,7 +214,7 @@ ${fieldDecls}
205
214
  ${eqChecks};
206
215
 
207
216
  @override
208
- int get hashCode => Object.hash(${hashFields});
217
+ int get hashCode => ${hashExpression};
209
218
 
210
219
  @override
211
220
  String toString() => '${typeName}.${variantClassName}(${toStringFields})';
@@ -242,11 +251,23 @@ ${fieldDecls}
242
251
  .join(", ");
243
252
 
244
253
  encoderVariants.push(
245
- `(${i}, transformEncoder(getStructEncoder([${encFields}]), (${variantClassName} value) => <String, Object?>{${toMapFields}}))`,
254
+ `(${i}, getStructEncoder([${encFields}]))`,
246
255
  );
247
256
  decoderVariants.push(
248
- `(${i}, transformDecoder(getStructDecoder([${decFields}]), (Map<String, Object?> map, Uint8List bytes, int offset) => ${variantClassName}(${fromMapFields})))`,
257
+ `(${i}, transformDecoder<Map<String, Object?>, Map<String, Object?>>(getStructDecoder([${decFields}]), (Map<String, Object?> map, Uint8List bytes, int offset) => map))`,
249
258
  );
259
+ const structPattern =
260
+ fields.length === 0
261
+ ? ''
262
+ : fields
263
+ .map((f: StructFieldTypeNode) => `${camelCase(f.name as string)}: final ${camelCase(f.name as string)}`)
264
+ .join(', ');
265
+ const structMapEntries =
266
+ fields.length === 0
267
+ ? ''
268
+ : `, ${fields.map((f: StructFieldTypeNode)=>`'${f.name as string}': ${camelCase(f.name as string)}`).join(', ')}`;
269
+ encodeCases.push(`${variantClassName}(${structPattern}) => <String, Object?>{'__kind': ${i}${structMapEntries}},`);
270
+ decodeCases.push(`case ${i}: return ${variantClassName}(${fromMapFields.replace(/\n/g, ' ').replace(/,$/, '')});`);
250
271
  } else if (variant.kind === "enumTupleVariantTypeNode") {
251
272
  const resolvedTuple = resolveNestedTypeNode(variant.tuple);
252
273
  const items = resolvedTuple.items;
@@ -270,11 +291,13 @@ ${fieldDecls}
270
291
  String toString() => '${typeName}.${variantClassName}(\$value)';
271
292
  }`);
272
293
  encoderVariants.push(
273
- `(${i}, transformEncoder(${manifest.encoder.content}, (${variantClassName} value) => value.value))`,
294
+ `(${i}, transformEncoder<${manifest.type.content}, Map<String, Object?>>(${manifest.encoder.content}, (Map<String, Object?> map) => map['value']! as ${manifest.type.content}))`,
274
295
  );
275
296
  decoderVariants.push(
276
- `(${i}, transformDecoder(${manifest.decoder.content}, (${manifest.type.content} value, Uint8List bytes, int offset) => ${variantClassName}(value)))`,
297
+ `(${i}, transformDecoder<${manifest.type.content}, Map<String, Object?>>(${manifest.decoder.content}, (${manifest.type.content} value, Uint8List bytes, int offset) => <String, Object?>{'value': value}))`,
277
298
  );
299
+ encodeCases.push(`${variantClassName}(value: final value) => <String, Object?>{'__kind': ${i}, 'value': value},`);
300
+ decodeCases.push(`case ${i}: return ${variantClassName}(map['value']! as ${manifest.type.content});`);
278
301
  }
279
302
  }
280
303
  }
@@ -290,7 +313,6 @@ ${fieldDecls}
290
313
  // ignore_for_file: type=lint
291
314
 
292
315
  ${use("Uint8List", "dartTypedData")}
293
- ${use("immutable", "meta")}
294
316
  ${use("Encoder", "solanaCodecsCore")}
295
317
  ${use("Decoder", "solanaCodecsCore")}
296
318
  ${use("Codec", "solanaCodecsCore")}
@@ -311,15 +333,28 @@ sealed class ${fragmentFromString(typeName)} {
311
333
  ${fragmentFromString(variantClasses.join("\n\n"))}
312
334
 
313
335
  Encoder<${fragmentFromString(typeName)}> ${fragmentFromString(encoderName)}() {
314
- return getDiscriminatedUnionEncoder([
315
- ${fragmentFromString(encoderVariants.join(",\n "))},
316
- ], size: get${fragmentFromString(sizeCodecName)}Encoder());
336
+ return transformEncoder<Map<String, Object?>, ${fragmentFromString(typeName)}>(
337
+ getDiscriminatedUnionEncoder([
338
+ ${fragmentFromString(encoderVariants.join(",\n "))},
339
+ ], size: get${fragmentFromString(sizeCodecName)}Encoder()),
340
+ (${fragmentFromString(typeName)} value) => switch (value) {
341
+ ${fragmentFromString(encodeCases.join("\n "))}
342
+ },
343
+ );
317
344
  }
318
345
 
319
346
  Decoder<${fragmentFromString(typeName)}> ${fragmentFromString(decoderName)}() {
320
- return getDiscriminatedUnionDecoder([
321
- ${fragmentFromString(decoderVariants.join(",\n "))},
322
- ], size: get${fragmentFromString(sizeCodecName)}Decoder());
347
+ return transformDecoder<Map<String, Object?>, ${fragmentFromString(typeName)}>(
348
+ getDiscriminatedUnionDecoder([
349
+ ${fragmentFromString(decoderVariants.join(",\n "))},
350
+ ], size: get${fragmentFromString(sizeCodecName)}Decoder()),
351
+ (Map<String, Object?> map, Uint8List bytes, int offset) {
352
+ switch (map['__kind']) {
353
+ ${fragmentFromString(decodeCases.join("\n "))}
354
+ }
355
+ throw StateError('Unsupported ${typeName} discriminator: \${map['__kind']}');
356
+ },
357
+ );
323
358
  }
324
359
 
325
360
  Codec<${fragmentFromString(typeName)}, ${fragmentFromString(typeName)}> ${fragmentFromString(codecName)}() {
@@ -365,6 +400,10 @@ function getStructPageFragment(
365
400
  const ctorParams = fields
366
401
  .map((f: StructFieldTypeNode) => ` required this.${camelCase(f.name as string)},`)
367
402
  .join("\n");
403
+ const ctorSignature =
404
+ fields.length === 0
405
+ ? ` const ${typeName}();`
406
+ : ` const ${typeName}({\n${ctorParams}\n });`;
368
407
 
369
408
  const eqChecks =
370
409
  fields.length === 0
@@ -376,9 +415,12 @@ function getStructPageFragment(
376
415
  )
377
416
  .join(" &&\n ");
378
417
 
379
- const hashFields = fields
380
- .map((f: StructFieldTypeNode) => camelCase(f.name as string))
381
- .join(", ");
418
+ const hashFields =
419
+ fields.length === 0
420
+ ? "runtimeType"
421
+ : fields
422
+ .map((f: StructFieldTypeNode) => camelCase(f.name as string))
423
+ .join(", ");
382
424
 
383
425
  const toStringFields = fields
384
426
  .map(
@@ -436,9 +478,7 @@ ${use("getStructDecoder", "solanaCodecsDataStructures")}
436
478
 
437
479
  @immutable
438
480
  class ${fragmentFromString(typeName)} {
439
- const ${fragmentFromString(typeName)}({
440
- ${fragmentFromString(ctorParams)}
441
- });
481
+ ${fragmentFromString(ctorSignature)}
442
482
 
443
483
  ${fragmentFromString(fieldDecls)}
444
484
 
@@ -450,7 +490,7 @@ ${fragmentFromString(fieldDecls)}
450
490
  ${fragmentFromString(eqChecks)};
451
491
 
452
492
  @override
453
- int get hashCode => Object.hash(${fragmentFromString(hashFields)});
493
+ int get hashCode => ${fragmentFromString(getHashExpression(fields.map((f: StructFieldTypeNode) => camelCase(f.name as string))))};
454
494
 
455
495
  @override
456
496
  String toString() => '${fragmentFromString(typeName)}(${fragmentFromString(toStringFields)})';
@@ -534,6 +574,12 @@ Codec<${fragmentFromString(typeName)}, ${fragmentFromString(typeName)}> ${fragme
534
574
  }`;
535
575
  }
536
576
 
577
+ function getHashExpression(fields: string[]): string {
578
+ if (fields.length === 0) return "runtimeType.hashCode";
579
+ if (fields.length === 1) return `${fields[0]}.hashCode`;
580
+ return `Object.hash(${fields.join(", ")})`;
581
+ }
582
+
537
583
  function getNumberCodecName(format: string): string {
538
584
  switch (format) {
539
585
  case "u8": return "U8";
@@ -89,7 +89,10 @@ export function getRenderMapVisitor(
89
89
  ...node.additionalPrograms.map((p) => visit(p, self)),
90
90
  ];
91
91
 
92
- return mergeRenderMaps(programMaps);
92
+ const mergedProgramMap = mergeRenderMaps(programMaps);
93
+ const categoryMaps = getSharedCategoryIndexMaps(mergedProgramMap);
94
+
95
+ return mergeRenderMaps([mergedProgramMap, ...categoryMaps]);
93
96
  },
94
97
 
95
98
  visitProgram(node: ProgramNode, { self }) {
@@ -151,76 +154,15 @@ export function getRenderMapVisitor(
151
154
  maps.push(errorMap);
152
155
  }
153
156
 
154
- // Barrel exports
157
+ // Root barrel
155
158
  const categories: string[] = [];
159
+ if (accountFiles.length > 0) categories.push("accounts");
160
+ if (instructionFiles.length > 0) categories.push("instructions");
161
+ if (typeFiles.length > 0) categories.push("types");
162
+ if (pdaFiles.length > 0) categories.push("pdas");
163
+ if (errorFiles.length > 0) categories.push("errors");
164
+ if (programFiles.length > 0) categories.push("programs");
156
165
 
157
- if (accountFiles.length > 0) {
158
- categories.push("accounts");
159
- let indexMap = createRenderMap<Fragment>();
160
- indexMap = addToRenderMap(
161
- indexMap,
162
- "accounts/accounts.dart",
163
- getIndexPageFragment(accountFiles),
164
- );
165
- maps.push(indexMap);
166
- }
167
-
168
- if (instructionFiles.length > 0) {
169
- categories.push("instructions");
170
- let indexMap = createRenderMap<Fragment>();
171
- indexMap = addToRenderMap(
172
- indexMap,
173
- "instructions/instructions.dart",
174
- getIndexPageFragment(instructionFiles),
175
- );
176
- maps.push(indexMap);
177
- }
178
-
179
- if (typeFiles.length > 0) {
180
- categories.push("types");
181
- let indexMap = createRenderMap<Fragment>();
182
- indexMap = addToRenderMap(
183
- indexMap,
184
- "types/types.dart",
185
- getIndexPageFragment(typeFiles),
186
- );
187
- maps.push(indexMap);
188
- }
189
-
190
- if (pdaFiles.length > 0) {
191
- categories.push("pdas");
192
- let indexMap = createRenderMap<Fragment>();
193
- indexMap = addToRenderMap(
194
- indexMap,
195
- "pdas/pdas.dart",
196
- getIndexPageFragment(pdaFiles),
197
- );
198
- maps.push(indexMap);
199
- }
200
-
201
- if (errorFiles.length > 0) {
202
- categories.push("errors");
203
- let indexMap = createRenderMap<Fragment>();
204
- indexMap = addToRenderMap(
205
- indexMap,
206
- "errors/errors.dart",
207
- getIndexPageFragment(errorFiles),
208
- );
209
- maps.push(indexMap);
210
- }
211
-
212
- if (programFiles.length > 0) {
213
- categories.push("programs");
214
- let indexMap = createRenderMap<Fragment>();
215
- indexMap = addToRenderMap(
216
- indexMap,
217
- "programs/programs.dart",
218
- getIndexPageFragment(programFiles),
219
- );
220
- maps.push(indexMap);
221
- }
222
-
223
- // Root barrel
224
166
  if (categories.length > 0) {
225
167
  let rootMap = createRenderMap<Fragment>();
226
168
  rootMap = addToRenderMap(
@@ -270,3 +212,46 @@ export function getRenderMapVisitor(
270
212
  (v) => recordLinkablesOnFirstVisitVisitor(v, linkables),
271
213
  );
272
214
  }
215
+
216
+ function getSharedCategoryIndexMaps(
217
+ renderMap: RenderMap<Fragment>,
218
+ ): RenderMap<Fragment>[] {
219
+ const categories = new Map<string, Set<string>>();
220
+ const categoryIndexNames: Record<string, string> = {
221
+ accounts: "accounts.dart",
222
+ instructions: "instructions.dart",
223
+ types: "types.dart",
224
+ pdas: "pdas.dart",
225
+ errors: "errors.dart",
226
+ programs: "programs.dart",
227
+ };
228
+
229
+ for (const renderPath of renderMap.keys()) {
230
+ const match = renderPath.match(
231
+ /^(accounts|instructions|types|pdas|errors|programs)\/([^/]+\.dart)$/,
232
+ );
233
+ if (!match) continue;
234
+
235
+ const [, category, fileName] = match;
236
+ if (fileName === categoryIndexNames[category]) continue;
237
+
238
+ if (!categories.has(category)) {
239
+ categories.set(category, new Set());
240
+ }
241
+ categories.get(category)!.add(fileName);
242
+ }
243
+
244
+ const maps: RenderMap<Fragment>[] = [];
245
+ for (const [category, files] of categories.entries()) {
246
+ const sortedFiles = [...files].sort();
247
+ let map = createRenderMap<Fragment>();
248
+ map = addToRenderMap(
249
+ map,
250
+ `${category}/${categoryIndexNames[category]}`,
251
+ getIndexPageFragment(sortedFiles),
252
+ );
253
+ maps.push(map);
254
+ }
255
+
256
+ return maps;
257
+ }
@@ -417,6 +417,7 @@ export function getTypeManifestVisitor(input: {
417
417
 
418
418
  visitOptionType(node: OptionTypeNode, { self }) {
419
419
  const itemManifest = visit(node.item, self);
420
+ const itemTypeStr = itemManifest.type.content;
420
421
  const resolvedPrefix = resolveNestedTypeNode(node.prefix);
421
422
  const prefixExpr = resolvedPrefix.format !== "u8"
422
423
  ? `, prefix: ${getNumberCodecExpression(resolvedPrefix.format, "Encoder")}`
@@ -424,17 +425,20 @@ export function getTypeManifestVisitor(input: {
424
425
  const prefixDecoderExpr = resolvedPrefix.format !== "u8"
425
426
  ? `, prefix: ${getNumberCodecExpression(resolvedPrefix.format, "Decoder")}`
426
427
  : "";
428
+ const noneValueExpr = node.fixed
429
+ ? ", noneValue: const ZeroesNoneValue()"
430
+ : "";
427
431
 
428
432
  return {
429
433
  type: fragment`${itemManifest.type}?`,
430
434
  encoder: fragment`${use(
431
435
  "getNullableEncoder",
432
436
  "solanaCodecsDataStructures",
433
- )}(${itemManifest.encoder}${fragmentFromString(prefixExpr)})`,
437
+ )}<${fragmentFromString(itemTypeStr)}>(${itemManifest.encoder}${fragmentFromString(prefixExpr)}${fragmentFromString(noneValueExpr)})`,
434
438
  decoder: fragment`${use(
435
439
  "getNullableDecoder",
436
440
  "solanaCodecsDataStructures",
437
- )}(${itemManifest.decoder}${fragmentFromString(prefixDecoderExpr)})`,
441
+ )}<${fragmentFromString(itemTypeStr)}>(${itemManifest.decoder}${fragmentFromString(prefixDecoderExpr)}${fragmentFromString(noneValueExpr)})`,
438
442
  value: emptyTypeManifest().value,
439
443
  isEnum: false,
440
444
  };
@@ -445,17 +449,18 @@ export function getTypeManifestVisitor(input: {
445
449
  { self },
446
450
  ) {
447
451
  const itemManifest = visit(node.item, self);
452
+ const itemTypeStr = itemManifest.type.content;
448
453
 
449
454
  return {
450
455
  type: fragment`${itemManifest.type}?`,
451
456
  encoder: fragment`${use(
452
457
  "getNullableEncoder",
453
458
  "solanaCodecsDataStructures",
454
- )}(${itemManifest.encoder}, hasPrefix: false)`,
459
+ )}<${fragmentFromString(itemTypeStr)}>(${itemManifest.encoder}, hasPrefix: false)`,
455
460
  decoder: fragment`${use(
456
461
  "getNullableDecoder",
457
462
  "solanaCodecsDataStructures",
458
- )}(${itemManifest.decoder}, hasPrefix: false)`,
463
+ )}<${fragmentFromString(itemTypeStr)}>(${itemManifest.decoder}, hasPrefix: false)`,
459
464
  value: emptyTypeManifest().value,
460
465
  isEnum: false,
461
466
  };
@@ -466,17 +471,18 @@ export function getTypeManifestVisitor(input: {
466
471
  { self },
467
472
  ) {
468
473
  const itemManifest = visit(node.item, self);
474
+ const itemTypeStr = itemManifest.type.content;
469
475
 
470
476
  return {
471
477
  type: fragment`${itemManifest.type}?`,
472
478
  encoder: fragment`${use(
473
479
  "getNullableEncoder",
474
480
  "solanaCodecsDataStructures",
475
- )}(${itemManifest.encoder}, hasPrefix: false, noneValue: const ZeroesNoneValue())`,
481
+ )}<${fragmentFromString(itemTypeStr)}>(${itemManifest.encoder}, hasPrefix: false, noneValue: const ZeroesNoneValue())`,
476
482
  decoder: fragment`${use(
477
483
  "getNullableDecoder",
478
484
  "solanaCodecsDataStructures",
479
- )}(${itemManifest.decoder}, hasPrefix: false, noneValue: const ZeroesNoneValue())`,
485
+ )}<${fragmentFromString(itemTypeStr)}>(${itemManifest.decoder}, hasPrefix: false, noneValue: const ZeroesNoneValue())`,
480
486
  value: emptyTypeManifest().value,
481
487
  isEnum: false,
482
488
  };
@@ -502,7 +508,26 @@ export function getTypeManifestVisitor(input: {
502
508
 
503
509
  visitSizePrefixType(node: SizePrefixTypeNode, { self }) {
504
510
  const innerManifest = visit(node.type, self);
505
- const prefixManifest = visit(node.prefix, self);
511
+
512
+ // The Dart addEncoderSizePrefix / addDecoderSizePrefix APIs require
513
+ // Encoder<num> / Decoder<num> for the prefix. BigInt-width number types
514
+ // (u64, u128, i64, i128) return Encoder<BigInt> which is incompatible.
515
+ // When the IDL specifies such a prefix, substitute u32 which is wide
516
+ // enough for any realistic size prefix and satisfies the num constraint.
517
+ const bigIntFormats = new Set(["u64", "u128", "i64", "i128"]);
518
+ let prefixManifest;
519
+ if (
520
+ node.prefix.kind === "numberTypeNode" &&
521
+ bigIntFormats.has((node.prefix as any).format)
522
+ ) {
523
+ // Use u32 encoder/decoder instead of the oversized type.
524
+ prefixManifest = {
525
+ encoder: fragment`${use("getU32Encoder", "solanaCodecsNumbers")}()`,
526
+ decoder: fragment`${use("getU32Decoder", "solanaCodecsNumbers")}()`,
527
+ };
528
+ } else {
529
+ prefixManifest = visit(node.prefix, self);
530
+ }
506
531
 
507
532
  return {
508
533
  type: innerManifest.type,
@@ -521,7 +546,7 @@ export function getTypeManifestVisitor(input: {
521
546
 
522
547
  visitHiddenPrefixType(node: HiddenPrefixTypeNode, { self }) {
523
548
  const innerManifest = visit(node.type, self);
524
- const prefixes = node.prefix.map((p) => visit(p, self));
549
+ const prefixes = node.prefix.map((p) => getHiddenAffixManifest(p, self));
525
550
  const prefixEncoders = prefixes
526
551
  .map((p) => p.encoder.content)
527
552
  .join(", ");
@@ -556,7 +581,7 @@ export function getTypeManifestVisitor(input: {
556
581
 
557
582
  visitHiddenSuffixType(node: HiddenSuffixTypeNode, { self }) {
558
583
  const innerManifest = visit(node.type, self);
559
- const suffixes = node.suffix.map((s) => visit(s, self));
584
+ const suffixes = node.suffix.map((s) => getHiddenAffixManifest(s, self));
560
585
  const suffixEncoders = suffixes
561
586
  .map((s) => s.encoder.content)
562
587
  .join(", ");
@@ -792,3 +817,55 @@ function getSetSizeExpression(node: SetTypeNode): string {
792
817
  return "";
793
818
  }
794
819
  }
820
+
821
+ type HiddenAffixManifest = {
822
+ encoder: Fragment;
823
+ decoder: Fragment;
824
+ };
825
+
826
+ function getHiddenAffixManifest(
827
+ node: any,
828
+ self: Visitor<TypeManifest>,
829
+ ): HiddenAffixManifest {
830
+ if (node?.kind !== "constantValueNode") {
831
+ const manifest = visit(node, self);
832
+ return { encoder: manifest.encoder, decoder: manifest.decoder };
833
+ }
834
+
835
+ const constantBytes = getConstantBytesExpression(node);
836
+ return {
837
+ encoder: fragment`${use("getConstantEncoder", "solanaCodecsDataStructures")}(${constantBytes})`,
838
+ decoder: fragment`${use("getConstantDecoder", "solanaCodecsDataStructures")}(${constantBytes})`,
839
+ };
840
+ }
841
+
842
+ function getConstantBytesExpression(node: any): Fragment {
843
+ const valueNode = node.value;
844
+ const typeNode = node.type;
845
+
846
+ if (valueNode?.kind !== "numberValueNode") {
847
+ throw new Error(
848
+ `Unsupported hidden affix constant value kind: ${valueNode?.kind}`,
849
+ );
850
+ }
851
+
852
+ const value = Number(valueNode.number);
853
+
854
+ if (typeNode?.kind === "numberTypeNode") {
855
+ return fragment`${use(`get${getNumberCodecName(typeNode.format)}Encoder`, "solanaCodecsNumbers")}().encode(${value})`;
856
+ }
857
+
858
+ if (typeNode?.kind === "preOffsetTypeNode") {
859
+ const inner = typeNode.type;
860
+ if (inner?.kind !== "numberTypeNode") {
861
+ throw new Error(
862
+ `Unsupported hidden affix pre-offset inner type kind: ${inner?.kind}`,
863
+ );
864
+ }
865
+ return fragment`${use("padLeftEncoder", "solanaCodecsCore")}(${use(`get${getNumberCodecName(inner.format)}Encoder`, "solanaCodecsNumbers")}(), ${typeNode.offset}).encode(${value})`;
866
+ }
867
+
868
+ throw new Error(
869
+ `Unsupported hidden affix constant type kind: ${typeNode?.kind}`,
870
+ );
871
+ }