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
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { ErrorNode, ProgramNode } from "@codama/nodes";
|
|
2
|
+
|
|
3
|
+
import type { Fragment } from "../utils/fragment.js";
|
|
4
|
+
import {
|
|
5
|
+
emptyFragment,
|
|
6
|
+
fragment,
|
|
7
|
+
fragmentFromString,
|
|
8
|
+
mergeFragments,
|
|
9
|
+
use,
|
|
10
|
+
} from "../utils/fragment.js";
|
|
11
|
+
import type { RenderScope } from "../utils/options.js";
|
|
12
|
+
import { camelCase, pascalCase, screamingSnakeCase } from "../utils/nameTransformers.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate a full Dart file for a program's errors.
|
|
16
|
+
*/
|
|
17
|
+
export function getErrorPageFragment(
|
|
18
|
+
programNode: ProgramNode,
|
|
19
|
+
scope: RenderScope,
|
|
20
|
+
): Fragment {
|
|
21
|
+
const errors = programNode.errors ?? [];
|
|
22
|
+
if (errors.length === 0) return emptyFragment();
|
|
23
|
+
|
|
24
|
+
const programName = programNode.name as string;
|
|
25
|
+
|
|
26
|
+
// Error code constants
|
|
27
|
+
const errorConstants = errors
|
|
28
|
+
.map((err) => {
|
|
29
|
+
const constName = scope.nameApi.errorConstant(
|
|
30
|
+
programName,
|
|
31
|
+
err.name as string,
|
|
32
|
+
);
|
|
33
|
+
const hexCode = `0x${err.code.toString(16)}`;
|
|
34
|
+
const docs = err.docs?.length
|
|
35
|
+
? err.docs.map((d) => `/// ${d}`).join("\n") + "\n"
|
|
36
|
+
: "";
|
|
37
|
+
const message = err.message ? `/// Message: "${err.message}"\n` : "";
|
|
38
|
+
return `${docs}${message}const int ${constName} = ${hexCode}; // ${err.code}`;
|
|
39
|
+
})
|
|
40
|
+
.join("\n\n");
|
|
41
|
+
|
|
42
|
+
// Error message map
|
|
43
|
+
const messageEntries = errors
|
|
44
|
+
.map((err) => {
|
|
45
|
+
const constName = scope.nameApi.errorConstant(
|
|
46
|
+
programName,
|
|
47
|
+
err.name as string,
|
|
48
|
+
);
|
|
49
|
+
const message = err.message ?? err.name as string;
|
|
50
|
+
return ` ${constName}: '${escapeString(message)}',`;
|
|
51
|
+
})
|
|
52
|
+
.join("\n");
|
|
53
|
+
|
|
54
|
+
const errorMessageFnName = scope.nameApi.errorMessageFunction(programName);
|
|
55
|
+
const isErrorFnName = `is${pascalCase(programName)}Error`;
|
|
56
|
+
|
|
57
|
+
return fragment`// Auto-generated. Do not edit.
|
|
58
|
+
// ignore_for_file: type=lint, constant_identifier_names
|
|
59
|
+
|
|
60
|
+
/// Error codes for the ${fragmentFromString(pascalCase(programName))} program.
|
|
61
|
+
|
|
62
|
+
${fragmentFromString(errorConstants)}
|
|
63
|
+
|
|
64
|
+
/// Map of error codes to human-readable messages.
|
|
65
|
+
const Map<int, String> _${fragmentFromString(camelCase(programName))}ErrorMessages = {
|
|
66
|
+
${fragmentFromString(messageEntries)}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/// Get the error message for a ${fragmentFromString(pascalCase(programName))} program error code.
|
|
70
|
+
String? ${fragmentFromString(errorMessageFnName)}(int code) {
|
|
71
|
+
return _${fragmentFromString(camelCase(programName))}ErrorMessages[code];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Check if an error code belongs to the ${fragmentFromString(pascalCase(programName))} program.
|
|
75
|
+
bool ${fragmentFromString(isErrorFnName)}(int code) {
|
|
76
|
+
return _${fragmentFromString(camelCase(programName))}ErrorMessages.containsKey(code);
|
|
77
|
+
}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function escapeString(str: string): string {
|
|
81
|
+
return str.replace(/'/g, "\\'").replace(/\n/g, "\\n");
|
|
82
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./accountPage.js";
|
|
2
|
+
export * from "./discriminatorConstants.js";
|
|
3
|
+
export * from "./errorPage.js";
|
|
4
|
+
export * from "./indexPage.js";
|
|
5
|
+
export * from "./instructionPage.js";
|
|
6
|
+
export * from "./pdaPage.js";
|
|
7
|
+
export * from "./programPage.js";
|
|
8
|
+
export * from "./typePage.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Fragment } from "../utils/fragment.js";
|
|
2
|
+
import { fragmentFromString } from "../utils/fragment.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generate a barrel export Dart file that exports all files in a category.
|
|
6
|
+
*/
|
|
7
|
+
export function getIndexPageFragment(
|
|
8
|
+
fileNames: string[],
|
|
9
|
+
relativePath: string = "",
|
|
10
|
+
): Fragment {
|
|
11
|
+
if (fileNames.length === 0) {
|
|
12
|
+
return fragmentFromString(
|
|
13
|
+
"// Auto-generated. Do not edit.\n// ignore_for_file: type=lint\n",
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const exports = fileNames
|
|
18
|
+
.sort()
|
|
19
|
+
.map((f) => {
|
|
20
|
+
const path = relativePath ? `${relativePath}/${f}` : f;
|
|
21
|
+
return `export '${path}';`;
|
|
22
|
+
})
|
|
23
|
+
.join("\n");
|
|
24
|
+
|
|
25
|
+
return fragmentFromString(
|
|
26
|
+
`// Auto-generated. Do not edit.\n// ignore_for_file: type=lint\n\n${exports}\n`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Generate the root barrel export file.
|
|
32
|
+
*/
|
|
33
|
+
export function getRootIndexPageFragment(
|
|
34
|
+
categories: string[],
|
|
35
|
+
): Fragment {
|
|
36
|
+
const exports = categories
|
|
37
|
+
.sort()
|
|
38
|
+
.map((cat) => `export '${cat}/${cat}.dart';`)
|
|
39
|
+
.join("\n");
|
|
40
|
+
|
|
41
|
+
return fragmentFromString(
|
|
42
|
+
`// Auto-generated. Do not edit.\n// ignore_for_file: type=lint\n\n${exports}\n`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InstructionAccountNode,
|
|
3
|
+
InstructionArgumentNode,
|
|
4
|
+
InstructionNode,
|
|
5
|
+
} from "@codama/nodes";
|
|
6
|
+
import { visit } from "@codama/visitors-core";
|
|
7
|
+
|
|
8
|
+
import type { Fragment } from "../utils/fragment.js";
|
|
9
|
+
import {
|
|
10
|
+
emptyFragment,
|
|
11
|
+
fragment,
|
|
12
|
+
fragmentFromString,
|
|
13
|
+
mergeFragments,
|
|
14
|
+
use,
|
|
15
|
+
} from "../utils/fragment.js";
|
|
16
|
+
import type { RenderScope } from "../utils/options.js";
|
|
17
|
+
import { camelCase, pascalCase } from "../utils/nameTransformers.js";
|
|
18
|
+
import { getDiscriminatorConstantsFragment } from "./discriminatorConstants.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate a full Dart file for an instruction.
|
|
22
|
+
*/
|
|
23
|
+
export function getInstructionPageFragment(
|
|
24
|
+
node: InstructionNode,
|
|
25
|
+
scope: RenderScope,
|
|
26
|
+
): Fragment {
|
|
27
|
+
const name = node.name as string;
|
|
28
|
+
const typeName = scope.nameApi.dataType(name);
|
|
29
|
+
const instrFnName = scope.nameApi.instructionFunction(name);
|
|
30
|
+
const parseFnName = scope.nameApi.instructionParseFunction(name);
|
|
31
|
+
|
|
32
|
+
const accounts = node.accounts ?? [];
|
|
33
|
+
const args = (node.arguments ?? []).filter(
|
|
34
|
+
(arg) => !isDiscriminatorArg(arg, node),
|
|
35
|
+
);
|
|
36
|
+
const allArgs = node.arguments ?? [];
|
|
37
|
+
|
|
38
|
+
// Build the instruction data class
|
|
39
|
+
const dataClassName = `${typeName}InstructionData`;
|
|
40
|
+
|
|
41
|
+
// Visit each arg type once and collect manifests for reuse
|
|
42
|
+
const allArgManifests = allArgs.map((arg) => ({
|
|
43
|
+
arg,
|
|
44
|
+
manifest: visit(arg.type, scope.typeManifestVisitor),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
// Data fields (non-discriminator arguments)
|
|
48
|
+
const dataFieldDecls = allArgManifests
|
|
49
|
+
.map(({ arg, manifest }) => {
|
|
50
|
+
const fieldName = camelCase(arg.name as string);
|
|
51
|
+
return ` final ${manifest.type.content} ${fieldName};`;
|
|
52
|
+
})
|
|
53
|
+
.join("\n");
|
|
54
|
+
|
|
55
|
+
const dataCtorParams = allArgManifests
|
|
56
|
+
.map(({ arg }) => {
|
|
57
|
+
const fieldName = camelCase(arg.name as string);
|
|
58
|
+
if (isDiscriminatorArg(arg, node)) {
|
|
59
|
+
return ` this.${fieldName} = ${getDiscriminatorDefault(arg, node)},`;
|
|
60
|
+
}
|
|
61
|
+
return ` required this.${fieldName},`;
|
|
62
|
+
})
|
|
63
|
+
.join("\n");
|
|
64
|
+
|
|
65
|
+
// Encoder/decoder for instruction data
|
|
66
|
+
const encFields = allArgManifests
|
|
67
|
+
.map(({ arg, manifest }) => {
|
|
68
|
+
return ` ('${arg.name as string}', ${manifest.encoder.content}),`;
|
|
69
|
+
})
|
|
70
|
+
.join("\n");
|
|
71
|
+
|
|
72
|
+
const decFields = allArgManifests
|
|
73
|
+
.map(({ arg, manifest }) => {
|
|
74
|
+
return ` ('${arg.name as string}', ${manifest.decoder.content}),`;
|
|
75
|
+
})
|
|
76
|
+
.join("\n");
|
|
77
|
+
|
|
78
|
+
const toMapFields = allArgs
|
|
79
|
+
.map(
|
|
80
|
+
(arg) =>
|
|
81
|
+
` '${arg.name as string}': value.${camelCase(arg.name as string)},`,
|
|
82
|
+
)
|
|
83
|
+
.join("\n");
|
|
84
|
+
|
|
85
|
+
const fromMapFields = allArgManifests
|
|
86
|
+
.map(({ arg, manifest }) => {
|
|
87
|
+
const typeStr = manifest.type.content;
|
|
88
|
+
const isNullable = typeStr.endsWith("?");
|
|
89
|
+
const accessor = isNullable ? `map['${arg.name as string}']` : `map['${arg.name as string}']!`;
|
|
90
|
+
return ` ${camelCase(arg.name as string)}: ${accessor} as ${typeStr},`;
|
|
91
|
+
})
|
|
92
|
+
.join("\n");
|
|
93
|
+
|
|
94
|
+
const dataEncoderName = `get${typeName}InstructionDataEncoder`;
|
|
95
|
+
const dataDecoderName = `get${typeName}InstructionDataDecoder`;
|
|
96
|
+
const dataCodecName = `get${typeName}InstructionDataCodec`;
|
|
97
|
+
|
|
98
|
+
// Build the instruction builder function
|
|
99
|
+
const accountParams = accounts
|
|
100
|
+
.map((acc) => {
|
|
101
|
+
const fieldName = camelCase(acc.name as string);
|
|
102
|
+
const isRequired = !(acc.isOptional ?? false);
|
|
103
|
+
return isRequired
|
|
104
|
+
? ` required Address ${fieldName},`
|
|
105
|
+
: ` Address? ${fieldName},`;
|
|
106
|
+
})
|
|
107
|
+
.join("\n");
|
|
108
|
+
|
|
109
|
+
// Build argParams using manifests from the allArgManifests (filtered to non-discriminator args)
|
|
110
|
+
const argManifestMap = new Map(allArgManifests.map(({ arg, manifest }) => [arg, manifest]));
|
|
111
|
+
const argParams = args
|
|
112
|
+
.map((arg) => {
|
|
113
|
+
const manifest = argManifestMap.get(arg)!;
|
|
114
|
+
const fieldName = camelCase(arg.name as string);
|
|
115
|
+
const hasDefault = arg.defaultValue != null;
|
|
116
|
+
if (hasDefault) {
|
|
117
|
+
return ` ${manifest.type.content}? ${fieldName},`;
|
|
118
|
+
}
|
|
119
|
+
return ` required ${manifest.type.content} ${fieldName},`;
|
|
120
|
+
})
|
|
121
|
+
.join("\n");
|
|
122
|
+
|
|
123
|
+
// Account metas
|
|
124
|
+
const accountMetas = accounts
|
|
125
|
+
.map((acc) => {
|
|
126
|
+
const fieldName = camelCase(acc.name as string);
|
|
127
|
+
const role = getAccountRole(acc);
|
|
128
|
+
const isOptional = acc.isOptional ?? false;
|
|
129
|
+
if (isOptional) {
|
|
130
|
+
return ` if (${fieldName} != null) AccountMeta(address: ${fieldName}, role: ${role}),`;
|
|
131
|
+
}
|
|
132
|
+
return ` AccountMeta(address: ${fieldName}, role: ${role}),`;
|
|
133
|
+
})
|
|
134
|
+
.join("\n");
|
|
135
|
+
|
|
136
|
+
// Instruction data construction
|
|
137
|
+
const dataConstruction = allArgs
|
|
138
|
+
.map((arg) => {
|
|
139
|
+
const fieldName = camelCase(arg.name as string);
|
|
140
|
+
if (isDiscriminatorArg(arg, node)) {
|
|
141
|
+
return ""; // Use default
|
|
142
|
+
}
|
|
143
|
+
return ` ${fieldName}: ${fieldName}${arg.defaultValue != null ? ` ?? ${getDefaultValue(arg)}` : ""},`;
|
|
144
|
+
})
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
.join("\n");
|
|
147
|
+
|
|
148
|
+
// Discriminator
|
|
149
|
+
const discFragment = getDiscriminatorConstantsFragment(node, scope);
|
|
150
|
+
|
|
151
|
+
const parts: Fragment[] = [
|
|
152
|
+
fragment`// Auto-generated. Do not edit.
|
|
153
|
+
// ignore_for_file: type=lint
|
|
154
|
+
|
|
155
|
+
${use("Uint8List", "dartTypedData")}
|
|
156
|
+
${use("immutable", "meta")}
|
|
157
|
+
${use("Encoder", "solanaCodecsCore")}
|
|
158
|
+
${use("Decoder", "solanaCodecsCore")}
|
|
159
|
+
${use("Codec", "solanaCodecsCore")}
|
|
160
|
+
${use("combineCodec", "solanaCodecsCore")}
|
|
161
|
+
${use("transformEncoder", "solanaCodecsCore")}
|
|
162
|
+
${use("transformDecoder", "solanaCodecsCore")}
|
|
163
|
+
${use("getStructEncoder", "solanaCodecsDataStructures")}
|
|
164
|
+
${use("getStructDecoder", "solanaCodecsDataStructures")}
|
|
165
|
+
${use("Address", "solanaAddresses")}
|
|
166
|
+
${use("Instruction", "solanaInstructions")}
|
|
167
|
+
${use("AccountMeta", "solanaInstructions")}
|
|
168
|
+
${use("AccountRole", "solanaInstructions")}`,
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
if (discFragment.content) parts.push(discFragment);
|
|
172
|
+
|
|
173
|
+
// Data class
|
|
174
|
+
parts.push(fragment`
|
|
175
|
+
@immutable
|
|
176
|
+
class ${fragmentFromString(dataClassName)} {
|
|
177
|
+
const ${fragmentFromString(dataClassName)}({
|
|
178
|
+
${fragmentFromString(dataCtorParams)}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
${fragmentFromString(dataFieldDecls)}
|
|
182
|
+
}`);
|
|
183
|
+
|
|
184
|
+
// Data encoder/decoder/codec
|
|
185
|
+
parts.push(fragment`
|
|
186
|
+
Encoder<${fragmentFromString(dataClassName)}> ${fragmentFromString(dataEncoderName)}() {
|
|
187
|
+
final structEncoder = getStructEncoder(<(String, Encoder<Object?>)>[
|
|
188
|
+
${fragmentFromString(encFields)}
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
return transformEncoder(
|
|
192
|
+
structEncoder,
|
|
193
|
+
(${fragmentFromString(dataClassName)} value) => <String, Object?>{
|
|
194
|
+
${fragmentFromString(toMapFields)}
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
Decoder<${fragmentFromString(dataClassName)}> ${fragmentFromString(dataDecoderName)}() {
|
|
200
|
+
final structDecoder = getStructDecoder(<(String, Decoder<Object?>)>[
|
|
201
|
+
${fragmentFromString(decFields)}
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
return transformDecoder(
|
|
205
|
+
structDecoder,
|
|
206
|
+
(Map<String, Object?> map, Uint8List bytes, int offset) => ${fragmentFromString(dataClassName)}(
|
|
207
|
+
${fragmentFromString(fromMapFields)}
|
|
208
|
+
),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
Codec<${fragmentFromString(dataClassName)}, ${fragmentFromString(dataClassName)}> ${fragmentFromString(dataCodecName)}() {
|
|
213
|
+
return combineCodec(${fragmentFromString(dataEncoderName)}(), ${fragmentFromString(dataDecoderName)}());
|
|
214
|
+
}`);
|
|
215
|
+
|
|
216
|
+
// Instruction builder
|
|
217
|
+
parts.push(fragment`
|
|
218
|
+
/// Creates a [${fragmentFromString(typeName)}] instruction.
|
|
219
|
+
Instruction ${fragmentFromString(instrFnName)}({
|
|
220
|
+
required Address programAddress,
|
|
221
|
+
${fragmentFromString(accountParams)}
|
|
222
|
+
${fragmentFromString(argParams)}
|
|
223
|
+
}) {
|
|
224
|
+
final data = ${fragmentFromString(dataClassName)}(
|
|
225
|
+
${fragmentFromString(dataConstruction)}
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
return Instruction(
|
|
229
|
+
programAddress: programAddress,
|
|
230
|
+
accounts: [
|
|
231
|
+
${fragmentFromString(accountMetas)}
|
|
232
|
+
],
|
|
233
|
+
data: ${fragmentFromString(dataEncoderName)}().encode(data),
|
|
234
|
+
);
|
|
235
|
+
}`);
|
|
236
|
+
|
|
237
|
+
// Parse function
|
|
238
|
+
parts.push(fragment`
|
|
239
|
+
/// Parses a [${fragmentFromString(typeName)}] instruction from raw instruction data.
|
|
240
|
+
${fragmentFromString(dataClassName)} ${fragmentFromString(parseFnName)}(Instruction instruction) {
|
|
241
|
+
return ${fragmentFromString(dataDecoderName)}().decode(instruction.data!);
|
|
242
|
+
}`);
|
|
243
|
+
|
|
244
|
+
const result = mergeFragments(parts, (cs) => cs.join("\n"));
|
|
245
|
+
|
|
246
|
+
// Merge arg type manifest imports (encoder, decoder, type) into the result
|
|
247
|
+
for (const { manifest } of allArgManifests) {
|
|
248
|
+
result.imports.mergeWith(manifest.encoder.imports);
|
|
249
|
+
result.imports.mergeWith(manifest.decoder.imports);
|
|
250
|
+
result.imports.mergeWith(manifest.type.imports);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function getAccountRole(acc: InstructionAccountNode): string {
|
|
257
|
+
const isSigner = acc.isSigner === true || acc.isSigner === "either";
|
|
258
|
+
const isWritable = acc.isWritable ?? false;
|
|
259
|
+
|
|
260
|
+
if (isSigner && isWritable) return "AccountRole.writableSigner";
|
|
261
|
+
if (isSigner) return "AccountRole.readonlySigner";
|
|
262
|
+
if (isWritable) return "AccountRole.writable";
|
|
263
|
+
return "AccountRole.readonly";
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function isDiscriminatorArg(
|
|
267
|
+
arg: InstructionArgumentNode,
|
|
268
|
+
node: InstructionNode,
|
|
269
|
+
): boolean {
|
|
270
|
+
const discriminators = node.discriminators ?? [];
|
|
271
|
+
return discriminators.some(
|
|
272
|
+
(d) =>
|
|
273
|
+
d.kind === "fieldDiscriminatorNode" && d.name === arg.name,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function getDiscriminatorDefault(
|
|
278
|
+
arg: InstructionArgumentNode,
|
|
279
|
+
_node: InstructionNode,
|
|
280
|
+
): string {
|
|
281
|
+
if (arg.defaultValue) {
|
|
282
|
+
if (arg.defaultValue.kind === "numberValueNode") {
|
|
283
|
+
return String(arg.defaultValue.number);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return "0";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getDefaultValue(arg: InstructionArgumentNode): string {
|
|
290
|
+
if (!arg.defaultValue) return "null";
|
|
291
|
+
const dv = arg.defaultValue;
|
|
292
|
+
switch (dv.kind) {
|
|
293
|
+
case "numberValueNode":
|
|
294
|
+
return String(dv.number);
|
|
295
|
+
case "booleanValueNode":
|
|
296
|
+
return String(dv.boolean);
|
|
297
|
+
case "stringValueNode":
|
|
298
|
+
return `'${dv.string}'`;
|
|
299
|
+
case "publicKeyValueNode":
|
|
300
|
+
return `Address('${dv.publicKey}')`;
|
|
301
|
+
case "noneValueNode":
|
|
302
|
+
return "null";
|
|
303
|
+
default:
|
|
304
|
+
return "null";
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { PdaNode } from "@codama/nodes";
|
|
2
|
+
import { visit } from "@codama/visitors-core";
|
|
3
|
+
|
|
4
|
+
import type { Fragment } from "../utils/fragment.js";
|
|
5
|
+
import {
|
|
6
|
+
fragment,
|
|
7
|
+
fragmentFromString,
|
|
8
|
+
mergeFragments,
|
|
9
|
+
use,
|
|
10
|
+
} from "../utils/fragment.js";
|
|
11
|
+
import type { RenderScope } from "../utils/options.js";
|
|
12
|
+
import { camelCase, pascalCase } from "../utils/nameTransformers.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate a full Dart file for a PDA.
|
|
16
|
+
*/
|
|
17
|
+
export function getPdaPageFragment(
|
|
18
|
+
node: PdaNode,
|
|
19
|
+
scope: RenderScope,
|
|
20
|
+
): Fragment {
|
|
21
|
+
const name = node.name as string;
|
|
22
|
+
const findFnName = scope.nameApi.pdaFindFunction(name);
|
|
23
|
+
const seedsClassName = `${pascalCase(name)}Seeds`;
|
|
24
|
+
|
|
25
|
+
const seeds = node.seeds ?? [];
|
|
26
|
+
|
|
27
|
+
// Separate variable seeds from constant seeds
|
|
28
|
+
const variableSeeds = seeds.filter(
|
|
29
|
+
(s) => s.kind === "variablePdaSeedNode",
|
|
30
|
+
);
|
|
31
|
+
const constantSeeds = seeds.filter(
|
|
32
|
+
(s) => s.kind === "constantPdaSeedNode",
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// Build seeds class if there are variable seeds
|
|
36
|
+
const hasSeedsClass = variableSeeds.length > 0;
|
|
37
|
+
|
|
38
|
+
const seedFieldDecls = variableSeeds
|
|
39
|
+
.map((s) => {
|
|
40
|
+
if (s.kind !== "variablePdaSeedNode") return "";
|
|
41
|
+
const manifest = visit(s.type, scope.typeManifestVisitor);
|
|
42
|
+
return ` final ${manifest.type.content} ${camelCase(s.name as string)};`;
|
|
43
|
+
})
|
|
44
|
+
.join("\n");
|
|
45
|
+
|
|
46
|
+
const seedCtorParams = variableSeeds
|
|
47
|
+
.map((s) => {
|
|
48
|
+
if (s.kind !== "variablePdaSeedNode") return "";
|
|
49
|
+
return ` required this.${camelCase(s.name as string)},`;
|
|
50
|
+
})
|
|
51
|
+
.join("\n");
|
|
52
|
+
|
|
53
|
+
// Build the seed bytes list
|
|
54
|
+
const seedBytesList: string[] = [];
|
|
55
|
+
for (const seed of seeds) {
|
|
56
|
+
if (seed.kind === "constantPdaSeedNode") {
|
|
57
|
+
if (seed.value.kind === "bytesValueNode") {
|
|
58
|
+
seedBytesList.push(` ...${bytesValueToDart(seed.value.data)},`);
|
|
59
|
+
} else if (seed.value.kind === "stringValueNode") {
|
|
60
|
+
seedBytesList.push(` ...utf8.encode('${seed.value.string}'),`);
|
|
61
|
+
} else if (seed.value.kind === "publicKeyValueNode") {
|
|
62
|
+
seedBytesList.push(` ...getAddressEncoder().encode(Address('${seed.value.publicKey}')),`);
|
|
63
|
+
} else if (seed.value.kind === "numberValueNode") {
|
|
64
|
+
const manifest = visit(seed.type, scope.typeManifestVisitor);
|
|
65
|
+
seedBytesList.push(` ...${manifest.encoder.content}.encode(${seed.value.number}),`);
|
|
66
|
+
}
|
|
67
|
+
} else if (seed.kind === "variablePdaSeedNode") {
|
|
68
|
+
const manifest = visit(seed.type, scope.typeManifestVisitor);
|
|
69
|
+
seedBytesList.push(
|
|
70
|
+
` ...${manifest.encoder.content}.encode(seeds.${camelCase(seed.name as string)}),`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Program address parameter
|
|
76
|
+
const programIdParam = node.programId
|
|
77
|
+
? `Address programAddress = const Address('${node.programId}')`
|
|
78
|
+
: "required Address programAddress";
|
|
79
|
+
|
|
80
|
+
const parts: Fragment[] = [
|
|
81
|
+
fragment`// Auto-generated. Do not edit.
|
|
82
|
+
// ignore_for_file: type=lint
|
|
83
|
+
|
|
84
|
+
${use("Uint8List", "dartTypedData")}
|
|
85
|
+
${use("immutable", "meta")}
|
|
86
|
+
${use("Address", "solanaAddresses")}
|
|
87
|
+
${use("getAddressEncoder", "solanaAddresses")}`,
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
if (hasSeedsClass) {
|
|
91
|
+
parts.push(fragment`
|
|
92
|
+
@immutable
|
|
93
|
+
class ${fragmentFromString(seedsClassName)} {
|
|
94
|
+
const ${fragmentFromString(seedsClassName)}({
|
|
95
|
+
${fragmentFromString(seedCtorParams)}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
${fragmentFromString(seedFieldDecls)}
|
|
99
|
+
}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const seedsParam = hasSeedsClass ? ` required ${seedsClassName} seeds,` : "";
|
|
103
|
+
|
|
104
|
+
parts.push(fragment`
|
|
105
|
+
/// Finds the program derived address for [${fragmentFromString(pascalCase(name))}].
|
|
106
|
+
Future<(Address, int)> ${fragmentFromString(findFnName)}({
|
|
107
|
+
${fragmentFromString(seedsParam)}
|
|
108
|
+
${fragmentFromString(programIdParam)},
|
|
109
|
+
}) async {
|
|
110
|
+
final seedBytes = <int>[
|
|
111
|
+
${fragmentFromString(seedBytesList.join("\n"))}
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
// TODO: Call getProgramDerivedAddress with seedBytes and programAddress
|
|
115
|
+
throw UnimplementedError('PDA derivation not yet implemented');
|
|
116
|
+
}`);
|
|
117
|
+
|
|
118
|
+
return mergeFragments(parts, (cs) => cs.join("\n"));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function bytesValueToDart(hex: string): string {
|
|
122
|
+
const clean = hex.replace(/^0x/, "");
|
|
123
|
+
const pairs: string[] = [];
|
|
124
|
+
for (let i = 0; i < clean.length; i += 2) {
|
|
125
|
+
pairs.push(`0x${clean.slice(i, i + 2)}`);
|
|
126
|
+
}
|
|
127
|
+
return `[${pairs.join(", ")}]`;
|
|
128
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ProgramNode } from "@codama/nodes";
|
|
2
|
+
|
|
3
|
+
import type { Fragment } from "../utils/fragment.js";
|
|
4
|
+
import {
|
|
5
|
+
emptyFragment,
|
|
6
|
+
fragment,
|
|
7
|
+
fragmentFromString,
|
|
8
|
+
mergeFragments,
|
|
9
|
+
use,
|
|
10
|
+
} from "../utils/fragment.js";
|
|
11
|
+
import type { RenderScope } from "../utils/options.js";
|
|
12
|
+
import { camelCase, pascalCase } from "../utils/nameTransformers.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate a full Dart file for a program.
|
|
16
|
+
*/
|
|
17
|
+
export function getProgramPageFragment(
|
|
18
|
+
node: ProgramNode,
|
|
19
|
+
scope: RenderScope,
|
|
20
|
+
): Fragment {
|
|
21
|
+
const name = node.name as string;
|
|
22
|
+
const addressConstName = scope.nameApi.programAddressConstant(name);
|
|
23
|
+
|
|
24
|
+
const parts: Fragment[] = [
|
|
25
|
+
fragment`// Auto-generated. Do not edit.
|
|
26
|
+
// ignore_for_file: type=lint
|
|
27
|
+
|
|
28
|
+
${use("Address", "solanaAddresses")}
|
|
29
|
+
|
|
30
|
+
/// The address of the ${fragmentFromString(pascalCase(name))} program.
|
|
31
|
+
const ${fragmentFromString(addressConstName)} = Address('${fragmentFromString(node.publicKey)}');`,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// Account identifier enum
|
|
35
|
+
const accounts = node.accounts ?? [];
|
|
36
|
+
if (accounts.length > 0) {
|
|
37
|
+
const accountVariants = accounts
|
|
38
|
+
.map((acc) => ` ${camelCase(acc.name as string)},`)
|
|
39
|
+
.join("\n");
|
|
40
|
+
|
|
41
|
+
parts.push(fragment`
|
|
42
|
+
/// Known accounts for the ${fragmentFromString(pascalCase(name))} program.
|
|
43
|
+
enum ${fragmentFromString(pascalCase(name))}Account {
|
|
44
|
+
${fragmentFromString(accountVariants)}
|
|
45
|
+
}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Instruction identifier enum
|
|
49
|
+
const instructions = node.instructions ?? [];
|
|
50
|
+
if (instructions.length > 0) {
|
|
51
|
+
const instrVariants = instructions
|
|
52
|
+
.map((instr) => ` ${camelCase(instr.name as string)},`)
|
|
53
|
+
.join("\n");
|
|
54
|
+
|
|
55
|
+
parts.push(fragment`
|
|
56
|
+
/// Known instructions for the ${fragmentFromString(pascalCase(name))} program.
|
|
57
|
+
enum ${fragmentFromString(pascalCase(name))}Instruction {
|
|
58
|
+
${fragmentFromString(instrVariants)}
|
|
59
|
+
}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return mergeFragments(parts, (cs) => cs.join("\n"));
|
|
63
|
+
}
|