codama-renderers-dart 0.4.3 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.browser.cjs +634 -159
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.js +635 -161
- package/dist/index.browser.js.map +1 -1
- package/dist/index.node.cjs +634 -159
- package/dist/index.node.cjs.map +1 -1
- package/dist/index.node.js +635 -161
- package/dist/index.node.js.map +1 -1
- package/dist/index.react-native.js +635 -161
- package/dist/index.react-native.js.map +1 -1
- package/dist/types/fragments/accountPage.d.ts.map +1 -1
- package/dist/types/fragments/instructionPage.d.ts.map +1 -1
- package/dist/types/fragments/programPage.d.ts +1 -1
- package/dist/types/fragments/programPage.d.ts.map +1 -1
- package/dist/types/fragments/typePage.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/utils/discriminators.d.ts +10 -0
- package/dist/types/utils/discriminators.d.ts.map +1 -0
- package/dist/types/utils/exactDecoder.d.ts +17 -0
- package/dist/types/utils/exactDecoder.d.ts.map +1 -0
- package/dist/types/utils/formatCode.d.ts +1 -1
- package/dist/types/utils/formatCode.d.ts.map +1 -1
- package/dist/types/utils/importMap.d.ts.map +1 -1
- package/dist/types/utils/index.d.ts +4 -0
- package/dist/types/utils/index.d.ts.map +1 -1
- package/dist/types/utils/nameTransformers.d.ts +1 -1
- package/dist/types/utils/nameTransformers.d.ts.map +1 -1
- package/dist/types/utils/normalizeRootNode.d.ts +11 -0
- package/dist/types/utils/normalizeRootNode.d.ts.map +1 -0
- package/dist/types/utils/valueNodes.d.ts +16 -0
- package/dist/types/utils/valueNodes.d.ts.map +1 -0
- package/dist/types/visitors/getTypeManifestVisitor.d.ts +1 -1
- package/dist/types/visitors/getTypeManifestVisitor.d.ts.map +1 -1
- package/dist/types/visitors/renderVisitor.d.ts.map +1 -1
- package/package.json +31 -23
- package/readme.md +30 -3
- package/src/fragments/accountPage.ts +90 -17
- package/src/fragments/instructionPage.ts +109 -72
- package/src/fragments/programPage.ts +165 -6
- package/src/fragments/typePage.ts +36 -15
- package/src/index.ts +1 -0
- package/src/utils/discriminators.ts +121 -0
- package/src/utils/exactDecoder.ts +82 -0
- package/src/utils/formatCode.ts +7 -11
- package/src/utils/importMap.ts +16 -3
- package/src/utils/index.ts +4 -0
- package/src/utils/nameTransformers.ts +3 -2
- package/src/utils/normalizeRootNode.ts +76 -0
- package/src/utils/valueNodes.ts +87 -0
- package/src/utils/wellKnownAddresses.ts +4 -4
- package/src/visitors/getRenderMapVisitor.ts +1 -1
- package/src/visitors/getTypeManifestVisitor.ts +48 -29
- package/src/visitors/renderVisitor.ts +11 -4
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccountNode,
|
|
3
|
+
ConstantValueNode,
|
|
4
|
+
InstructionNode,
|
|
5
|
+
StructFieldTypeNode,
|
|
6
|
+
} from "@codama/nodes";
|
|
7
|
+
import { resolveNestedTypeNode } from "@codama/nodes";
|
|
8
|
+
import { visit } from "@codama/visitors-core";
|
|
9
|
+
|
|
10
|
+
import type { Fragment } from "./fragment.js";
|
|
11
|
+
import {
|
|
12
|
+
emptyFragment,
|
|
13
|
+
fragment,
|
|
14
|
+
fragmentFromString,
|
|
15
|
+
mergeFragments,
|
|
16
|
+
use,
|
|
17
|
+
} from "./fragment.js";
|
|
18
|
+
import type { RenderScope } from "./options.js";
|
|
19
|
+
import { getDartValueFragment } from "./valueNodes.js";
|
|
20
|
+
|
|
21
|
+
type DiscriminatedNode = AccountNode | InstructionNode;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate decoder guards for every discriminator declared by a Codama node.
|
|
25
|
+
*/
|
|
26
|
+
export function getDiscriminatorValidationFragment(
|
|
27
|
+
node: DiscriminatedNode,
|
|
28
|
+
scope: RenderScope,
|
|
29
|
+
): Fragment {
|
|
30
|
+
const discriminators = node.discriminators ?? [];
|
|
31
|
+
if (discriminators.length === 0) return emptyFragment();
|
|
32
|
+
|
|
33
|
+
const fields = getDiscriminatorFields(node);
|
|
34
|
+
const fragments = discriminators.map((discriminator): Fragment => {
|
|
35
|
+
switch (discriminator.kind) {
|
|
36
|
+
case "constantDiscriminatorNode":
|
|
37
|
+
return getConstantValidationFragment(
|
|
38
|
+
discriminator.constant,
|
|
39
|
+
discriminator.offset,
|
|
40
|
+
scope,
|
|
41
|
+
);
|
|
42
|
+
case "fieldDiscriminatorNode": {
|
|
43
|
+
const field = fields.find(
|
|
44
|
+
(candidate) => candidate.name === discriminator.name,
|
|
45
|
+
);
|
|
46
|
+
if (field?.defaultValue == null) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Field discriminator "${discriminator.name}" on ${node.kind} "${node.name}" must reference a field with a default value.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return getConstantValidationFragment(
|
|
52
|
+
{
|
|
53
|
+
kind: "constantValueNode",
|
|
54
|
+
type: field.type,
|
|
55
|
+
value: field.defaultValue,
|
|
56
|
+
} as ConstantValueNode,
|
|
57
|
+
discriminator.offset,
|
|
58
|
+
scope,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
case "sizeDiscriminatorNode": {
|
|
62
|
+
if (
|
|
63
|
+
!Number.isSafeInteger(discriminator.size) ||
|
|
64
|
+
discriminator.size < 0
|
|
65
|
+
) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Discriminator size must be a non-negative safe integer, got ${discriminator.size}.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return fragment`if (bytes.length - offset != ${fragmentFromString(String(discriminator.size))}) {
|
|
71
|
+
throw ${use("SolanaError", "solanaErrors")}(
|
|
72
|
+
${use("SolanaErrorCode", "solanaErrors")}.codecsInvalidByteLength,
|
|
73
|
+
{
|
|
74
|
+
'codecDescription': '${fragmentFromString(node.name as string)} discriminator',
|
|
75
|
+
'expected': ${fragmentFromString(String(discriminator.size))},
|
|
76
|
+
'bytesLength': bytes.length - offset,
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return mergeFragments(fragments, (contents) => contents.join("\n"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getConstantValidationFragment(
|
|
88
|
+
constant: ConstantValueNode,
|
|
89
|
+
discriminatorOffset: number,
|
|
90
|
+
scope: RenderScope,
|
|
91
|
+
): Fragment {
|
|
92
|
+
if (!Number.isSafeInteger(discriminatorOffset) || discriminatorOffset < 0) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Discriminator offset must be a non-negative safe integer, got ${discriminatorOffset}.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const manifest = visit(constant.type, scope.typeManifestVisitor);
|
|
99
|
+
const value = getDartValueFragment(
|
|
100
|
+
constant.value,
|
|
101
|
+
manifest.type.content,
|
|
102
|
+
);
|
|
103
|
+
return fragment`${use("getConstantDecoder", "solanaCodecsDataStructures")}(
|
|
104
|
+
${manifest.encoder}.encode(${value}),
|
|
105
|
+
).read(bytes, offset + ${fragmentFromString(String(discriminatorOffset))});`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getDiscriminatorFields(
|
|
109
|
+
node: DiscriminatedNode,
|
|
110
|
+
): readonly StructFieldTypeNode[] {
|
|
111
|
+
if (node.kind === "accountNode") {
|
|
112
|
+
return resolveNestedTypeNode(node.data).fields ?? [];
|
|
113
|
+
}
|
|
114
|
+
return (node.arguments ?? []).map(
|
|
115
|
+
(argument) =>
|
|
116
|
+
({
|
|
117
|
+
...argument,
|
|
118
|
+
kind: "structFieldTypeNode",
|
|
119
|
+
}) as StructFieldTypeNode,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Fragment } from "./fragment.js";
|
|
2
|
+
import { fragment, fragmentFromString, use } from "./fragment.js";
|
|
3
|
+
|
|
4
|
+
export interface TopLevelDecoderFragmentOptions {
|
|
5
|
+
readonly typeName: string;
|
|
6
|
+
readonly description: string;
|
|
7
|
+
readonly discriminatorValidation: Fragment;
|
|
8
|
+
readonly fromMapFields: string;
|
|
9
|
+
readonly requireExactConsumption: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Wrap a generated top-level struct decoder with discriminator and length
|
|
14
|
+
* validation.
|
|
15
|
+
*
|
|
16
|
+
* The wrapper preserves fixed/variable decoder metadata so it remains
|
|
17
|
+
* compatible with the matching generated encoder when combined into a codec.
|
|
18
|
+
*/
|
|
19
|
+
export function getTopLevelDecoderFragment({
|
|
20
|
+
typeName,
|
|
21
|
+
description,
|
|
22
|
+
discriminatorValidation,
|
|
23
|
+
fromMapFields,
|
|
24
|
+
requireExactConsumption,
|
|
25
|
+
}: TopLevelDecoderFragmentOptions): Fragment {
|
|
26
|
+
const validation = discriminatorValidation.content
|
|
27
|
+
? ` ${discriminatorValidation.content.replaceAll("\n", "\n ")}\n`
|
|
28
|
+
: "";
|
|
29
|
+
|
|
30
|
+
const consumptionCheck = requireExactConsumption
|
|
31
|
+
? ` if (newOffset != bytes.length) {
|
|
32
|
+
throwInvalidByteLength(newOffset - offset, bytes.length - offset);
|
|
33
|
+
}
|
|
34
|
+
`
|
|
35
|
+
: "";
|
|
36
|
+
const fixedSizeCheck = requireExactConsumption
|
|
37
|
+
? "bytesLength != structDecoder.fixedSize"
|
|
38
|
+
: "bytesLength < structDecoder.fixedSize";
|
|
39
|
+
|
|
40
|
+
const result = fragment` Never throwInvalidByteLength(int expected, int bytesLength) {
|
|
41
|
+
throw ${use("SolanaError", "solanaErrors")}(
|
|
42
|
+
${use("SolanaErrorCode", "solanaErrors")}.codecsInvalidByteLength,
|
|
43
|
+
{
|
|
44
|
+
'codecDescription': '${fragmentFromString(description)}',
|
|
45
|
+
'expected': expected,
|
|
46
|
+
'bytesLength': bytesLength,
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
(${fragmentFromString(typeName)}, int) readTopLevel(Uint8List bytes, int offset) {
|
|
52
|
+
${fragmentFromString(validation)} final (map, newOffset) = structDecoder.read(bytes, offset);
|
|
53
|
+
${fragmentFromString(consumptionCheck)}
|
|
54
|
+
return (
|
|
55
|
+
${fragmentFromString(typeName)}(
|
|
56
|
+
${fragmentFromString(fromMapFields)}
|
|
57
|
+
),
|
|
58
|
+
newOffset,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return switch (structDecoder) {
|
|
63
|
+
${use("FixedSizeDecoder", "solanaCodecsCore")}<Map<String, Object?>>() =>
|
|
64
|
+
${use("FixedSizeDecoder", "solanaCodecsCore")}<${fragmentFromString(typeName)}>(
|
|
65
|
+
fixedSize: structDecoder.fixedSize,
|
|
66
|
+
read: (bytes, offset) {
|
|
67
|
+
final bytesLength = bytes.length - offset;
|
|
68
|
+
if (${fragmentFromString(fixedSizeCheck)}) {
|
|
69
|
+
throwInvalidByteLength(structDecoder.fixedSize, bytesLength);
|
|
70
|
+
}
|
|
71
|
+
return readTopLevel(bytes, offset);
|
|
72
|
+
},
|
|
73
|
+
),
|
|
74
|
+
${use("VariableSizeDecoder", "solanaCodecsCore")}<Map<String, Object?>>() =>
|
|
75
|
+
${use("VariableSizeDecoder", "solanaCodecsCore")}<${fragmentFromString(typeName)}>(
|
|
76
|
+
read: readTopLevel,
|
|
77
|
+
maxSize: structDecoder.maxSize,
|
|
78
|
+
),
|
|
79
|
+
};`;
|
|
80
|
+
result.imports.mergeWith(discriminatorValidation.imports);
|
|
81
|
+
return result;
|
|
82
|
+
}
|
package/src/utils/formatCode.ts
CHANGED
|
@@ -2,11 +2,11 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Format Dart code using `dart format`.
|
|
5
|
-
* Falls back to returning the unformatted code if
|
|
5
|
+
* Falls back to returning the unformatted code if Dart is not available.
|
|
6
6
|
*/
|
|
7
7
|
export function formatDartCode(code: string): string {
|
|
8
8
|
try {
|
|
9
|
-
const result = execSync("dart format --
|
|
9
|
+
const result = execSync("dart format --output=show", {
|
|
10
10
|
input: code,
|
|
11
11
|
encoding: "utf-8",
|
|
12
12
|
timeout: 30_000,
|
|
@@ -23,13 +23,9 @@ export function formatDartCode(code: string): string {
|
|
|
23
23
|
* Format all Dart files in a directory using `dart format`.
|
|
24
24
|
*/
|
|
25
25
|
export function formatDartDirectory(dir: string): void {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
});
|
|
32
|
-
} catch {
|
|
33
|
-
// Silently ignore if dart format is not available
|
|
34
|
-
}
|
|
26
|
+
execSync(`dart format "${dir}"`, {
|
|
27
|
+
encoding: "utf-8",
|
|
28
|
+
timeout: 60_000,
|
|
29
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
30
|
+
});
|
|
35
31
|
}
|
package/src/utils/importMap.ts
CHANGED
|
@@ -77,15 +77,20 @@ export class DartImportMap {
|
|
|
77
77
|
for (const module of this._imports) {
|
|
78
78
|
// Check internal map first (generated cross-references)
|
|
79
79
|
if (module in internalMap) {
|
|
80
|
-
|
|
80
|
+
const uri = internalMap[module];
|
|
81
|
+
if (uri) {
|
|
82
|
+
uris.add(uri);
|
|
83
|
+
}
|
|
81
84
|
}
|
|
82
85
|
// Then external package map
|
|
83
86
|
else if (module in DART_EXTERNAL_PACKAGE_MAP) {
|
|
84
87
|
uris.add(DART_EXTERNAL_PACKAGE_MAP[module]);
|
|
85
88
|
}
|
|
86
|
-
//
|
|
87
|
-
else {
|
|
89
|
+
// Finally, accept explicit Dart URIs and relative Dart paths.
|
|
90
|
+
else if (isRawDartImportUri(module)) {
|
|
88
91
|
uris.add(module);
|
|
92
|
+
} else {
|
|
93
|
+
throw new Error(`Unresolved Dart import module "${module}"`);
|
|
89
94
|
}
|
|
90
95
|
}
|
|
91
96
|
|
|
@@ -127,3 +132,11 @@ export class DartImportMap {
|
|
|
127
132
|
return lines.join("\n");
|
|
128
133
|
}
|
|
129
134
|
}
|
|
135
|
+
|
|
136
|
+
function isRawDartImportUri(module: string): boolean {
|
|
137
|
+
return (
|
|
138
|
+
module.startsWith("dart:") ||
|
|
139
|
+
module.startsWith("package:") ||
|
|
140
|
+
(module.endsWith(".dart") && !module.includes(":"))
|
|
141
|
+
);
|
|
142
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export * from "./codecs.js";
|
|
2
|
+
export * from "./discriminators.js";
|
|
3
|
+
export * from "./exactDecoder.js";
|
|
2
4
|
export * from "./formatCode.js";
|
|
3
5
|
export * from "./fragment.js";
|
|
4
6
|
export * from "./importMap.js";
|
|
5
7
|
export * from "./nameTransformers.js";
|
|
8
|
+
export * from "./normalizeRootNode.js";
|
|
6
9
|
export * from "./options.js";
|
|
7
10
|
export * from "./typeManifest.js";
|
|
11
|
+
export * from "./valueNodes.js";
|
|
8
12
|
export * from "./wellKnownAddresses.js";
|
|
@@ -66,7 +66,7 @@ export interface DartNameApi {
|
|
|
66
66
|
errorMessageFunction(name: string): string;
|
|
67
67
|
/** Enum variant: `camelCase` */
|
|
68
68
|
enumVariant(name: string): string;
|
|
69
|
-
/** Sealed class variant (data enum): `PascalCase` */
|
|
69
|
+
/** Sealed class variant (data enum): `{ParentName}{PascalCase}` */
|
|
70
70
|
sealedClassVariant(parentName: string, variantName: string): string;
|
|
71
71
|
/** File name: `snake_case.dart` */
|
|
72
72
|
fileName(name: string): string;
|
|
@@ -97,7 +97,8 @@ export function createDartNameApi(): DartNameApi {
|
|
|
97
97
|
`${camelCase(programName)}Error${pascalCase(errorName)}`,
|
|
98
98
|
errorMessageFunction: (name) => `get${pascalCase(name)}ErrorMessage`,
|
|
99
99
|
enumVariant: (name) => camelCase(name),
|
|
100
|
-
sealedClassVariant: (
|
|
100
|
+
sealedClassVariant: (parentName, variantName) =>
|
|
101
|
+
`${pascalCase(parentName)}${pascalCase(variantName)}`,
|
|
101
102
|
isTypeFunction: (name) => `is${pascalCase(name)}`,
|
|
102
103
|
fileName: (name) => snakeCase(name),
|
|
103
104
|
discriminatorConstant: (name) => `${camelCase(name)}Discriminator`,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { RootNode } from "@codama/nodes";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes a serialized Codama root before it reaches Codama's visitors.
|
|
5
|
+
*
|
|
6
|
+
* Codama constructors materialize empty child collections, while generated
|
|
7
|
+
* JSON commonly omits them. Pina intentionally omits those empty fields, so
|
|
8
|
+
* restore only the structural defaults required by Codama traversal. This is
|
|
9
|
+
* a copy: callers retain their original parsed IDL unchanged.
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeRootNode(root: RootNode): RootNode {
|
|
12
|
+
return normalizeNode(root) as RootNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeNode(value: unknown): unknown {
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return value.map(normalizeNode);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!isRecord(value)) {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const normalized = Object.fromEntries(
|
|
25
|
+
Object.entries(value).map(([key, child]) => [key, normalizeNode(child)]),
|
|
26
|
+
) as Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
switch (normalized.kind) {
|
|
29
|
+
case "rootNode":
|
|
30
|
+
setEmptyCollection(normalized, "additionalPrograms");
|
|
31
|
+
break;
|
|
32
|
+
case "programNode":
|
|
33
|
+
setEmptyCollection(normalized, "accounts");
|
|
34
|
+
setEmptyCollection(normalized, "constants");
|
|
35
|
+
setEmptyCollection(normalized, "definedTypes");
|
|
36
|
+
setEmptyCollection(normalized, "errors");
|
|
37
|
+
setEmptyCollection(normalized, "events");
|
|
38
|
+
setEmptyCollection(normalized, "instructions");
|
|
39
|
+
setEmptyCollection(normalized, "pdas");
|
|
40
|
+
break;
|
|
41
|
+
case "instructionNode":
|
|
42
|
+
setEmptyCollection(normalized, "accounts");
|
|
43
|
+
setEmptyCollection(normalized, "arguments");
|
|
44
|
+
break;
|
|
45
|
+
case "pdaNode":
|
|
46
|
+
setEmptyCollection(normalized, "seeds");
|
|
47
|
+
break;
|
|
48
|
+
case "structTypeNode":
|
|
49
|
+
setEmptyCollection(normalized, "fields");
|
|
50
|
+
break;
|
|
51
|
+
case "enumTypeNode":
|
|
52
|
+
setEmptyCollection(normalized, "variants");
|
|
53
|
+
break;
|
|
54
|
+
case "tupleTypeNode":
|
|
55
|
+
setEmptyCollection(normalized, "items");
|
|
56
|
+
break;
|
|
57
|
+
case "hiddenPrefixTypeNode":
|
|
58
|
+
setEmptyCollection(normalized, "prefix");
|
|
59
|
+
break;
|
|
60
|
+
case "hiddenSuffixTypeNode":
|
|
61
|
+
setEmptyCollection(normalized, "suffix");
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
69
|
+
return typeof value === "object" && value !== null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function setEmptyCollection(record: Record<string, unknown>, key: string): void {
|
|
73
|
+
if (record[key] === undefined) {
|
|
74
|
+
record[key] = [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InstructionInputValueNode,
|
|
3
|
+
ValueNode,
|
|
4
|
+
} from "@codama/nodes";
|
|
5
|
+
|
|
6
|
+
import type { Fragment } from "./fragment.js";
|
|
7
|
+
import { fragment, fragmentFromString, use } from "./fragment.js";
|
|
8
|
+
import { WELL_KNOWN_ADDRESSES } from "./wellKnownAddresses.js";
|
|
9
|
+
|
|
10
|
+
type RenderableValueNode = InstructionInputValueNode | ValueNode;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Render a standalone Codama value as a Dart expression.
|
|
14
|
+
*
|
|
15
|
+
* Contextual instruction values need resolver state and are deliberately
|
|
16
|
+
* rejected here. Defaults and discriminators must be deterministic at code
|
|
17
|
+
* generation time so generated encoders cannot silently choose a different
|
|
18
|
+
* wire value.
|
|
19
|
+
*/
|
|
20
|
+
export function getDartValueFragment(
|
|
21
|
+
value: RenderableValueNode,
|
|
22
|
+
dartType?: string,
|
|
23
|
+
): Fragment {
|
|
24
|
+
switch (value.kind) {
|
|
25
|
+
case "numberValueNode":
|
|
26
|
+
return fragmentFromString(
|
|
27
|
+
dartType === "BigInt"
|
|
28
|
+
? `BigInt.from(${value.number})`
|
|
29
|
+
: String(value.number),
|
|
30
|
+
);
|
|
31
|
+
case "booleanValueNode":
|
|
32
|
+
return fragmentFromString(String(value.boolean));
|
|
33
|
+
case "stringValueNode":
|
|
34
|
+
return fragmentFromString(toDartStringLiteral(value.string));
|
|
35
|
+
case "noneValueNode":
|
|
36
|
+
return fragmentFromString("null");
|
|
37
|
+
case "bytesValueNode": {
|
|
38
|
+
if (value.encoding !== "base16") {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Unsupported deterministic bytes encoding: ${value.encoding}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const clean = value.data.replace(/^0x/, "");
|
|
44
|
+
if (clean.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(clean)) {
|
|
45
|
+
throw new Error(`Invalid hexadecimal bytes value: ${value.data}`);
|
|
46
|
+
}
|
|
47
|
+
const bytes = clean.match(/.{2}/g)?.map((byte) => `0x${byte}`) ?? [];
|
|
48
|
+
return fragment`${use("Uint8List", "dartTypedData")}.fromList([${fragmentFromString(bytes.join(", "))}])`;
|
|
49
|
+
}
|
|
50
|
+
case "publicKeyValueNode": {
|
|
51
|
+
const wellKnownName = WELL_KNOWN_ADDRESSES.get(value.publicKey);
|
|
52
|
+
return wellKnownName
|
|
53
|
+
? fragment`${use(wellKnownName, "solanaAddresses")}`
|
|
54
|
+
: fragment`${use("Address", "solanaAddresses")}(${fragmentFromString(toDartStringLiteral(value.publicKey))})`;
|
|
55
|
+
}
|
|
56
|
+
default:
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Unsupported deterministic Dart value node: ${value.kind}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Return whether a rendered standalone value is valid in a const initializer. */
|
|
64
|
+
export function isConstDartValueNode(value: RenderableValueNode): boolean {
|
|
65
|
+
switch (value.kind) {
|
|
66
|
+
case "numberValueNode":
|
|
67
|
+
case "booleanValueNode":
|
|
68
|
+
case "stringValueNode":
|
|
69
|
+
case "noneValueNode":
|
|
70
|
+
case "publicKeyValueNode":
|
|
71
|
+
return true;
|
|
72
|
+
default:
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function toDartStringLiteral(value: string): string {
|
|
78
|
+
return `'${value
|
|
79
|
+
.replaceAll("\\", "\\\\")
|
|
80
|
+
.replaceAll("\n", "\\n")
|
|
81
|
+
.replaceAll("\r", "\\r")
|
|
82
|
+
.replaceAll("\t", "\\t")
|
|
83
|
+
.replaceAll("\b", "\\b")
|
|
84
|
+
.replaceAll("\f", "\\f")
|
|
85
|
+
.replaceAll("$", "\\$")
|
|
86
|
+
.replaceAll("'", "\\'")}'`;
|
|
87
|
+
}
|
|
@@ -44,10 +44,10 @@ export const WELL_KNOWN_ADDRESSES: ReadonlyMap<string, string> = new Map([
|
|
|
44
44
|
|
|
45
45
|
// SPL programs
|
|
46
46
|
['TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'tokenProgramAddress'],
|
|
47
|
-
['
|
|
48
|
-
['
|
|
49
|
-
['
|
|
50
|
-
['
|
|
47
|
+
['TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb', 'token2022ProgramAddress'],
|
|
48
|
+
['ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL', 'associatedTokenProgramAddress'],
|
|
49
|
+
['MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr', 'memoProgramAddress'],
|
|
50
|
+
['Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo', 'memoLegacyProgramAddress'],
|
|
51
51
|
|
|
52
52
|
// Metaplex programs
|
|
53
53
|
['metaqbxxU9qX8rZXkmUJ7haCrXXz2W1PSUq1Rn3E1od', 'tokenMetadataProgramAddress'],
|
|
@@ -86,7 +86,7 @@ export function getRenderMapVisitor(
|
|
|
86
86
|
visitRoot(node: RootNode, { self }) {
|
|
87
87
|
const programMaps = [
|
|
88
88
|
visit(node.program, self),
|
|
89
|
-
...node.additionalPrograms.map((p) => visit(p, self)),
|
|
89
|
+
...(node.additionalPrograms ?? []).map((p) => visit(p, self)),
|
|
90
90
|
];
|
|
91
91
|
|
|
92
92
|
const mergedProgramMap = mergeRenderMaps(programMaps);
|