gen-typescript-from-tolk-dev 0.1.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/README.md +47 -0
- package/bin/generator.js +15 -0
- package/package.json +32 -0
- package/src/abi-types.ts +157 -0
- package/src/abi.ts +132 -0
- package/src/cli-generate-from-abi-json.ts +21 -0
- package/src/codegen-ctx.ts +115 -0
- package/src/dynamic-ctx.ts +55 -0
- package/src/dynamic-debug-print.ts +191 -0
- package/src/dynamic-get-methods.ts +454 -0
- package/src/dynamic-serialization.ts +430 -0
- package/src/dynamic-validation.ts +55 -0
- package/src/emit-field-defs.ts +60 -0
- package/src/emit-pack-unpack.ts +280 -0
- package/src/emit-stack-rw.ts +239 -0
- package/src/emit-ts-types.ts +66 -0
- package/src/formatting.ts +98 -0
- package/src/generate-from-abi-json.ts +22 -0
- package/src/generate-ts-wrappers.ts +477 -0
- package/src/out-template.ts +514 -0
- package/src/tolk-to-abi.ts +5 -0
- package/src/types-kernel.ts +215 -0
- package/src/unsupported-errors.ts +82 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// OutBuf helps to generate TypeScript output with proper indentation.
|
|
2
|
+
export class OutBuf {
|
|
3
|
+
static TAB = ' '
|
|
4
|
+
|
|
5
|
+
static onNewLine(s: string): string {
|
|
6
|
+
return `{indent}${s}{outdent}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
private static indentNested(input: string, indentLevel: number): string {
|
|
10
|
+
let result = '';
|
|
11
|
+
let i = 0;
|
|
12
|
+
|
|
13
|
+
while (i < input.length) {
|
|
14
|
+
if (input.startsWith('{indent}', i)) {
|
|
15
|
+
result += '\n' + ' '.repeat(++indentLevel);
|
|
16
|
+
i += 8;
|
|
17
|
+
} else if (input.startsWith('{outdent}', i)) {
|
|
18
|
+
result += '\n' + ' '.repeat(--indentLevel);
|
|
19
|
+
i += 9;
|
|
20
|
+
} else if (input[i] === '\n') {
|
|
21
|
+
result += '\n' + ' '.repeat(indentLevel);
|
|
22
|
+
i++;
|
|
23
|
+
} else {
|
|
24
|
+
result += input[i];
|
|
25
|
+
i++;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private lines = [] as string[];
|
|
33
|
+
private indentLevel = 0;
|
|
34
|
+
|
|
35
|
+
indent() { this.indentLevel++; }
|
|
36
|
+
outdent() { this.indentLevel--; }
|
|
37
|
+
emptyLine() { this.lines.push(''); }
|
|
38
|
+
|
|
39
|
+
push(line: string) {
|
|
40
|
+
if (line.includes('{indent}') || line.includes('\n')) {
|
|
41
|
+
line = OutBuf.indentNested(line.trimEnd(), this.indentLevel);
|
|
42
|
+
line = line.replace(/\s+\n/g, '\n');
|
|
43
|
+
line = line.replace(/\n\s+,/g, ',');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (line.startsWith('}') || line.startsWith(']') || line.startsWith(')')) {
|
|
47
|
+
this.outdent();
|
|
48
|
+
}
|
|
49
|
+
this.lines.push(OutBuf.TAB.repeat(this.indentLevel) + line);
|
|
50
|
+
if (line.endsWith('{') || line.endsWith('[') || line.endsWith('(')) {
|
|
51
|
+
this.indent();
|
|
52
|
+
}
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
toString() {
|
|
57
|
+
return this.lines.join('\n');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// In Tolk, similar to Kotlin, identifiers (structs, fields, etc.) can be embraced:
|
|
62
|
+
// > struct `foo-bar` { `a()`: int }
|
|
63
|
+
// Such names are incorrect in TS/JS and require special handling.
|
|
64
|
+
function isSafeJsIdent(tolkStructName: string): boolean {
|
|
65
|
+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(tolkStructName);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// In TS, `interface` and `const` can not contain '-' and other invalid symbols,
|
|
69
|
+
// so we mangle them: "foo-bar@" -> "foo_bar_".
|
|
70
|
+
// NB! Keywords, like 'await' or 'function', are also invalid identifiers,
|
|
71
|
+
// but since structs and type aliases are recommended to be named CamelCase in Tolk contracts,
|
|
72
|
+
// we don't expect collisions with JS keywords here.
|
|
73
|
+
export function safeJsIdent(tolkIdentifier: string): string {
|
|
74
|
+
if (isSafeJsIdent(tolkIdentifier)) {
|
|
75
|
+
return tolkIdentifier;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let safe = tolkIdentifier.replace(/[^a-zA-Z0-9_$]/g, '_');
|
|
79
|
+
if (/^[0-9]/.test(safe)) {
|
|
80
|
+
safe = '_' + safe;
|
|
81
|
+
}
|
|
82
|
+
return safe;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// In object literals, 'foo-bar' can be used (without mangling to '_') if quoted:
|
|
86
|
+
// > interface A { foo: number, "foo-bar": number }
|
|
87
|
+
// > return { foo: 0, "foo-bar": 123 }
|
|
88
|
+
// Also, `__proto__` requires special handling, but I decided not to take it into account.
|
|
89
|
+
export function safeFieldDecl(fieldName: string): string {
|
|
90
|
+
return isSafeJsIdent(fieldName) ? fieldName : JSON.stringify(fieldName);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// When accessing object fields, use ["syntax"] for invalid identifiers:
|
|
94
|
+
// > obj.foo
|
|
95
|
+
// > obj["foo-bar"]
|
|
96
|
+
export function safeFieldRead(fieldName: string): string {
|
|
97
|
+
return isSafeJsIdent(fieldName) ? `.${fieldName}` : `[${JSON.stringify(fieldName)}]`;
|
|
98
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ContractABI } from './abi';
|
|
2
|
+
import { generateTypeScriptFileForContract } from './generate-ts-wrappers';
|
|
3
|
+
|
|
4
|
+
function parseContractAbi(value: unknown): ContractABI {
|
|
5
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
6
|
+
throw new Error('ABI JSON must describe an object');
|
|
7
|
+
}
|
|
8
|
+
if (typeof (value as { contractName?: unknown }).contractName !== 'string') {
|
|
9
|
+
throw new Error("ABI JSON must contain a string 'contractName'");
|
|
10
|
+
}
|
|
11
|
+
return value as ContractABI;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function generateTypeScriptFromAbiJson(abiJson: string): string {
|
|
15
|
+
let parsedJson: unknown;
|
|
16
|
+
try {
|
|
17
|
+
parsedJson = JSON.parse(abiJson);
|
|
18
|
+
} catch (error: any) {
|
|
19
|
+
throw new Error(`Failed to parse ABI JSON: ${error.message ?? error.toString()}`);
|
|
20
|
+
}
|
|
21
|
+
return generateTypeScriptFileForContract(parseContractAbi(parsedJson));
|
|
22
|
+
}
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import type { Ty, ABIStruct, ABIAlias, ABIEnum } from './abi-types';
|
|
3
|
+
import type { ContractABI, ABIGetMethod } from './abi';
|
|
4
|
+
import { CodegenCtx, RUNTIME } from './codegen-ctx';
|
|
5
|
+
import { OutBuf, safeJsIdent, safeFieldDecl, safeFieldRead } from './formatting';
|
|
6
|
+
import { emitTsType, emitUnionLabelAndValue } from './emit-ts-types';
|
|
7
|
+
import { emitLoadExpr, emitStoreStatement, emitMakeCellFromExpr, emitCallToCreateMethodExpr } from './emit-pack-unpack';
|
|
8
|
+
import { emitStackReadExpr, emitStackWriteItems } from './emit-stack-rw';
|
|
9
|
+
import { emitFieldDefault, emitFieldDefaultInComment, isDefaultValueSupported } from './emit-field-defs';
|
|
10
|
+
import { CantGeneratePackUnpack, CantGenerateWrappersAtAll } from './unsupported-errors';
|
|
11
|
+
import { calcWidthOnStack, createLabelsForUnion, instantiateGenerics, renderTy } from './types-kernel'
|
|
12
|
+
|
|
13
|
+
// Output a struct as a doc comment.
|
|
14
|
+
function renderStructDocComment(s: ABIStruct): string {
|
|
15
|
+
const genericTs = s.typeParams ? `<${s.typeParams.join(', ')}>` : '';
|
|
16
|
+
return [
|
|
17
|
+
'/**',
|
|
18
|
+
` > struct${s.prefix ? ` (${s.prefix.prefixStr})` : ''} ${s.name}${genericTs} {`,
|
|
19
|
+
...s.fields.map(f => ` > ${f.name}: ${renderTy(f.ty)}`),
|
|
20
|
+
' > }',
|
|
21
|
+
' */'
|
|
22
|
+
].join('\n')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Output an alias as a doc comment.
|
|
26
|
+
function renderAliasDocComment(a: ABIAlias): string {
|
|
27
|
+
const genericTs = a.typeParams ? `<${a.typeParams.join(', ')}>` : '';
|
|
28
|
+
return [
|
|
29
|
+
'/**',
|
|
30
|
+
` > type ${a.name}${genericTs} = ${renderTy(a.targetTy)}`,
|
|
31
|
+
' */'
|
|
32
|
+
].join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Output an enum as a doc comment.
|
|
36
|
+
function renderEnumDocComment(e: ABIEnum): string {
|
|
37
|
+
return [
|
|
38
|
+
'/**',
|
|
39
|
+
` > enum ${e.name} { ${e.members.length} variants }`,
|
|
40
|
+
' */'
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// If a struct can't be serialized, then its `fromSlice`/`store` is just `throw` with description.
|
|
45
|
+
// Since Tolk code contains many structs (some are used for contract getters, for example),
|
|
46
|
+
// not all of them are intended for serialization, it's expected.
|
|
47
|
+
function renderThrowCantPackUnpack(structName: string, caughtEx: any, isPack: boolean): string {
|
|
48
|
+
if (!(caughtEx instanceof CantGeneratePackUnpack))
|
|
49
|
+
throw caughtEx
|
|
50
|
+
|
|
51
|
+
const action = isPack ? `pack '${structName}' to cell` : `unpack '${structName}' from cell`;
|
|
52
|
+
return `throw new Error(\`Can't ${action}, because ${caughtEx.message}\`);`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// All-in-one: a struct with autogen serializers.
|
|
56
|
+
// Tolk `struct X` -> TS `interface X` { $: 'X'; ...fields } + `const X` with serializers.
|
|
57
|
+
function tolkStructToTypeScript(ctx: CodegenCtx, s: ABIStruct, buf: OutBuf): string {
|
|
58
|
+
const ps = s.typeParams;
|
|
59
|
+
const genericTs = ps ? `<${ps.join(', ')}>` : '';
|
|
60
|
+
const structName = safeJsIdent(s.name);
|
|
61
|
+
const structTy: Ty = { kind: 'StructRef', structName: s.name, typeArgs: s.typeParams?.map(nameT => ({kind: 'genericT', nameT})) };
|
|
62
|
+
|
|
63
|
+
const loadCallbackParams = ps ? ', ' + ps.map(p => `loadFn_${p}: LoadCallback<${p}>`).join(', ') : '';
|
|
64
|
+
const storeCallbackParams = ps ? ', ' + ps.map(p => `storeFn_${p}: StoreCallback<${p}>`).join(', ') : '';
|
|
65
|
+
|
|
66
|
+
buf.push(renderStructDocComment(s));
|
|
67
|
+
buf.push(`export interface ${structName}${genericTs} {`);
|
|
68
|
+
buf.push(`readonly $: '${s.name}'`);
|
|
69
|
+
for (const f of s.fields) {
|
|
70
|
+
let comment = f.defaultValue ? ` /* = ${emitFieldDefaultInComment(f.defaultValue)} */` : '';
|
|
71
|
+
buf.push(`${safeFieldDecl(f.name)}: ${emitTsType(ctx, f.ty)}${comment}`);
|
|
72
|
+
}
|
|
73
|
+
buf.push('}');
|
|
74
|
+
buf.emptyLine();
|
|
75
|
+
buf.push(`export const ${structName} = {`);
|
|
76
|
+
if (s.prefix) {
|
|
77
|
+
buf.push(`PREFIX: ${s.prefix.prefixStr},`);
|
|
78
|
+
buf.emptyLine();
|
|
79
|
+
}
|
|
80
|
+
let argsParam = s.fields.length ? 'args: ' + structParameterNoLabel(ctx, s.name) : '';
|
|
81
|
+
buf.push(`create${genericTs}(${argsParam}): ${structName}${genericTs} {`);
|
|
82
|
+
buf.push(`return {`);
|
|
83
|
+
buf.push(`$: '${s.name}',`);
|
|
84
|
+
for (let f of s.fields.filter(f => f.defaultValue && isDefaultValueSupported(f.ty))) {
|
|
85
|
+
let needAsAny = f.ty.kind === 'genericT';
|
|
86
|
+
buf.push(`${safeFieldDecl(f.name)}: ${emitFieldDefault(ctx, f.defaultValue!)}${needAsAny ? ' as any' : ''},`);
|
|
87
|
+
}
|
|
88
|
+
if (s.fields.length > 0) {
|
|
89
|
+
buf.push('...args');
|
|
90
|
+
}
|
|
91
|
+
buf.push('}');
|
|
92
|
+
buf.push(`},`);
|
|
93
|
+
buf.push(`fromSlice${genericTs}(s: c.Slice${loadCallbackParams}): ${structName}${genericTs} {`);
|
|
94
|
+
if (s.customPackUnpack?.unpackFromSlice) {
|
|
95
|
+
buf.push(`return invokeCustomUnpackFromSlice<${structName}>('${s.name}', s);`);
|
|
96
|
+
} else try {
|
|
97
|
+
const fieldsLoads = s.fields.map(f =>
|
|
98
|
+
`${safeFieldDecl(f.name)}: ${emitLoadExpr(ctx, `${structName}${safeFieldRead(f.name)}`, f.ty)},`
|
|
99
|
+
)
|
|
100
|
+
if (s.prefix && s.prefix.prefixLen === 32) {
|
|
101
|
+
buf.push(`${RUNTIME.loadAndCheckPrefix32}(s, ${s.prefix.prefixStr}, '${s.name}');`);
|
|
102
|
+
} else if (s.prefix) {
|
|
103
|
+
ctx.has_non32Prefixes = true;
|
|
104
|
+
buf.push(`${RUNTIME.loadAndCheckPrefix}(s, ${s.prefix.prefixStr}, ${s.prefix.prefixLen}, '${s.name}');`);
|
|
105
|
+
}
|
|
106
|
+
buf.push(`return {`);
|
|
107
|
+
buf.push(`$: '${s.name}',`);
|
|
108
|
+
fieldsLoads.forEach(l => buf.push(l));
|
|
109
|
+
buf.push('}');
|
|
110
|
+
} catch (ex) {
|
|
111
|
+
buf.push(renderThrowCantPackUnpack(structName, ex, false));
|
|
112
|
+
}
|
|
113
|
+
buf.push(`},`);
|
|
114
|
+
buf.push(`store${genericTs}(self: ${structName}${genericTs}, b: c.Builder${storeCallbackParams}): void {`);
|
|
115
|
+
if (s.customPackUnpack?.packToBuilder) {
|
|
116
|
+
buf.push(`invokeCustomPackToBuilder<${structName}>('${s.name}', self, b);`);
|
|
117
|
+
} else try {
|
|
118
|
+
const fieldsStores = s.fields.map(f =>
|
|
119
|
+
emitStoreStatement(ctx, `self${safeFieldRead(f.name)}`, f.ty)
|
|
120
|
+
);
|
|
121
|
+
if (s.prefix) {
|
|
122
|
+
buf.push(`b.storeUint(${s.prefix.prefixStr}, ${s.prefix.prefixLen});`);
|
|
123
|
+
}
|
|
124
|
+
fieldsStores.forEach(l => buf.push(l));
|
|
125
|
+
} catch (ex) {
|
|
126
|
+
buf.push(renderThrowCantPackUnpack(structName, ex, true))
|
|
127
|
+
}
|
|
128
|
+
buf.push(`},`);
|
|
129
|
+
buf.push(`toCell${genericTs}(self: ${structName}${genericTs}${storeCallbackParams}): c.Cell {`);
|
|
130
|
+
buf.push(`return ${emitMakeCellFromExpr(ctx, 'self', structTy, true)};`);
|
|
131
|
+
buf.push('}');
|
|
132
|
+
buf.push('}');
|
|
133
|
+
return buf.toString();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// All-in-one: an alias with autogen serializers.
|
|
137
|
+
// Tolk `type X = Y` -> TS `type X = Y` (interchangeable with its underlying type, like in Tolk).
|
|
138
|
+
function tolkAliasToTypeScript(ctx: CodegenCtx, a: ABIAlias, buf: OutBuf): string {
|
|
139
|
+
const ps = a.typeParams;
|
|
140
|
+
const genericTs = ps ? `<${ps.join(', ')}>` : '';
|
|
141
|
+
const aliasName = safeJsIdent(a.name);
|
|
142
|
+
const aliasTy: Ty = { kind: 'AliasRef', aliasName: a.name, typeArgs: a.typeParams?.map(nameT => ({kind: 'genericT', nameT})) };
|
|
143
|
+
|
|
144
|
+
const loadCallbackParams = ps ? ', ' + ps.map(p => `loadFn_${p}: LoadCallback<${p}>`).join(', ') : '';
|
|
145
|
+
const storeCallbackParams = ps ? ', ' + ps.map(p => `storeFn_${p}: StoreCallback<${p}>`).join(', ') : '';
|
|
146
|
+
|
|
147
|
+
ctx.has_customPackUnpack ||= a.customPackUnpack !== undefined;
|
|
148
|
+
|
|
149
|
+
buf.push(renderAliasDocComment(a));
|
|
150
|
+
if (a.targetTy.kind === 'union') {
|
|
151
|
+
buf.push(`export type ${aliasName}${genericTs} =`).indent();
|
|
152
|
+
for (const v of createLabelsForUnion(ctx.symbols, a.targetTy.variants)) {
|
|
153
|
+
buf.push('| ' + emitUnionLabelAndValue(v, emitTsType(ctx, v.variantTy)));
|
|
154
|
+
}
|
|
155
|
+
buf.outdent();
|
|
156
|
+
} else {
|
|
157
|
+
buf.push(`export type ${aliasName}${genericTs} = ${emitTsType(ctx, a.targetTy)}`);
|
|
158
|
+
}
|
|
159
|
+
buf.emptyLine();
|
|
160
|
+
buf.push(`export const ${aliasName} = {`);
|
|
161
|
+
buf.push(`fromSlice${genericTs}(s: c.Slice${loadCallbackParams}): ${aliasName}${genericTs} {`);
|
|
162
|
+
if (a.customPackUnpack?.unpackFromSlice) {
|
|
163
|
+
buf.push(`return invokeCustomUnpackFromSlice<${aliasName}>('${a.name}', s);`);
|
|
164
|
+
} else try {
|
|
165
|
+
buf.push(`return ${emitLoadExpr(ctx, aliasName, a.targetTy)};`);
|
|
166
|
+
} catch (ex) {
|
|
167
|
+
buf.push(renderThrowCantPackUnpack(aliasName, ex, false));
|
|
168
|
+
}
|
|
169
|
+
buf.push(`},`);
|
|
170
|
+
buf.push(`store${genericTs}(self: ${aliasName}${genericTs}, b: c.Builder${storeCallbackParams}): void {`);
|
|
171
|
+
if (a.customPackUnpack?.packToBuilder) {
|
|
172
|
+
buf.push(`invokeCustomPackToBuilder<${aliasName}>('${a.name}', self, b);`);
|
|
173
|
+
} else try {
|
|
174
|
+
buf.push(emitStoreStatement(ctx, 'self', a.targetTy));
|
|
175
|
+
} catch (ex) {
|
|
176
|
+
buf.push(renderThrowCantPackUnpack(aliasName, ex, true));
|
|
177
|
+
}
|
|
178
|
+
buf.push(`},`);
|
|
179
|
+
buf.push(`toCell${genericTs}(self: ${aliasName}${genericTs}${storeCallbackParams}): c.Cell {`);
|
|
180
|
+
buf.push(`return ${emitMakeCellFromExpr(ctx, 'self', aliasTy, true)};`);
|
|
181
|
+
buf.push('}');
|
|
182
|
+
buf.push('}');
|
|
183
|
+
return buf.toString();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// All-in-one: an enum with autogen serializers.
|
|
187
|
+
// Tolk `enum X` -> TS `type X = bigint` + `const X` with members and serializers.
|
|
188
|
+
function tolkEnumToTypeScript(ctx: CodegenCtx, e: ABIEnum, buf: OutBuf): string {
|
|
189
|
+
const enumName = safeJsIdent(e.name);
|
|
190
|
+
const enumTy: Ty = { kind: 'EnumRef', enumName: e.name };
|
|
191
|
+
|
|
192
|
+
buf.push(renderEnumDocComment(e));
|
|
193
|
+
buf.push(`export type ${enumName} = bigint`);
|
|
194
|
+
buf.emptyLine();
|
|
195
|
+
buf.push(`export const ${enumName} = {`);
|
|
196
|
+
for (const v of e.members) {
|
|
197
|
+
let memberName = v.name;
|
|
198
|
+
if (memberName === 'toCell' || memberName === 'store' || memberName === 'fromSlice') {
|
|
199
|
+
memberName += '_';
|
|
200
|
+
}
|
|
201
|
+
buf.push(`${safeFieldDecl(memberName)}: ${v.value}n,`);
|
|
202
|
+
}
|
|
203
|
+
buf.emptyLine();
|
|
204
|
+
buf.push(`fromSlice(s: c.Slice): ${enumName} {`);
|
|
205
|
+
if (e.customPackUnpack?.unpackFromSlice) {
|
|
206
|
+
buf.push(`return invokeCustomUnpackFromSlice<${enumName}>('${e.name}', s);`);
|
|
207
|
+
} else {
|
|
208
|
+
buf.push(`return ${emitLoadExpr(ctx, e.name, e.encodedAs)};`);
|
|
209
|
+
}
|
|
210
|
+
buf.push(`},`);
|
|
211
|
+
buf.push(`store(self: ${enumName}, b: c.Builder): void {`);
|
|
212
|
+
if (e.customPackUnpack?.packToBuilder) {
|
|
213
|
+
buf.push(`return invokeCustomPackToBuilder<${enumName}>('${e.name}', self, b);`);
|
|
214
|
+
} else {
|
|
215
|
+
buf.push(emitStoreStatement(ctx, 'self', e.encodedAs));
|
|
216
|
+
}
|
|
217
|
+
buf.push(`},`);
|
|
218
|
+
buf.push(`toCell(self: ${enumName}): c.Cell {`);
|
|
219
|
+
buf.push(`return ${emitMakeCellFromExpr(ctx, 'self', enumTy, true)};`);
|
|
220
|
+
buf.push('}');
|
|
221
|
+
buf.push('}');
|
|
222
|
+
return buf.toString();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Convert "get_wallet_data" to "getWalletData".
|
|
226
|
+
function snakeCaseToCamelCase(snakeString: string): string {
|
|
227
|
+
return snakeString.replace(/_([a-zA-Z])/g, (_, letter: string) => letter.toUpperCase());
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// For sendXXX contract methods, we generate not `(msg: XXX)` but `(body: { ...fields })`,
|
|
231
|
+
// so that on a caller side, one can use `contract.sendXXX({ queryId: 0n, ... })`
|
|
232
|
+
// instead of `contract.sendXXX(XXX.create({ queryId: 0n, ... }))` (although the latter is also acceptable).
|
|
233
|
+
// Note that if a field has a default value, it's declared as optional (`f?: T`, not `f: T`).
|
|
234
|
+
function structParameterNoLabel(ctx: CodegenCtx, structName: string, typeArgs?: Ty[]): string {
|
|
235
|
+
const structRef = ctx.symbols.getStruct(structName);
|
|
236
|
+
let declBuf = new OutBuf();
|
|
237
|
+
declBuf.push(`{`);
|
|
238
|
+
for (let f of structRef.fields) {
|
|
239
|
+
let comment = f.defaultValue ? ` /* = ${emitFieldDefaultInComment(f.defaultValue)} */` : '';
|
|
240
|
+
let fTy = typeArgs ? instantiateGenerics(f.ty, structRef.typeParams, typeArgs) : f.ty;
|
|
241
|
+
declBuf.push(`${safeFieldDecl(f.name)}${f.defaultValue && isDefaultValueSupported(fTy) ? '?' : ''}: ${emitTsType(ctx, fTy)}${comment}`);
|
|
242
|
+
}
|
|
243
|
+
declBuf.push(`}`);
|
|
244
|
+
return declBuf.toString();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Generate `static fromStorage()` method for a storage of type {ty}.
|
|
248
|
+
// Most likely, it's a struct, then a caller can just use `MyContract.fromStorage({ fields })`.
|
|
249
|
+
function generateFromStorageMethod(ctx: CodegenCtx, ty: Ty | undefined, contractName: string): string {
|
|
250
|
+
if (ty == null || ty.kind === 'nullLiteral') { // means "no storage, don't emit a method"
|
|
251
|
+
return '';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let storageParamT = emitTsType(ctx, ty);
|
|
255
|
+
let bodyArg = 'emptyStorage';
|
|
256
|
+
if (ty.kind === 'StructRef') {
|
|
257
|
+
storageParamT = structParameterNoLabel(ctx, ty.structName, ty.typeArgs);
|
|
258
|
+
bodyArg = emitCallToCreateMethodExpr(ctx, 'emptyStorage', ty);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let buf = new OutBuf();
|
|
262
|
+
buf.push(`static fromStorage(emptyStorage: ${storageParamT}, deployedOptions?: DeployedAddrOptions) {`);
|
|
263
|
+
buf.push(`const initialState = {`);
|
|
264
|
+
buf.push(`code: deployedOptions?.overrideContractCode ?? ${contractName}.CodeCell,`);
|
|
265
|
+
buf.push(`data: ${emitMakeCellFromExpr(ctx, bodyArg, ty)},`);
|
|
266
|
+
buf.push(`};`);
|
|
267
|
+
buf.push(`const address = calculateDeployedAddress(initialState.code, initialState.data, deployedOptions ?? {});`);
|
|
268
|
+
buf.push(`return new ${contractName}(address, initialState);`);
|
|
269
|
+
buf.push(`}`);
|
|
270
|
+
return buf.toString();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Generate a `createCellOfXXX` static method for a contract class.
|
|
274
|
+
// It's essentially a wrapper for `XXX.toCell(XXX.create(body))`, but quite handy for the end user,
|
|
275
|
+
// to make a cell for TON Connect. In practice, `toCell` is needed almost for messages only.
|
|
276
|
+
function generateCreateCellOfMessageMethod(ctx: CodegenCtx, ty: Ty): string {
|
|
277
|
+
let functionName = snakeCaseToCamelCase(safeJsIdent(`createCellOf_${renderTy(ty)}`));
|
|
278
|
+
|
|
279
|
+
let bodyParamT = emitTsType(ctx, ty);
|
|
280
|
+
let bodyArg = 'body';
|
|
281
|
+
if (ty.kind === 'StructRef') {
|
|
282
|
+
bodyParamT = structParameterNoLabel(ctx, ty.structName, ty.typeArgs);
|
|
283
|
+
bodyArg = emitCallToCreateMethodExpr(ctx, 'body', ty);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let buf = new OutBuf();
|
|
287
|
+
buf.push(`static ${functionName}(body: ${bodyParamT}) {`);
|
|
288
|
+
buf.push(`return ${emitMakeCellFromExpr(ctx, bodyArg, ty)};`);
|
|
289
|
+
buf.push(`}`);
|
|
290
|
+
return buf.toString();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Generate a `sendXXX` method for a message of type {ty}.
|
|
294
|
+
// Most likely, it's a struct (an instantiated one also possible), but it can also be an alias for example.
|
|
295
|
+
function generateSendMethod(ctx: CodegenCtx, ty: Ty): string {
|
|
296
|
+
let functionName = snakeCaseToCamelCase(safeJsIdent(`send_${renderTy(ty)}`));
|
|
297
|
+
|
|
298
|
+
let bodyParamT = emitTsType(ctx, ty);
|
|
299
|
+
let bodyArg = 'body';
|
|
300
|
+
if (ty.kind === 'StructRef') {
|
|
301
|
+
bodyParamT = structParameterNoLabel(ctx, ty.structName, ty.typeArgs);
|
|
302
|
+
bodyArg = emitCallToCreateMethodExpr(ctx, 'body', ty);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let buf = new OutBuf();
|
|
306
|
+
buf.push(`async ${functionName}(provider: ContractProvider, via: Sender, msgValue: coins, body: ${bodyParamT}, extraOptions?: ${RUNTIME.ExtraSendOptions}) {`);
|
|
307
|
+
buf.push(`return provider.internal(via, {`);
|
|
308
|
+
buf.push(`value: msgValue,`);
|
|
309
|
+
buf.push(`body: ${emitMakeCellFromExpr(ctx, bodyArg, ty)},`);
|
|
310
|
+
buf.push(`...extraOptions`);
|
|
311
|
+
buf.push(`});`);
|
|
312
|
+
buf.push(`}`);
|
|
313
|
+
return buf.toString();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Generate a `getXXX` method: a contract getter that works off-chain, via the stack.
|
|
317
|
+
function generateGetMethod(ctx: CodegenCtx, getM: ABIGetMethod): string {
|
|
318
|
+
let functionName = snakeCaseToCamelCase(safeJsIdent('get_' + getM.name));
|
|
319
|
+
functionName = functionName.replace(/^getGet/, 'get');
|
|
320
|
+
|
|
321
|
+
let nParams = getM.parameters.length;
|
|
322
|
+
let paramNames = getM.parameters.map(p => {
|
|
323
|
+
let collision = Object.values(RUNTIME).some(n => p.name === n)
|
|
324
|
+
|| p.name === 'c' // import @ton/core
|
|
325
|
+
|| p.name === 'r' // StackReader in body
|
|
326
|
+
|| p.name === 'provider' // implicit parameter
|
|
327
|
+
return safeJsIdent(collision ? p.name + '_' : p.name)
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
let buf = new OutBuf();
|
|
331
|
+
let paramsStr = nParams ? `, ${getM.parameters.map((p, i) =>
|
|
332
|
+
`${paramNames[i]}: ${emitTsType(ctx, p.ty)}${p.defaultValue ? ` = ${emitFieldDefault(ctx, p.defaultValue)}` : ''}`
|
|
333
|
+
).join(', ')}` : ''
|
|
334
|
+
if (getM.returnTy.kind === 'tensor') {
|
|
335
|
+
buf.push(`async ${functionName}(provider: ContractProvider${paramsStr}): Promise<[`);
|
|
336
|
+
for (let item of getM.returnTy.items) {
|
|
337
|
+
buf.push(emitTsType(ctx, item) + ',')
|
|
338
|
+
}
|
|
339
|
+
buf.push(']> {')
|
|
340
|
+
} else {
|
|
341
|
+
buf.push(`async ${functionName}(provider: ContractProvider${paramsStr}): Promise<${emitTsType(ctx, getM.returnTy)}> {`);
|
|
342
|
+
}
|
|
343
|
+
let stackW = calcWidthOnStack(ctx.symbols, getM.returnTy);
|
|
344
|
+
if (nParams) {
|
|
345
|
+
buf.push(`const r = ${RUNTIME.StackReader}.fromGetMethod(${stackW}, await provider.get('${getM.name}', [`);
|
|
346
|
+
let stackWrites = getM.parameters.flatMap((p, i) => emitStackWriteItems(ctx, paramNames[i], p.ty))
|
|
347
|
+
for (let w of stackWrites) {
|
|
348
|
+
buf.push(w + ',');
|
|
349
|
+
}
|
|
350
|
+
buf.push(`]));`);
|
|
351
|
+
} else {
|
|
352
|
+
buf.push(`const r = ${RUNTIME.StackReader}.fromGetMethod(${stackW}, await provider.get('${getM.name}', []));`);
|
|
353
|
+
}
|
|
354
|
+
buf.push(`return ${emitStackReadExpr(ctx, 'result', getM.returnTy)};`);
|
|
355
|
+
buf.push(`}`);
|
|
356
|
+
return buf.toString();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Render a template of a TypeScript wrapper.
|
|
360
|
+
function renderTsTemplate(tplStr: string, content: {
|
|
361
|
+
contractClassName: string
|
|
362
|
+
flags: Record<string, boolean> // {{if:flag}} ... {{/if:flag}}
|
|
363
|
+
slots: Record<string, string> // {{slot}}
|
|
364
|
+
}): string {
|
|
365
|
+
// {{if:...}} blocks: keep blank lines before it if true
|
|
366
|
+
const IF_BLOCK_RE = /(^(?:\s*\n)*)\s*\/\/\s*{{if:(\w+)}}[^\n]*\n([\s\S]+?)^\s*\/\/\s*{{\/if:\2}}[^\n]*/gm;
|
|
367
|
+
tplStr = tplStr.replace(IF_BLOCK_RE, (_m, leadingBlank: string, name: string, inner: string) =>
|
|
368
|
+
content.flags[name] ? leadingBlank + inner : ''
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// {{slot}}: keep indentation, remove a commented marker
|
|
372
|
+
const SLOT_RE = /(^ *)(?:\/\/\s*)?{{(\w+)}}[^\n]*$/gm;
|
|
373
|
+
tplStr = tplStr.replace(SLOT_RE, (_m, indent: string, name: string) =>
|
|
374
|
+
content.slots[name].split('\n').map(l => l.length ? indent + l : '').join('\n')
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
// {{slot}} inlined (without a preceding comment)
|
|
378
|
+
tplStr = tplStr.replace(/{{(\w+)}}/g, (_m, name: string) => content.slots[name]);
|
|
379
|
+
|
|
380
|
+
return tplStr
|
|
381
|
+
.replace(/\n\s*\n(\s*})/g, '\n$1') // remove an empty line before closing brace
|
|
382
|
+
.replace(/\n\s*\n(?:\s*\n)+/g, '\n\n') // merge 2+ blank lines
|
|
383
|
+
.replace(/CONTRACT_CLASS_NAME/g, content.contractClassName);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* The main function: accepts top-level declarations from Tolk sources (converted to ABI)
|
|
388
|
+
* and returns the full TypeScript wrapper with all serializers, send, and get methods.
|
|
389
|
+
* @throws CantGenerateWrappersAtAll
|
|
390
|
+
*/
|
|
391
|
+
export function generateTypeScriptFileForContract(contract: ContractABI): string {
|
|
392
|
+
let ctx = new CodegenCtx(contract.declarations);
|
|
393
|
+
|
|
394
|
+
const declarationsBuf = new OutBuf();
|
|
395
|
+
for (const n of contract.declarations) {
|
|
396
|
+
try {
|
|
397
|
+
if (n.kind === 'Struct') tolkStructToTypeScript(ctx, n, declarationsBuf);
|
|
398
|
+
else if (n.kind === 'Alias') tolkAliasToTypeScript(ctx, n, declarationsBuf);
|
|
399
|
+
else if (n.kind === 'Enum') tolkEnumToTypeScript(ctx, n, declarationsBuf);
|
|
400
|
+
declarationsBuf.emptyLine();
|
|
401
|
+
} catch (ex: any) {
|
|
402
|
+
throw new CantGenerateWrappersAtAll(`Error while generating ${n.kind.toLowerCase()} '${n.name}'`, ex);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
let packUnpackSerializers = declarationsBuf.toString();
|
|
406
|
+
|
|
407
|
+
let createCellsMethodsStr = contract.incomingMessages.map(msg =>
|
|
408
|
+
generateCreateCellOfMessageMethod(ctx, msg.bodyTy)
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
let sendMethodsStr = contract.incomingMessages.map(msg => {
|
|
412
|
+
try {
|
|
413
|
+
return generateSendMethod(ctx, msg.bodyTy);
|
|
414
|
+
} catch (ex: any) {
|
|
415
|
+
throw new CantGenerateWrappersAtAll(`Error while generating send method '${renderTy(msg.bodyTy)}'`, ex);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
let getMethodsStr = contract.getMethods.map(getM => {
|
|
420
|
+
try {
|
|
421
|
+
return generateGetMethod(ctx, getM);
|
|
422
|
+
} catch (ex: any) {
|
|
423
|
+
throw new CantGenerateWrappersAtAll(`Error while generating get method '${getM.name}'`, ex);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
let intNAliasesStr = ctx.sortOccurred(ctx.intNOccurred).map(intN => `type ${intN} = bigint`);
|
|
428
|
+
let uintNAliasesStr = ctx.sortOccurred(ctx.uintNOccurred).map(uintN => `type ${uintN} = bigint`);
|
|
429
|
+
let varIntNAliasesStr = ctx.sortOccurred(ctx.varIntNOccurred).map(varIntN => `type ${varIntN} = bigint`);
|
|
430
|
+
let sliceAliasesStr = ctx.sortOccurred(ctx.bitsNOccurred).map(bitsN => `type ${bitsN} = c.Slice`);
|
|
431
|
+
|
|
432
|
+
let errorCodesStr = contract.thrownErrors.filter(t => t.name).map(t =>
|
|
433
|
+
`'${t.name}': ${t.errCode},`
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
return renderTsTemplate(fs.readFileSync(__dirname + '/out-template.ts', 'utf-8'), {
|
|
437
|
+
contractClassName: contract.contractName,
|
|
438
|
+
flags: {
|
|
439
|
+
'has_sendDeploy': true,
|
|
440
|
+
'has_bitsN': ctx.bitsNOccurred.size > 0,
|
|
441
|
+
'has_varIntN': ctx.varIntNOccurred.size > 0,
|
|
442
|
+
'has_remaining': ctx.has_RemainingBitsAndRefs,
|
|
443
|
+
'has_customPackUnpack': ctx.has_customPackUnpack,
|
|
444
|
+
'has_customDictV': ctx.has_customDictV,
|
|
445
|
+
'has_implicitUnionPrefix': ctx.has_implicitUnionPrefix,
|
|
446
|
+
'has_non32Prefixes': ctx.has_non32Prefixes,
|
|
447
|
+
'has_addressAny': ctx.has_addressAny,
|
|
448
|
+
'has_arrayOf': ctx.has_arrayOf,
|
|
449
|
+
'has_lispListOf': ctx.has_lispListOf,
|
|
450
|
+
'stackReadsUnknown': ctx.stackReadsUnknown,
|
|
451
|
+
'stackReadsArrayOf': ctx.stackReadsArrayOf,
|
|
452
|
+
'stackReadsLispListOf': ctx.stackReadsLispListOf,
|
|
453
|
+
'stackReadsSnakeString': ctx.stackReadsSnakeString,
|
|
454
|
+
'stackReadsTuple': ctx.stackReadsTuple,
|
|
455
|
+
'stackReadsMapKV': ctx.stackReadsMapKV,
|
|
456
|
+
'stackReadsBuilder': ctx.stackReadsBuilder,
|
|
457
|
+
'stackReadsNullable': ctx.stackReadsNullable,
|
|
458
|
+
'stackReadsWideNullable': ctx.stackReadsWideNullable,
|
|
459
|
+
'stackReadsUnionType': ctx.stackReadsUnionType,
|
|
460
|
+
'stackReadsCellRef': ctx.stackReadsCellRef,
|
|
461
|
+
'stackReadsNullLiteral': ctx.stackReadsNullLiteral,
|
|
462
|
+
},
|
|
463
|
+
slots: {
|
|
464
|
+
'intNAliases': intNAliasesStr.join('\n'),
|
|
465
|
+
'uintNAliases': uintNAliasesStr.join('\n'),
|
|
466
|
+
'varIntNAliases': varIntNAliasesStr.join('\n'),
|
|
467
|
+
'sliceAliases': sliceAliasesStr.join('\n'),
|
|
468
|
+
'packUnpackSerializers': packUnpackSerializers,
|
|
469
|
+
'codeBoc64': contract.codeBoc64,
|
|
470
|
+
'errorCodes': errorCodesStr.join('\n'),
|
|
471
|
+
'fromStorageMethod': generateFromStorageMethod(ctx, contract.storage.storageAtDeploymentTy ?? contract.storage.storageTy, contract.contractName),
|
|
472
|
+
'createCellsMethods': createCellsMethodsStr.join('\n\n'),
|
|
473
|
+
'sendMethods': sendMethodsStr.join('\n\n'),
|
|
474
|
+
'getMethods': getMethodsStr.join('\n\n'),
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|