codama-renderers-dart 0.2.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/LICENSE +21 -0
- package/package.json +61 -0
- package/readme.md +297 -0
- package/src/fragments/accountPage.ts +201 -0
- package/src/fragments/discriminatorConstants.ts +74 -0
- package/src/fragments/errorPage.ts +82 -0
- package/src/fragments/index.ts +8 -0
- package/src/fragments/indexPage.ts +44 -0
- package/src/fragments/instructionPage.ts +306 -0
- package/src/fragments/pdaPage.ts +128 -0
- package/src/fragments/programPage.ts +63 -0
- package/src/fragments/typePage.ts +554 -0
- package/src/index.ts +13 -0
- package/src/types/global.d.ts +2 -0
- package/src/utils/codecs.ts +19 -0
- package/src/utils/formatCode.ts +35 -0
- package/src/utils/fragment.ts +83 -0
- package/src/utils/importMap.ts +129 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/nameTransformers.ts +112 -0
- package/src/utils/options.ts +46 -0
- package/src/utils/typeManifest.ts +31 -0
- package/src/visitors/getRenderMapVisitor.ts +272 -0
- package/src/visitors/getTypeManifestVisitor.ts +794 -0
- package/src/visitors/index.ts +3 -0
- package/src/visitors/renderVisitor.ts +154 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ifiok Jr.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codama-renderers-dart",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Codama renderer for generating Dart code targeting the solana_kit SDK",
|
|
5
|
+
"exports": {
|
|
6
|
+
"node": {
|
|
7
|
+
"import": "./dist/index.node.mjs",
|
|
8
|
+
"require": "./dist/index.node.cjs"
|
|
9
|
+
},
|
|
10
|
+
"browser": {
|
|
11
|
+
"import": "./dist/index.browser.mjs",
|
|
12
|
+
"require": "./dist/index.browser.cjs"
|
|
13
|
+
},
|
|
14
|
+
"react-native": "./dist/index.react-native.mjs",
|
|
15
|
+
"types": "./dist/types/index.d.ts"
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/index.node.cjs",
|
|
18
|
+
"module": "./dist/index.node.mjs",
|
|
19
|
+
"types": "./dist/types/index.d.ts",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"src"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"codama",
|
|
27
|
+
"solana",
|
|
28
|
+
"dart",
|
|
29
|
+
"renderer",
|
|
30
|
+
"code-generator"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@codama/errors": "^1.4.4",
|
|
35
|
+
"@codama/nodes": "^1.4.4",
|
|
36
|
+
"@codama/renderers-core": "^1.3.3",
|
|
37
|
+
"@codama/visitors-core": "^1.4.4"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@codama/nodes-from-anchor": "^1.3.9",
|
|
41
|
+
"@codama/renderers-js": "^2.0.2",
|
|
42
|
+
"@types/node": "^25.3.0",
|
|
43
|
+
"rimraf": "^6.0.1",
|
|
44
|
+
"tsup": "^8.4.0",
|
|
45
|
+
"typescript": "^5.7.3",
|
|
46
|
+
"vitest": "^3.2.1"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=18"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "rimraf dist && tsup && tsc -p tsconfig.declarations.json",
|
|
56
|
+
"lint": "eslint --ext .ts src",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"test:watch": "vitest",
|
|
59
|
+
"typecheck": "tsc --noEmit"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# codama-renderers-dart
|
|
2
|
+
|
|
3
|
+
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
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add codama-renderers-dart
|
|
11
|
+
# or
|
|
12
|
+
npm install codama-renderers-dart
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### Programmatic API
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { renderVisitor } from "codama-renderers-dart";
|
|
21
|
+
import { visit } from "@codama/visitors-core";
|
|
22
|
+
import { rootNode, programNode /* ... */ } from "@codama/nodes";
|
|
23
|
+
|
|
24
|
+
// Build or load a Codama IDL root node
|
|
25
|
+
const root = rootNode(programNode({ /* ... */ }));
|
|
26
|
+
|
|
27
|
+
// Generate Dart files into the output directory
|
|
28
|
+
visit(root, renderVisitor("lib/src/generated", {
|
|
29
|
+
formatCode: true, // Run `dart format` on output (default: false)
|
|
30
|
+
deleteFolderBeforeRendering: true, // Clean output dir first (default: true)
|
|
31
|
+
}));
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Codama CLI
|
|
35
|
+
|
|
36
|
+
Create a `codama.json` configuration file:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"idl": "idl.json",
|
|
41
|
+
"scripts": {
|
|
42
|
+
"dart": {
|
|
43
|
+
"from": "codama-renderers-dart",
|
|
44
|
+
"args": ["lib/src/generated"]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then run:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
codama run dart
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Generated Output Structure
|
|
57
|
+
|
|
58
|
+
For a program called `myProgram`, the renderer generates:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
lib/src/generated/
|
|
62
|
+
my_program.dart # Root barrel export
|
|
63
|
+
accounts/
|
|
64
|
+
accounts.dart # Category barrel
|
|
65
|
+
my_account.dart # Account type + codecs + decode helper
|
|
66
|
+
instructions/
|
|
67
|
+
instructions.dart # Category barrel
|
|
68
|
+
my_instruction.dart # Instruction data + builder + parser
|
|
69
|
+
types/
|
|
70
|
+
types.dart # Category barrel
|
|
71
|
+
my_struct.dart # Struct type + codecs
|
|
72
|
+
my_enum.dart # Enum/sealed class + codecs
|
|
73
|
+
errors/
|
|
74
|
+
errors.dart # Category barrel
|
|
75
|
+
my_program.dart # Error constants + message helpers
|
|
76
|
+
programs/
|
|
77
|
+
programs.dart # Category barrel
|
|
78
|
+
my_program.dart # Program address + identifier enums
|
|
79
|
+
pdas/
|
|
80
|
+
pdas.dart # Category barrel
|
|
81
|
+
my_pda.dart # PDA seeds class + finder function
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Generated Code Patterns
|
|
85
|
+
|
|
86
|
+
### Accounts
|
|
87
|
+
|
|
88
|
+
Each account generates an `@immutable` Dart class with typed fields, const constructor, proper `==`/`hashCode`/`toString` implementations, and codec functions:
|
|
89
|
+
|
|
90
|
+
```dart
|
|
91
|
+
@immutable
|
|
92
|
+
class MyAccount {
|
|
93
|
+
const MyAccount({
|
|
94
|
+
required this.authority,
|
|
95
|
+
required this.count,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
final Address authority;
|
|
99
|
+
final BigInt count;
|
|
100
|
+
|
|
101
|
+
// ... equality, hashCode, toString
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
Encoder<MyAccount> getMyAccountEncoder() { ... }
|
|
105
|
+
Decoder<MyAccount> getMyAccountDecoder() { ... }
|
|
106
|
+
Codec<MyAccount, MyAccount> getMyAccountCodec() { ... }
|
|
107
|
+
Account<MyAccount> decodeMyAccount(EncodedAccount encodedAccount) { ... }
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Instructions
|
|
111
|
+
|
|
112
|
+
Each instruction generates a data class, codec functions, a builder function, and a parse function:
|
|
113
|
+
|
|
114
|
+
```dart
|
|
115
|
+
@immutable
|
|
116
|
+
class TransferInstructionData {
|
|
117
|
+
const TransferInstructionData({
|
|
118
|
+
this.discriminator = 3,
|
|
119
|
+
required this.amount,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
final int discriminator;
|
|
123
|
+
final BigInt amount;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
Instruction getTransferInstruction({
|
|
127
|
+
required Address programAddress,
|
|
128
|
+
required Address source,
|
|
129
|
+
required Address destination,
|
|
130
|
+
required BigInt amount,
|
|
131
|
+
}) { ... }
|
|
132
|
+
|
|
133
|
+
TransferInstructionData parseTransferInstruction(Instruction instruction) { ... }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Scalar Enums
|
|
137
|
+
|
|
138
|
+
Scalar enums (all-empty variants) generate a Dart `enum` with index-based encoder/decoder:
|
|
139
|
+
|
|
140
|
+
```dart
|
|
141
|
+
enum AccountStatus {
|
|
142
|
+
active,
|
|
143
|
+
frozen,
|
|
144
|
+
closed,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
Encoder<AccountStatus> getAccountStatusEncoder() { ... }
|
|
148
|
+
Decoder<AccountStatus> getAccountStatusDecoder() { ... }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Data Enums (Discriminated Unions)
|
|
152
|
+
|
|
153
|
+
Data enums generate Dart 3 `sealed class` hierarchies:
|
|
154
|
+
|
|
155
|
+
```dart
|
|
156
|
+
sealed class TokenInstruction {
|
|
157
|
+
const TokenInstruction();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
final class Transfer extends TokenInstruction {
|
|
161
|
+
const Transfer({required this.amount});
|
|
162
|
+
final BigInt amount;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
final class Approve extends TokenInstruction {
|
|
166
|
+
const Approve({required this.amount});
|
|
167
|
+
final BigInt amount;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Errors
|
|
172
|
+
|
|
173
|
+
Program errors generate constants, a message map, and helper functions:
|
|
174
|
+
|
|
175
|
+
```dart
|
|
176
|
+
const int myProgramErrorInvalidAuthority = 0x1770; // 6000
|
|
177
|
+
|
|
178
|
+
String? getMyProgramErrorMessage(int code) { ... }
|
|
179
|
+
bool isMyProgramError(int code) { ... }
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### PDAs
|
|
183
|
+
|
|
184
|
+
PDAs generate a seeds class and finder function:
|
|
185
|
+
|
|
186
|
+
```dart
|
|
187
|
+
@immutable
|
|
188
|
+
class MyPdaSeeds {
|
|
189
|
+
const MyPdaSeeds({required this.authority});
|
|
190
|
+
final Address authority;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
Future<(Address, int)> findMyPdaPda({
|
|
194
|
+
required MyPdaSeeds seeds,
|
|
195
|
+
required Address programAddress,
|
|
196
|
+
}) async { ... }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Type Mapping
|
|
200
|
+
|
|
201
|
+
| Codama Type | Dart Type | Codec |
|
|
202
|
+
| ----------------------------------- | --------------- | -------------------------------- |
|
|
203
|
+
| `numberTypeNode(u8/u16/u32)` | `int` | `getU8Encoder()` etc. |
|
|
204
|
+
| `numberTypeNode(u64/u128/i64/i128)` | `BigInt` | `getU64Encoder()` etc. |
|
|
205
|
+
| `numberTypeNode(f32/f64)` | `double` | `getF32Encoder()` etc. |
|
|
206
|
+
| `booleanTypeNode` | `bool` | `getBooleanEncoder()` |
|
|
207
|
+
| `stringTypeNode` | `String` | `getUtf8Encoder()` etc. |
|
|
208
|
+
| `publicKeyTypeNode` | `Address` | `getAddressEncoder()` |
|
|
209
|
+
| `bytesTypeNode` | `Uint8List` | `getBytesEncoder()` |
|
|
210
|
+
| `arrayTypeNode` | `List<T>` | `getArrayEncoder()` |
|
|
211
|
+
| `mapTypeNode` | `Map<K, V>` | `getMapEncoder()` |
|
|
212
|
+
| `setTypeNode` | `Set<T>` | `getSetEncoder()` |
|
|
213
|
+
| `tupleTypeNode` | `(T1, T2, ...)` | `getTupleEncoder()` |
|
|
214
|
+
| `optionTypeNode` | `T?` | `getNullableEncoder()` |
|
|
215
|
+
| `structTypeNode` | Named class | `getStructEncoder()` |
|
|
216
|
+
| `enumTypeNode` (scalar) | `enum` | `transformEncoder()` |
|
|
217
|
+
| `enumTypeNode` (data) | `sealed class` | `getDiscriminatedUnionEncoder()` |
|
|
218
|
+
|
|
219
|
+
## Options
|
|
220
|
+
|
|
221
|
+
### `RenderOptions`
|
|
222
|
+
|
|
223
|
+
| Option | Type | Default | Description |
|
|
224
|
+
| ----------------------------- | ------------------------ | ------- | --------------------------------------------------- |
|
|
225
|
+
| `deleteFolderBeforeRendering` | `boolean` | `true` | Delete output directory before generating |
|
|
226
|
+
| `formatCode` | `boolean` | `false` | Run `dart format` on generated files |
|
|
227
|
+
| `nameApi` | `Partial<DartNameApi>` | — | Override naming conventions |
|
|
228
|
+
| `dependencyMap` | `Record<string, string>` | — | Override logical module to Dart package URI mapping |
|
|
229
|
+
|
|
230
|
+
### `DartNameApi`
|
|
231
|
+
|
|
232
|
+
All naming conventions are customizable:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { renderVisitor, createDartNameApi } from "codama-renderers-dart";
|
|
236
|
+
|
|
237
|
+
const nameApi = {
|
|
238
|
+
...createDartNameApi(),
|
|
239
|
+
dataType: (name) => `My${pascalCase(name)}`,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
visit(root, renderVisitor("output", { nameApi }));
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Target Packages
|
|
246
|
+
|
|
247
|
+
Generated Dart code depends on these solana_kit packages:
|
|
248
|
+
|
|
249
|
+
- `solana_kit_addresses` — `Address` type and encoder/decoder
|
|
250
|
+
- `solana_kit_codecs_core` — `Encoder`, `Decoder`, `Codec` base types
|
|
251
|
+
- `solana_kit_codecs_data_structures` — Struct, array, boolean, nullable codecs
|
|
252
|
+
- `solana_kit_codecs_numbers` — Number codecs (u8–u128, i8–i128, f32, f64)
|
|
253
|
+
- `solana_kit_codecs_strings` — String codecs (utf8, base58, base64, base16)
|
|
254
|
+
- `solana_kit_accounts` — `Account`, `EncodedAccount`, `decodeAccount`
|
|
255
|
+
- `solana_kit_instructions` — `Instruction`, `AccountMeta`, `AccountRole`
|
|
256
|
+
- `solana_kit_errors` — `SolanaError` error types
|
|
257
|
+
- `meta` — `@immutable` annotation
|
|
258
|
+
|
|
259
|
+
## Architecture
|
|
260
|
+
|
|
261
|
+
This renderer follows the same architecture as the official Codama renderers:
|
|
262
|
+
|
|
263
|
+
1. **Visitor pattern** — Uses `@codama/visitors-core` to traverse the IDL node tree
|
|
264
|
+
2. **Fragment system** — Composable tagged template literals that track imports
|
|
265
|
+
3. **RenderMap** — Maps file paths to code fragments
|
|
266
|
+
4. **Type manifest** — Maps Codama type nodes to Dart types and codec expressions
|
|
267
|
+
|
|
268
|
+
Key source files:
|
|
269
|
+
|
|
270
|
+
- `src/visitors/renderVisitor.ts` — Top-level entry point (file I/O)
|
|
271
|
+
- `src/visitors/getRenderMapVisitor.ts` — Maps IDL nodes to output files
|
|
272
|
+
- `src/visitors/getTypeManifestVisitor.ts` — Maps type nodes to Dart types/codecs
|
|
273
|
+
- `src/fragments/*.ts` — Code generation for each entity type
|
|
274
|
+
- `src/utils/` — Import maps, naming, fragment helpers
|
|
275
|
+
|
|
276
|
+
## Development
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
# Install dependencies
|
|
280
|
+
pnpm install
|
|
281
|
+
|
|
282
|
+
# Type check
|
|
283
|
+
npx tsc --noEmit
|
|
284
|
+
|
|
285
|
+
# Run tests
|
|
286
|
+
pnpm test
|
|
287
|
+
|
|
288
|
+
# Watch mode
|
|
289
|
+
pnpm test:watch
|
|
290
|
+
|
|
291
|
+
# Build
|
|
292
|
+
pnpm build
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## License
|
|
296
|
+
|
|
297
|
+
MIT
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { AccountNode, StructFieldTypeNode } from "@codama/nodes";
|
|
2
|
+
import { resolveNestedTypeNode } from "@codama/nodes";
|
|
3
|
+
import { visit } from "@codama/visitors-core";
|
|
4
|
+
|
|
5
|
+
import type { Fragment } from "../utils/fragment.js";
|
|
6
|
+
import {
|
|
7
|
+
emptyFragment,
|
|
8
|
+
fragment,
|
|
9
|
+
fragmentFromString,
|
|
10
|
+
mergeFragments,
|
|
11
|
+
use,
|
|
12
|
+
} from "../utils/fragment.js";
|
|
13
|
+
import type { RenderScope } from "../utils/options.js";
|
|
14
|
+
import { camelCase } from "../utils/nameTransformers.js";
|
|
15
|
+
import { getDiscriminatorConstantsFragment } from "./discriminatorConstants.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate a full Dart file for an account.
|
|
19
|
+
*/
|
|
20
|
+
export function getAccountPageFragment(
|
|
21
|
+
node: AccountNode,
|
|
22
|
+
scope: RenderScope,
|
|
23
|
+
): Fragment {
|
|
24
|
+
const name = node.name as string;
|
|
25
|
+
const typeName = scope.nameApi.dataType(name);
|
|
26
|
+
const dataNode = resolveNestedTypeNode(node.data);
|
|
27
|
+
|
|
28
|
+
// Fields from the struct data
|
|
29
|
+
const fields = dataNode.fields;
|
|
30
|
+
|
|
31
|
+
// Visit each field type once and collect manifests for reuse
|
|
32
|
+
const fieldManifests = fields.map((f: StructFieldTypeNode) => ({
|
|
33
|
+
field: f,
|
|
34
|
+
manifest: visit(f.type, scope.typeManifestVisitor),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const fieldDecls = fieldManifests
|
|
38
|
+
.map(({ field: f, manifest }) => {
|
|
39
|
+
return ` final ${manifest.type.content} ${camelCase(f.name as string)};`;
|
|
40
|
+
})
|
|
41
|
+
.join("\n");
|
|
42
|
+
|
|
43
|
+
const ctorParams = fields
|
|
44
|
+
.map((f: StructFieldTypeNode) => ` required this.${camelCase(f.name as string)},`)
|
|
45
|
+
.join("\n");
|
|
46
|
+
|
|
47
|
+
const eqChecks =
|
|
48
|
+
fields.length === 0
|
|
49
|
+
? "true"
|
|
50
|
+
: fields
|
|
51
|
+
.map(
|
|
52
|
+
(f: StructFieldTypeNode) =>
|
|
53
|
+
`${camelCase(f.name as string)} == other.${camelCase(f.name as string)}`,
|
|
54
|
+
)
|
|
55
|
+
.join(" &&\n ");
|
|
56
|
+
|
|
57
|
+
const hashFields = fields
|
|
58
|
+
.map((f: StructFieldTypeNode) => camelCase(f.name as string))
|
|
59
|
+
.join(", ");
|
|
60
|
+
|
|
61
|
+
const toStringFields = fields
|
|
62
|
+
.map(
|
|
63
|
+
(f: StructFieldTypeNode) =>
|
|
64
|
+
`${camelCase(f.name as string)}: \$${camelCase(f.name as string)}`,
|
|
65
|
+
)
|
|
66
|
+
.join(", ");
|
|
67
|
+
|
|
68
|
+
// Encoder fields
|
|
69
|
+
const encFields = fieldManifests
|
|
70
|
+
.map(({ field: f, manifest }) => {
|
|
71
|
+
return ` ('${f.name as string}', ${manifest.encoder.content}),`;
|
|
72
|
+
})
|
|
73
|
+
.join("\n");
|
|
74
|
+
|
|
75
|
+
const decFields = fieldManifests
|
|
76
|
+
.map(({ field: f, manifest }) => {
|
|
77
|
+
return ` ('${f.name as string}', ${manifest.decoder.content}),`;
|
|
78
|
+
})
|
|
79
|
+
.join("\n");
|
|
80
|
+
|
|
81
|
+
const toMapFields = fields
|
|
82
|
+
.map(
|
|
83
|
+
(f: StructFieldTypeNode) =>
|
|
84
|
+
` '${f.name as string}': value.${camelCase(f.name as string)},`,
|
|
85
|
+
)
|
|
86
|
+
.join("\n");
|
|
87
|
+
|
|
88
|
+
const fromMapFields = fieldManifests
|
|
89
|
+
.map(({ field: f, manifest }) => {
|
|
90
|
+
const typeStr = manifest.type.content;
|
|
91
|
+
const isNullable = typeStr.endsWith("?");
|
|
92
|
+
const accessor = isNullable ? `map['${f.name as string}']` : `map['${f.name as string}']!`;
|
|
93
|
+
return ` ${camelCase(f.name as string)}: ${accessor} as ${typeStr},`;
|
|
94
|
+
})
|
|
95
|
+
.join("\n");
|
|
96
|
+
|
|
97
|
+
const encoderName = scope.nameApi.encoderFunction(name);
|
|
98
|
+
const decoderName = scope.nameApi.decoderFunction(name);
|
|
99
|
+
const codecName = scope.nameApi.codecFunction(name);
|
|
100
|
+
const decodeFnName = scope.nameApi.accountDecodeFunction(name);
|
|
101
|
+
|
|
102
|
+
// Size
|
|
103
|
+
const sizeFragment = node.size != null
|
|
104
|
+
? fragment`
|
|
105
|
+
/// The size of the [${fragmentFromString(typeName)}] account data in bytes.
|
|
106
|
+
const int ${fragmentFromString(scope.nameApi.accountSizeConstant(name))} = ${fragmentFromString(String(node.size))};`
|
|
107
|
+
: emptyFragment();
|
|
108
|
+
|
|
109
|
+
// Discriminator
|
|
110
|
+
const discFragment = getDiscriminatorConstantsFragment(node, scope);
|
|
111
|
+
|
|
112
|
+
const parts: Fragment[] = [
|
|
113
|
+
fragment`// Auto-generated. Do not edit.
|
|
114
|
+
// ignore_for_file: type=lint
|
|
115
|
+
|
|
116
|
+
${use("Uint8List", "dartTypedData")}
|
|
117
|
+
${use("immutable", "meta")}
|
|
118
|
+
${use("Encoder", "solanaCodecsCore")}
|
|
119
|
+
${use("Decoder", "solanaCodecsCore")}
|
|
120
|
+
${use("Codec", "solanaCodecsCore")}
|
|
121
|
+
${use("combineCodec", "solanaCodecsCore")}
|
|
122
|
+
${use("transformEncoder", "solanaCodecsCore")}
|
|
123
|
+
${use("transformDecoder", "solanaCodecsCore")}
|
|
124
|
+
${use("getStructEncoder", "solanaCodecsDataStructures")}
|
|
125
|
+
${use("getStructDecoder", "solanaCodecsDataStructures")}
|
|
126
|
+
${use("Account", "solanaAccounts")}
|
|
127
|
+
${use("EncodedAccount", "solanaAccounts")}
|
|
128
|
+
${use("decodeAccount", "solanaAccounts")}
|
|
129
|
+
|
|
130
|
+
@immutable
|
|
131
|
+
class ${fragmentFromString(typeName)} {
|
|
132
|
+
const ${fragmentFromString(typeName)}({
|
|
133
|
+
${fragmentFromString(ctorParams)}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
${fragmentFromString(fieldDecls)}
|
|
137
|
+
|
|
138
|
+
@override
|
|
139
|
+
bool operator ==(Object other) =>
|
|
140
|
+
identical(this, other) ||
|
|
141
|
+
other is ${fragmentFromString(typeName)} &&
|
|
142
|
+
runtimeType == other.runtimeType &&
|
|
143
|
+
${fragmentFromString(eqChecks)};
|
|
144
|
+
|
|
145
|
+
@override
|
|
146
|
+
int get hashCode => Object.hash(${fragmentFromString(hashFields)});
|
|
147
|
+
|
|
148
|
+
@override
|
|
149
|
+
String toString() => '${fragmentFromString(typeName)}(${fragmentFromString(toStringFields)})';
|
|
150
|
+
}`,
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
if (sizeFragment.content) parts.push(sizeFragment);
|
|
154
|
+
if (discFragment.content) parts.push(discFragment);
|
|
155
|
+
|
|
156
|
+
parts.push(fragment`
|
|
157
|
+
Encoder<${fragmentFromString(typeName)}> ${fragmentFromString(encoderName)}() {
|
|
158
|
+
final structEncoder = getStructEncoder(<(String, Encoder<Object?>)>[
|
|
159
|
+
${fragmentFromString(encFields)}
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
return transformEncoder(
|
|
163
|
+
structEncoder,
|
|
164
|
+
(${fragmentFromString(typeName)} value) => <String, Object?>{
|
|
165
|
+
${fragmentFromString(toMapFields)}
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
Decoder<${fragmentFromString(typeName)}> ${fragmentFromString(decoderName)}() {
|
|
171
|
+
final structDecoder = getStructDecoder(<(String, Decoder<Object?>)>[
|
|
172
|
+
${fragmentFromString(decFields)}
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
return transformDecoder(
|
|
176
|
+
structDecoder,
|
|
177
|
+
(Map<String, Object?> map, Uint8List bytes, int offset) => ${fragmentFromString(typeName)}(
|
|
178
|
+
${fragmentFromString(fromMapFields)}
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
Codec<${fragmentFromString(typeName)}, ${fragmentFromString(typeName)}> ${fragmentFromString(codecName)}() {
|
|
184
|
+
return combineCodec(${fragmentFromString(encoderName)}(), ${fragmentFromString(decoderName)}());
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
Account<${fragmentFromString(typeName)}> ${fragmentFromString(decodeFnName)}(EncodedAccount encodedAccount) {
|
|
188
|
+
return decodeAccount(encodedAccount, ${fragmentFromString(decoderName)}());
|
|
189
|
+
}`);
|
|
190
|
+
|
|
191
|
+
const result = mergeFragments(parts, (cs) => cs.join("\n\n"));
|
|
192
|
+
|
|
193
|
+
// Merge field type manifest imports (encoder, decoder, type) into the result
|
|
194
|
+
for (const { manifest } of fieldManifests) {
|
|
195
|
+
result.imports.mergeWith(manifest.encoder.imports);
|
|
196
|
+
result.imports.mergeWith(manifest.decoder.imports);
|
|
197
|
+
result.imports.mergeWith(manifest.type.imports);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccountNode,
|
|
3
|
+
ConstantDiscriminatorNode,
|
|
4
|
+
FieldDiscriminatorNode,
|
|
5
|
+
InstructionNode,
|
|
6
|
+
SizeDiscriminatorNode,
|
|
7
|
+
} from "@codama/nodes";
|
|
8
|
+
import { visit } from "@codama/visitors-core";
|
|
9
|
+
|
|
10
|
+
import type { Fragment } from "../utils/fragment.js";
|
|
11
|
+
import { emptyFragment, fragment, fragmentFromString, mergeFragments, use } from "../utils/fragment.js";
|
|
12
|
+
import type { RenderScope } from "../utils/options.js";
|
|
13
|
+
import { bytesToDartHexList } from "../utils/codecs.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate discriminator constant declarations for an account or instruction.
|
|
17
|
+
*/
|
|
18
|
+
export function getDiscriminatorConstantsFragment(
|
|
19
|
+
node: AccountNode | InstructionNode,
|
|
20
|
+
scope: RenderScope,
|
|
21
|
+
): Fragment {
|
|
22
|
+
const discriminators = node.discriminators ?? [];
|
|
23
|
+
if (discriminators.length === 0) return emptyFragment();
|
|
24
|
+
|
|
25
|
+
const name = node.name as string;
|
|
26
|
+
const constName = scope.nameApi.discriminatorConstant(name);
|
|
27
|
+
|
|
28
|
+
const fragments: Fragment[] = [];
|
|
29
|
+
|
|
30
|
+
for (const disc of discriminators) {
|
|
31
|
+
switch (disc.kind) {
|
|
32
|
+
case "constantDiscriminatorNode": {
|
|
33
|
+
const constDisc = disc as ConstantDiscriminatorNode;
|
|
34
|
+
if (constDisc.constant.value.kind === "bytesValueNode") {
|
|
35
|
+
const bytes = hexToBytes(constDisc.constant.value.data);
|
|
36
|
+
const hexList = bytesToDartHexList(bytes);
|
|
37
|
+
fragments.push(
|
|
38
|
+
fragment`/// The discriminator bytes for this ${node.kind === "accountNode" ? "account" : "instruction"}.
|
|
39
|
+
final ${constName} = ${use("Uint8List", "dartTypedData")}.fromList(${fragmentFromString(hexList)});`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case "fieldDiscriminatorNode": {
|
|
45
|
+
const fieldDisc = disc as FieldDiscriminatorNode;
|
|
46
|
+
fragments.push(
|
|
47
|
+
fragment`/// The discriminator field name: '${fragmentFromString(fieldDisc.name as string)}'.
|
|
48
|
+
/// Offset: ${fragmentFromString(String(fieldDisc.offset))}.`,
|
|
49
|
+
);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
case "sizeDiscriminatorNode": {
|
|
53
|
+
const sizeDisc = disc as SizeDiscriminatorNode;
|
|
54
|
+
fragments.push(
|
|
55
|
+
fragment`/// This ${node.kind === "accountNode" ? "account" : "instruction"} has a size discriminator of ${fragmentFromString(String(sizeDisc.size))} bytes.`,
|
|
56
|
+
);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (fragments.length === 0) return emptyFragment();
|
|
63
|
+
|
|
64
|
+
return mergeFragments(fragments, (cs) => cs.join("\n\n"));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hexToBytes(hex: string): Uint8Array {
|
|
68
|
+
const clean = hex.replace(/^0x/, "");
|
|
69
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
70
|
+
for (let i = 0; i < clean.length; i += 2) {
|
|
71
|
+
bytes[i / 2] = parseInt(clean.slice(i, i + 2), 16);
|
|
72
|
+
}
|
|
73
|
+
return bytes;
|
|
74
|
+
}
|