codama-renderers-dart 0.3.1 → 0.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codama-renderers-dart",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
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
 
@@ -1,4 +1,4 @@
1
- import type {
1
+ import {
2
2
  InstructionAccountNode,
3
3
  InstructionArgumentNode,
4
4
  InstructionNode,
@@ -16,6 +16,7 @@ import {
16
16
  import type { RenderScope } from "../utils/options.js";
17
17
  import { camelCase, pascalCase } from "../utils/nameTransformers.js";
18
18
  import { getDiscriminatorConstantsFragment } from "./discriminatorConstants.js";
19
+ import { WELL_KNOWN_ADDRESSES } from "../utils/wellKnownAddresses.js";
19
20
 
20
21
  /**
21
22
  * Generate a full Dart file for an instruction.
@@ -53,15 +54,28 @@ export function getInstructionPageFragment(
53
54
  .join("\n");
54
55
 
55
56
  const dataCtorParams = allArgManifests
56
- .map(({ arg }) => {
57
+ .map(({ arg, manifest }) => {
57
58
  const fieldName = camelCase(arg.name as string);
58
59
  if (isDiscriminatorArg(arg, node)) {
59
- return ` this.${fieldName} = ${getDiscriminatorDefault(arg, node)},`;
60
+ return isConstDefaultValue(arg.defaultValue)
61
+ ? ` this.${fieldName} = ${getDiscriminatorDefault(arg, node)},`
62
+ : ` ${manifest.type.content}? ${fieldName},`;
60
63
  }
61
64
  return ` required this.${fieldName},`;
62
65
  })
63
66
  .join("\n");
64
67
 
68
+ const dataCtorInitializers = allArgManifests
69
+ .map(({ arg }) => {
70
+ const fieldName = camelCase(arg.name as string);
71
+ if (isDiscriminatorArg(arg, node) && !isConstDefaultValue(arg.defaultValue)) {
72
+ return ` ${fieldName} = ${fieldName} ?? ${getDiscriminatorDefault(arg, node)}`;
73
+ }
74
+ return null;
75
+ })
76
+ .filter((value): value is string => value !== null)
77
+ .join(",\n");
78
+
65
79
  // Encoder/decoder for instruction data
66
80
  const encFields = allArgManifests
67
81
  .map(({ arg, manifest }) => {
@@ -91,10 +105,17 @@ export function getInstructionPageFragment(
91
105
  })
92
106
  .join("\n");
93
107
 
108
+ const dataCtorKeyword = dataCtorInitializers ? "" : "const ";
94
109
  const dataEncoderName = `get${typeName}InstructionDataEncoder`;
95
110
  const dataDecoderName = `get${typeName}InstructionDataDecoder`;
96
111
  const dataCodecName = `get${typeName}InstructionDataCodec`;
97
112
 
113
+ // Determine whether any arg or account name collides with 'programAddress'
114
+ const argNames = new Set(args.map((a) => camelCase(a.name as string)));
115
+ const accountNames = new Set(accounts.map((a) => camelCase(a.name as string)));
116
+ const hasProgramAddressCollision = argNames.has('programAddress') || accountNames.has('programAddress');
117
+ const instrProgramParam = hasProgramAddressCollision ? 'instructionProgramAddress' : 'programAddress';
118
+
98
119
  // Build the instruction builder function
99
120
  const accountParams = accounts
100
121
  .map((acc) => {
@@ -112,11 +133,14 @@ export function getInstructionPageFragment(
112
133
  .map((arg) => {
113
134
  const manifest = argManifestMap.get(arg)!;
114
135
  const fieldName = camelCase(arg.name as string);
136
+ const typeStr = manifest.type.content;
115
137
  const hasDefault = arg.defaultValue != null;
116
138
  if (hasDefault) {
117
- return ` ${manifest.type.content}? ${fieldName},`;
139
+ // Don't add `?` if the type is already nullable.
140
+ const nullableType = typeStr.endsWith("?") ? typeStr : `${typeStr}?`;
141
+ return ` ${nullableType} ${fieldName},`;
118
142
  }
119
- return ` required ${manifest.type.content} ${fieldName},`;
143
+ return ` required ${typeStr} ${fieldName},`;
120
144
  })
121
145
  .join("\n");
122
146
 
@@ -140,6 +164,7 @@ export function getInstructionPageFragment(
140
164
  if (isDiscriminatorArg(arg, node)) {
141
165
  return ""; // Use default
142
166
  }
167
+ // Use 'arg_<name>' prefix to reference builder params without shadowing.
143
168
  return ` ${fieldName}: ${fieldName}${arg.defaultValue != null ? ` ?? ${getDefaultValue(arg)}` : ""},`;
144
169
  })
145
170
  .filter(Boolean)
@@ -174,9 +199,9 @@ ${use("AccountRole", "solanaInstructions")}`,
174
199
  parts.push(fragment`
175
200
  @immutable
176
201
  class ${fragmentFromString(dataClassName)} {
177
- const ${fragmentFromString(dataClassName)}({
202
+ ${fragmentFromString(dataCtorKeyword)}${fragmentFromString(dataClassName)}({
178
203
  ${fragmentFromString(dataCtorParams)}
179
- });
204
+ })${fragmentFromString(dataCtorInitializers ? ` :\n${dataCtorInitializers}` : "")};
180
205
 
181
206
  ${fragmentFromString(dataFieldDecls)}
182
207
  }`);
@@ -217,20 +242,20 @@ Codec<${fragmentFromString(dataClassName)}, ${fragmentFromString(dataClassName)}
217
242
  parts.push(fragment`
218
243
  /// Creates a [${fragmentFromString(typeName)}] instruction.
219
244
  Instruction ${fragmentFromString(instrFnName)}({
220
- required Address programAddress,
245
+ required Address ${fragmentFromString(instrProgramParam)},
221
246
  ${fragmentFromString(accountParams)}
222
247
  ${fragmentFromString(argParams)}
223
248
  }) {
224
- final data = ${fragmentFromString(dataClassName)}(
249
+ final instructionData = ${fragmentFromString(dataClassName)}(
225
250
  ${fragmentFromString(dataConstruction)}
226
251
  );
227
252
 
228
253
  return Instruction(
229
- programAddress: programAddress,
254
+ programAddress: ${fragmentFromString(instrProgramParam)},
230
255
  accounts: [
231
256
  ${fragmentFromString(accountMetas)}
232
257
  ],
233
- data: ${fragmentFromString(dataEncoderName)}().encode(data),
258
+ data: ${fragmentFromString(dataEncoderName)}().encode(instructionData),
234
259
  );
235
260
  }`);
236
261
 
@@ -282,10 +307,28 @@ function getDiscriminatorDefault(
282
307
  if (arg.defaultValue.kind === "numberValueNode") {
283
308
  return String(arg.defaultValue.number);
284
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
+ }
285
315
  }
286
316
  return "0";
287
317
  }
288
318
 
319
+ function isConstDefaultValue(defaultValue: InstructionArgumentNode["defaultValue"]): boolean {
320
+ if (!defaultValue) return false;
321
+ switch (defaultValue.kind) {
322
+ case "numberValueNode":
323
+ case "booleanValueNode":
324
+ case "stringValueNode":
325
+ case "noneValueNode":
326
+ return true;
327
+ default:
328
+ return false;
329
+ }
330
+ }
331
+
289
332
  function getDefaultValue(arg: InstructionArgumentNode): string {
290
333
  if (!arg.defaultValue) return "null";
291
334
  const dv = arg.defaultValue;
@@ -296,10 +339,20 @@ function getDefaultValue(arg: InstructionArgumentNode): string {
296
339
  return String(dv.boolean);
297
340
  case "stringValueNode":
298
341
  return `'${dv.string}'`;
299
- case "publicKeyValueNode":
342
+ case "publicKeyValueNode": {
343
+ const wellKnownName = WELL_KNOWN_ADDRESSES.get(dv.publicKey);
344
+ if (wellKnownName) {
345
+ use(wellKnownName, "solanaAddresses");
346
+ return wellKnownName;
347
+ }
300
348
  return `Address('${dv.publicKey}')`;
349
+ }
301
350
  case "noneValueNode":
302
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
+ }
303
356
  default:
304
357
  return "null";
305
358
  }
@@ -10,6 +10,7 @@ import {
10
10
  } from "../utils/fragment.js";
11
11
  import type { RenderScope } from "../utils/options.js";
12
12
  import { camelCase, pascalCase } from "../utils/nameTransformers.js";
13
+ import { WELL_KNOWN_ADDRESSES } from "../utils/wellKnownAddresses.js";
13
14
 
14
15
  /**
15
16
  * Generate a full Dart file for a PDA.
@@ -23,6 +24,11 @@ export function getPdaPageFragment(
23
24
  const seedsClassName = `${pascalCase(name)}Seeds`;
24
25
 
25
26
  const seeds = node.seeds ?? [];
27
+ const hasByteSeeds = seeds.some(
28
+ (seed) =>
29
+ seed.kind === "constantPdaSeedNode" &&
30
+ seed.value.kind === "bytesValueNode",
31
+ );
26
32
 
27
33
  // Separate variable seeds from constant seeds
28
34
  const variableSeeds = seeds.filter(
@@ -59,9 +65,17 @@ export function getPdaPageFragment(
59
65
  } else if (seed.value.kind === "stringValueNode") {
60
66
  seedValues.push(` '${seed.value.string}',`);
61
67
  } else if (seed.value.kind === "publicKeyValueNode") {
62
- seedValues.push(
63
- ` getAddressEncoder().encode(Address('${seed.value.publicKey}')),`,
64
- );
68
+ const wellKnownSeedName = WELL_KNOWN_ADDRESSES.get(seed.value.publicKey);
69
+ if (wellKnownSeedName) {
70
+ use(wellKnownSeedName, "solanaAddresses");
71
+ seedValues.push(
72
+ ` getAddressEncoder().encode(${wellKnownSeedName}),`,
73
+ );
74
+ } else {
75
+ seedValues.push(
76
+ ` getAddressEncoder().encode(Address('${seed.value.publicKey}')),`,
77
+ );
78
+ }
65
79
  } else if (seed.value.kind === "numberValueNode") {
66
80
  const manifest = visit(seed.type, scope.typeManifestVisitor);
67
81
  seedValues.push(` ${manifest.encoder.content}.encode(${seed.value.number}),`);
@@ -75,15 +89,27 @@ export function getPdaPageFragment(
75
89
  }
76
90
 
77
91
  // Program address parameter
92
+ const wellKnownProgramName = node.programId
93
+ ? WELL_KNOWN_ADDRESSES.get(node.programId)
94
+ : undefined;
95
+ if (wellKnownProgramName) {
96
+ use(wellKnownProgramName, "solanaAddresses");
97
+ }
78
98
  const programIdParam = node.programId
79
- ? `Address programAddress = const Address('${node.programId}')`
99
+ ? wellKnownProgramName
100
+ ? `Address programAddress = ${wellKnownProgramName}`
101
+ : `Address programAddress = const Address('${node.programId}')`
80
102
  : "required Address programAddress";
81
103
 
104
+ const typedDataImport = hasByteSeeds
105
+ ? use("Uint8List", "dartTypedData")
106
+ : fragmentFromString("");
107
+
82
108
  const parts: Fragment[] = [
83
109
  fragment`// Auto-generated. Do not edit.
84
110
  // ignore_for_file: type=lint
85
111
 
86
- ${use("Uint8List", "dartTypedData")}
112
+ ${typedDataImport}
87
113
  ${use("immutable", "meta")}
88
114
  ${use("Address", "solanaAddresses")}
89
115
  ${use("getAddressEncoder", "solanaAddresses")}
@@ -110,13 +136,13 @@ Future<(Address, int)> ${fragmentFromString(findFnName)}({
110
136
  ${fragmentFromString(seedsParam)}
111
137
  ${fragmentFromString(programIdParam)},
112
138
  }) async {
113
- final seeds = <Object>[
139
+ final seedValues = <Object>[
114
140
  ${fragmentFromString(seedValues.join("\n"))}
115
141
  ];
116
142
 
117
143
  return getProgramDerivedAddress(
118
144
  programAddress: programAddress,
119
- seeds: seeds,
145
+ seeds: seedValues,
120
146
  );
121
147
  }`);
122
148
 
@@ -10,6 +10,7 @@ import {
10
10
  } from "../utils/fragment.js";
11
11
  import type { RenderScope } from "../utils/options.js";
12
12
  import { camelCase, pascalCase } from "../utils/nameTransformers.js";
13
+ import { WELL_KNOWN_ADDRESSES } from "../utils/wellKnownAddresses.js";
13
14
 
14
15
  /**
15
16
  * Generate a full Dart file for a program.
@@ -21,15 +22,36 @@ export function getProgramPageFragment(
21
22
  const name = node.name as string;
22
23
  const addressConstName = scope.nameApi.programAddressConstant(name);
23
24
 
24
- const parts: Fragment[] = [
25
- fragment`// Auto-generated. Do not edit.
25
+ const wellKnownName = WELL_KNOWN_ADDRESSES.get(node.publicKey);
26
+
27
+ // For well-known addresses, export and alias the canonical constant from
28
+ // solana_kit_addresses instead of hardcoding the address string.
29
+ // When the generated name matches the canonical name, a simple export suffices.
30
+ // When they differ, we also need a local const alias.
31
+ // For unknown addresses, fall back to the original Address('...') pattern.
32
+ const addressDeclaration: Fragment = wellKnownName
33
+ ? addressConstName === wellKnownName
34
+ ? fragment`// Auto-generated. Do not edit.
35
+ // ignore_for_file: type=lint
36
+
37
+ /// The address of the ${fragmentFromString(pascalCase(name))} program.
38
+ export 'package:solana_kit_addresses/solana_kit_addresses.dart' show ${fragmentFromString(wellKnownName)};`
39
+ : fragment`${use(wellKnownName, "solanaAddresses")}
40
+
41
+ // Auto-generated. Do not edit.
42
+ // ignore_for_file: type=lint
43
+
44
+ /// The address of the ${fragmentFromString(pascalCase(name))} program.
45
+ const ${fragmentFromString(addressConstName)} = ${fragmentFromString(wellKnownName)};`
46
+ : fragment`// Auto-generated. Do not edit.
26
47
  // ignore_for_file: type=lint
27
48
 
28
49
  ${use("Address", "solanaAddresses")}
29
50
 
30
51
  /// The address of the ${fragmentFromString(pascalCase(name))} program.
31
- const ${fragmentFromString(addressConstName)} = Address('${fragmentFromString(node.publicKey)}');`,
32
- ];
52
+ const ${fragmentFromString(addressConstName)} = Address('${fragmentFromString(node.publicKey)}');`;
53
+
54
+ const parts: Fragment[] = [addressDeclaration];
33
55
 
34
56
  // Account identifier enum
35
57
  const accounts = node.accounts ?? [];
@@ -60,4 +82,4 @@ ${fragmentFromString(instrVariants)}
60
82
  }
61
83
 
62
84
  return mergeFragments(parts, (cs) => cs.join("\n"));
63
- }
85
+ }
@@ -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";
@@ -5,3 +5,4 @@ export * from "./importMap.js";
5
5
  export * from "./nameTransformers.js";
6
6
  export * from "./options.js";
7
7
  export * from "./typeManifest.js";
8
+ export * from "./wellKnownAddresses.js";
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Maps well-known Solana public keys to their canonical Dart constant names
3
+ * in `package:solana_kit_addresses`. When the renderer encounters a known
4
+ * address, it emits a re-export from that package instead of hardcoding the
5
+ * address string.
6
+ *
7
+ * The keys are the base-58 public key strings as they appear in IDL/Codama nodes.
8
+ * The values are the Dart constant names exported by `solana_kit_addresses`.
9
+ */
10
+ export const WELL_KNOWN_ADDRESSES: ReadonlyMap<string, string> = new Map([
11
+ // Agave / Solana native programs
12
+ ['11111111111111111111111111111111', 'systemProgramAddress'],
13
+ ['AddressLookupTab1e1111111111111111111111111', 'addressLookupTableProgramAddress'],
14
+ ['BPFLoader2111111111111111111111111111111111', 'bpfLoaderProgramAddress'],
15
+ ['BPFLoader1111111111111111111111111111111111', 'bpfLoaderDeprecatedProgramAddress'],
16
+ ['BPFLoaderUpgradeab1e11111111111111111111111', 'bpfLoaderUpgradeableProgramAddress'],
17
+ ['ComputeBudget111111111111111111111111111111', 'computeBudgetProgramAddress'],
18
+ ['Config1111111111111111111111111111111111111', 'configProgramAddress'],
19
+ ['Ed25519SigVerify111111111111111111111111111', 'ed25519ProgramAddress'],
20
+ ['Feature111111111111111111111111111111111111', 'featureProgramAddress'],
21
+ ['1nc1nerator11111111111111111111111111111111', 'incineratorAddress'],
22
+ ['LoaderV411111111111111111111111111111111111', 'loaderV4ProgramAddress'],
23
+ ['NativeLoader1111111111111111111111111111', 'nativeLoaderProgramAddress'],
24
+ ['KeccakSecp256k11111111111111111111111111111', 'secp256k1ProgramAddress'],
25
+ ['Secp256r1SigVerify1111111111111111111111111', 'secp256r1ProgramAddress'],
26
+ ['Stake11111111111111111111111111111111111111', 'stakeProgramAddress'],
27
+ ['StakeConfig1111111111111111111111111111', 'stakeConfigAddress'],
28
+ ['Vote111111111111111111111111111111111111111', 'voteProgramAddress'],
29
+ ['ZkTokenProof1111111111111111111111111111111', 'zkTokenProofProgramAddress'],
30
+ ['ZkE1Gama1Proof11111111111111111111111111111', 'zkElgamalProofProgramAddress'],
31
+
32
+ // Sysvars
33
+ ['SysvarC1ock11111111111111111111111111111111', 'sysvarClockAddress'],
34
+ ['Rent11111111111111111111111111111111111111', 'sysvarRentAddress'],
35
+ ['11111111111111111111111111111111111111111111', 'sysvarRecentBlockhashesAddress'], // note: 44 chars
36
+ ['SysvarFees1111111111111111111111111111111111', 'sysvarFeesAddress'],
37
+ ['Sysvar1nstruct1ons11111111111111111111111111', 'sysvarInstructionsAddress'],
38
+ ['SysvarEpochSchedu1e111111111111111111111111', 'sysvarEpochScheduleAddress'],
39
+ ['SysvarRewards111111111111111111111111111111', 'sysvarRewardsAddress'],
40
+ ['SysvarStakeHistory1111111111111111111111111', 'sysvarStakeHistoryAddress'],
41
+ ['SysvarS1otHashes111111111111111111111111111', 'sysvarSlotHashesAddress'],
42
+ ['SysvarS1otHistory11111111111111111111111111', 'sysvarSlotHistoryAddress'],
43
+ ['Sysvar1111111111111111111111111111111111111', 'sysvarOwnerAddress'],
44
+
45
+ // SPL programs
46
+ ['TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'tokenProgramAddress'],
47
+ ['TokenzQdBNb4qyze1S1U9AHB8MGXmNK1REkTPT5Z3Y', 'token2022ProgramAddress'],
48
+ ['ATokenGPvbdGVxr1b2hvZbsiqW5xWH25ef7s3c8BnQKu', 'associatedTokenProgramAddress'],
49
+ ['Memo1UhkJRfRhVq1sR7Y7Nto1s3J2mXTT3L6Wk4K2m6', 'memoProgramAddress'],
50
+ ['MemoSq4gqABXKKYWVqKkV3m3kb1cTR7F2UMy9r6kFNK', 'memoLegacyProgramAddress'],
51
+
52
+ // Metaplex programs
53
+ ['metaqbxxU9qX8rZXkmUJ7haCrXXz2W1PSUq1Rn3E1od', 'tokenMetadataProgramAddress'],
54
+ ['BGUMAp9V1mQ2KV8t34L5u3gZsXYZaYDBdKkc1xYk1qA1', 'mplBubblegumProgramAddress'],
55
+ ['auth9SigNpDKz4s8cHr3MFBPS1phbRKLSCN3aBe9sS', 'authRulesProgramAddress'],
56
+ ['CoREENxT6tW1Ho6BW6W5Z4N41ci5Xz2mmrR2kErmM5TR', 'metaplexCoreProgramAddress'],
57
+ ['cmtMqR4bPM9sEV3bPnJQq5s2EnYw4N5vQ8hWhEd1xzS', 'splAccountCompressionProgramAddress'],
58
+ ['noopb9skMVngR5Qi4sczwFr1jU4V4F7nm8xzFQK9C', 'noopProgramAddress'],
59
+
60
+ // Well-known mints
61
+ ['So11111111111111111111111111111111111111112', 'wrappedSolAddress'],
62
+ ['EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', 'usdcAddress'],
63
+ ['Es9vMFrzaCERm6YKXx6bqKr1Rmd9b5v3J8rM5e4rfF4', 'usdtAddress'],
64
+ ]);
@@ -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
+ }