Skip to content

Commit 278f4dc

Browse files
adg-flareLukaAvbreht
authored andcommitted
feat: add dto generation script
1 parent a119a50 commit 278f4dc

11 files changed

Lines changed: 366 additions & 63 deletions

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"start:debug": "nest start --debug --watch",
1717
"start:prod": "node dist/main",
1818
"docker:build": "docker build --build-arg PROJECT_VERSION=\"$(git describe --tags --always)\" --build-arg PROJECT_COMMIT_HASH=\"$(git rev-parse HEAD)\" -t verifier-indexer-api:local .",
19+
"generate:dto": "ts-node scripts/dto/generate.ts",
1920
"-----Linting-----": "echo \"Linting scripts\"",
2021
"lint:fix": "eslint \"{src,test}/**/*.ts\" --fix",
2122
"lint:check": "eslint \"{src,test}/**/*.ts\"",
@@ -26,6 +27,7 @@
2627
},
2728
"dependencies": {
2829
"@braintree/sanitize-url": "^7.1.1",
30+
"@flarenetwork/js-flare-common": "0.0.2",
2931
"@flarenetwork/mcc": "4.4.0",
3032
"@jq-tools/jq": "^0.0.11",
3133
"@nestjs/common": "^10.0.0",
@@ -39,8 +41,8 @@
3941
"base-x": "^5.0.0",
4042
"class-transformer": "^0.5.1",
4143
"class-validator": "^0.14.1",
42-
"express": "^4.19.2",
4344
"ethers": "^6.13.2",
45+
"express": "^4.19.2",
4446
"helmet": "^7.1.0",
4547
"passport": "^0.7.0",
4648
"passport-headerapikey": "^1.2.2",

pnpm-lock.yaml

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/dto/generate.ts

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
/* eslint-disable no-console */
2+
import { Options, format } from 'prettier';
3+
import * as fs from 'fs';
4+
import * as path from 'path';
5+
import {
6+
ParamRecord,
7+
StructRecord,
8+
TypeRecord,
9+
} from './types';
10+
import {
11+
DEFAULT_ATTESTATION_TYPE_CONFIGS_PATH,
12+
encodeAttestationName,
13+
} from '@flarenetwork/js-flare-common';
14+
15+
const ROOT = process.cwd();
16+
const TYPE_DEFS_DIR = DEFAULT_ATTESTATION_TYPE_CONFIGS_PATH;
17+
const DTO_DIR = path.join(ROOT, 'src/dtos/attestation-types');
18+
19+
function readJson(filePath: string): unknown {
20+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
21+
}
22+
23+
const PRETTIER_SETTINGS_NEST_JS_DTO: Options = {
24+
trailingComma: 'all',
25+
tabWidth: 2,
26+
printWidth: 120,
27+
semi: true,
28+
singleQuote: true,
29+
parser: 'typescript',
30+
};
31+
32+
function JSDocCommentText(text?: string): string {
33+
if (!text || text.trim() === '') return '';
34+
return `/**
35+
* ${text.trim().replace(/\r/g, '').replace(/\n/g, '\n * ').replace(/\*\//g, '* /')}
36+
*/`;
37+
}
38+
39+
const attestationResponseStatusEnum = `
40+
/**
41+
* Attestation status
42+
*/
43+
export enum AttestationResponseStatus {
44+
/**
45+
* Attestation request is valid.
46+
*/
47+
VALID = 'VALID',
48+
/**
49+
* Attestation request is invalid.
50+
*/
51+
INVALID = 'INVALID',
52+
/**
53+
* Attestation request cannot be confirmed neither rejected by the verifier at the moment.
54+
*/
55+
INDETERMINATE = 'INDETERMINATE',
56+
}
57+
`;
58+
59+
function solidityToDTOTypeInitialized(
60+
typeName: string,
61+
attestationTypeName: string,
62+
): string {
63+
const match = typeName.match(/^(.+)(\[\d*])$/);
64+
if (match) {
65+
return (
66+
solidityToDTOTypeInitialized(match[1], attestationTypeName) + '[]'
67+
);
68+
}
69+
if (typeName.match(/^u?int\d+$/)) return 'string';
70+
if (typeName.match(/^bool$/)) return 'boolean';
71+
if (typeName.match(/^bytes\d*$/)) return 'string';
72+
if (typeName.match(/^address$/)) return 'string';
73+
if (typeName.match(/^string$/)) return 'string';
74+
if (typeName.match(/^byte$/)) return 'string';
75+
const structMatch = typeName.match(/^struct ([\w.]+)$/);
76+
if (structMatch) {
77+
const name = structMatch[1];
78+
return attestationTypeName + '_' + name.split('.').slice(-1)[0];
79+
}
80+
throw new Error(`Unknown type ${typeName}`);
81+
}
82+
83+
function validationAnnotation(
84+
typeName: string,
85+
typeNameSimple: string,
86+
attestationTypeName: string,
87+
isArray = false,
88+
): string {
89+
const arrayOpts = isArray ? '{ each: true }' : '';
90+
const arrayOptsComma = isArray ? ', { each: true }' : '';
91+
if (typeName.match(/^u?int\d+$/))
92+
return `@Validate(IsUnsignedIntLike${arrayOptsComma})`;
93+
if (typeName.match(/^int\d+$/))
94+
return `@Validate(IsUnsignedIntLike${arrayOptsComma})`;
95+
if (typeName.match(/^bool$/)) return `@IsBoolean(${arrayOpts})`;
96+
if (typeName.match(/^bytes32$/))
97+
return `@Validate(IsHash32${arrayOptsComma})`;
98+
if (typeName.match(/^bytes\d*$/))
99+
return `@Validate(Is0xHex${arrayOptsComma})`;
100+
if (typeName.match(/^address$/))
101+
return `@Validate(IsEVMAddress${arrayOptsComma})`;
102+
if (typeName.match(/^string$/)) return `@IsString(${arrayOpts})`;
103+
if (typeName.match(/^byte$/))
104+
return `@Validate(Is0xHex${arrayOptsComma})`;
105+
if (typeName.startsWith('struct ')) {
106+
const isEmptyObject = isArray ? '' : '\n @IsNotEmptyObject()';
107+
return `@ValidateNested(${arrayOpts})
108+
@Type(() => ${attestationTypeName}_${typeNameSimple})
109+
@IsDefined(${arrayOpts})${isEmptyObject}
110+
@IsObject(${arrayOpts})`;
111+
}
112+
throw new Error(`Unknown type ${typeName}`);
113+
}
114+
115+
function exampleFor(
116+
typeName: string,
117+
typeNameSimple: string,
118+
isArray = false,
119+
): string {
120+
const left = isArray ? '[' : '';
121+
const right = isArray ? ']' : '';
122+
if (typeName.match(/^uint\d+$/)) return `${left}'123'${right}`;
123+
if (typeName.match(/^int\d+$/)) return `${left}'123'${right}`;
124+
if (typeName.match(/^bool$/)) return `${left}true${right}`;
125+
if (typeName.match(/^bytes32$/))
126+
return `${left}'0x0000000000000000000000000000000000000000000000000000000000000000'${right}`;
127+
if (typeName.match(/^bytes\d*$/)) return `${left}'0x1234abcd'${right}`;
128+
if (typeName.match(/^address$/))
129+
return `${left}'0x5d4BEB38B6b71aaF6e30D0F9FeB6e21a7Ac40b3a'${right}`;
130+
if (typeName.match(/^string$/)) return `${left}'Example string'${right}`;
131+
if (typeName.match(/^byte$/)) return `${left}'0x12'${right}`;
132+
if (typeName.startsWith('struct ')) return '';
133+
throw new Error(`Unknown type '${typeName}' / '${typeNameSimple}'`);
134+
}
135+
136+
function commentsAndAnnotations(
137+
paramRec: ParamRecord,
138+
attestationTypeName: string,
139+
structName: string,
140+
): string {
141+
const typeName = paramRec.type;
142+
const typeNameSimple = paramRec.typeSimple || paramRec.type;
143+
const comment = JSDocCommentText(paramRec.comment);
144+
const description = paramRec.comment
145+
.replace(/\`/g, "'")
146+
.replace(/\r/g, '')
147+
.replace(/\n/g, ' ');
148+
const match = typeName.match(/^(.+)(\[\d*])$/);
149+
150+
if (match) {
151+
const match2 = typeNameSimple.match(/^(.+)(\[\d*])$/);
152+
if (!match2) {
153+
throw new Error(
154+
`Unexpected type name '${typeNameSimple}' for '${typeName}'.`,
155+
);
156+
}
157+
const apiPropertyAnnotation = typeName.startsWith('struct ')
158+
? `@ApiProperty({ description: \`${description}\` })`
159+
: `@ApiProperty({ description: \`${description}\`, example: ${exampleFor(match[1], match2[1], true)} })`;
160+
const annotations = `${validationAnnotation(match[1], match2[1], attestationTypeName, true)}
161+
${apiPropertyAnnotation}`;
162+
return comment ? `${comment}\n ${annotations}` : ` ${annotations}`;
163+
}
164+
165+
if (
166+
attestationTypeName === 'Web2Json' &&
167+
structName === 'RequestBody' &&
168+
paramRec.name === 'httpMethod'
169+
) {
170+
const annotations = `@IsEnum(HTTP_METHOD)
171+
@ApiProperty({ description: \`${description}\`, example: 'GET', enum: HTTP_METHOD })`;
172+
return comment ? `${comment}\n ${annotations}` : ` ${annotations}`;
173+
}
174+
175+
if (typeName.match(/^bytes32$/)) {
176+
let example;
177+
if (paramRec.name === 'attestationType') {
178+
example = `'${encodeAttestationName(attestationTypeName)}'`;
179+
} else if (paramRec.name === 'sourceId') {
180+
example = `'${encodeAttestationName('DOGE')}'`;
181+
} else {
182+
example = exampleFor(typeName, typeNameSimple);
183+
}
184+
const annotations = `${validationAnnotation(typeName, typeNameSimple, attestationTypeName)}
185+
@Transform(transformHash32)
186+
@ApiProperty({ description: \`${description}\`, example: ${example} })`;
187+
return comment ? `${comment}\n ${annotations}` : ` ${annotations}`;
188+
}
189+
190+
if (typeName.startsWith('struct ')) {
191+
const annotations = `${validationAnnotation(typeName, typeNameSimple, attestationTypeName)}
192+
@ApiProperty({ description: \`${description}\` })`;
193+
return comment ? `${comment}\n ${annotations}` : ` ${annotations}`;
194+
}
195+
196+
const annotations = `${validationAnnotation(typeName, typeNameSimple, attestationTypeName)}
197+
@ApiProperty({ description: \`${description}\`, example: ${exampleFor(typeName, typeNameSimple)} })`;
198+
return comment ? `${comment}\n ${annotations}` : ` ${annotations}`;
199+
}
200+
201+
function paramFormat(
202+
param: ParamRecord,
203+
attestationTypeName: string,
204+
structName: string,
205+
) {
206+
if (param.name === 'messageIntegrityCode') return '';
207+
const resolvedType =
208+
attestationTypeName === 'Web2Json' &&
209+
structName === 'RequestBody' &&
210+
param.name === 'httpMethod'
211+
? 'HTTP_METHOD'
212+
: solidityToDTOTypeInitialized(param.type, attestationTypeName);
213+
214+
return `${commentsAndAnnotations(param, attestationTypeName, structName)}
215+
${param.name}: ${resolvedType};`;
216+
}
217+
218+
function structType(structRec: StructRecord, attestationTypeName: string): string {
219+
return `export class ${attestationTypeName}_${structRec.name} {
220+
constructor(params: Required<${attestationTypeName}_${structRec.name}>) {
221+
Object.assign(this, params);
222+
}
223+
224+
${structRec.params
225+
.map((param) => paramFormat(param, attestationTypeName, structRec.name))
226+
.filter(Boolean)
227+
.join('\n\n')}
228+
}`;
229+
}
230+
231+
const autoGenerateCodeNotice = `
232+
//////////////////////////////////////////////////////////////////////////////////////////
233+
/////// THIS CODE IS AUTOGENERATED. DO NOT CHANGE!!! /////////
234+
//////////////////////////////////////////////////////////////////////////////////////////
235+
`;
236+
237+
function attestationResponseDTOSpecific(name: string) {
238+
if (name === 'EVMTransaction') return '';
239+
return `
240+
/**
241+
* Attestation response for specific attestation type (flattened)
242+
*/
243+
export class AttestationResponseDTO_${name}_Response {
244+
constructor(params: Required<AttestationResponseDTO_${name}_Response>) {
245+
Object.assign(this, params);
246+
}
247+
248+
status: AttestationResponseStatus;
249+
250+
response?: ${name}_Response;
251+
}
252+
`;
253+
}
254+
255+
export function getDTOsForName(name: string, typeRec: TypeRecord): string {
256+
const reversedRequestStructs = [...(typeRec.requestStructs || [])].reverse();
257+
const reversedResponseStructs = [...(typeRec.responseStructs || [])].reverse();
258+
const hasHttpMethod =
259+
name === 'Web2Json' &&
260+
(typeRec.requestBody.params || []).some((p) => p.name === 'httpMethod');
261+
const hasStatusEnum = name !== 'EVMTransaction';
262+
const hasScalarHash32 = [
263+
...(typeRec.requestStructs || []),
264+
...(typeRec.responseStructs || []),
265+
typeRec.responseBody,
266+
typeRec.requestBody,
267+
typeRec.request,
268+
typeRec.response,
269+
typeRec.proof,
270+
]
271+
.filter(Boolean)
272+
.some((s) => (s.params || []).some((p) => p.type === 'bytes32'));
273+
274+
return (
275+
(name === 'EVMTransaction' ? autoGenerateCodeNotice : '') +
276+
`import { ApiProperty } from '@nestjs/swagger';
277+
import { ${hasScalarHash32 ? 'Transform, ' : ''}Type } from 'class-transformer';
278+
import { IsBoolean, IsDefined, IsNotEmptyObject, IsObject, IsString, Validate, ValidateNested${hasHttpMethod ? ', IsEnum' : ''} } from 'class-validator';
279+
import { Is0xHex, IsEVMAddress, IsHash32, IsUnsignedIntLike } from '../dto-validators';
280+
${hasScalarHash32 ? "import { transformHash32 } from '../dto-transform-utils';" : ''}
281+
${hasStatusEnum ? "import { AttestationResponseStatus } from '../../verification/response-status';" : ''}
282+
${hasHttpMethod ? "import { HTTP_METHOD } from '../../config/interfaces/web2-json';" : ''}
283+
${name === 'EVMTransaction' ? attestationResponseStatusEnum : ''}
284+
285+
///////////////////////////////////////////////////////////////////////////////////////////////
286+
//////////////////////////////////// DTOs /////////////////////////////////////////////////////
287+
///////////////////////////////////////////////////////////////////////////////////////////////
288+
${attestationResponseDTOSpecific(name)}
289+
${reversedRequestStructs.map((struct) => structType(struct, name)).join('\n\n')}
290+
${reversedResponseStructs.map((struct) => structType(struct, name)).join('\n\n')}
291+
${structType(typeRec.responseBody, name)}
292+
${structType(typeRec.requestBody, name)}
293+
${structType(typeRec.request, name)}
294+
${structType(typeRec.response, name)}
295+
${structType(typeRec.proof, name)}
296+
`
297+
);
298+
}
299+
300+
async function main() {
301+
const args = process.argv.slice(2);
302+
const outDirArg = args.indexOf('--out-dir');
303+
const outDir =
304+
outDirArg >= 0 && args[outDirArg + 1] ? path.resolve(args[outDirArg + 1]) : DTO_DIR;
305+
306+
const defs = fs
307+
.readdirSync(TYPE_DEFS_DIR)
308+
.filter((f) => f.endsWith('.json'))
309+
.sort()
310+
.map((f) => readJson(path.join(TYPE_DEFS_DIR, f)) as TypeRecord);
311+
312+
if (!fs.existsSync(outDir)) {
313+
fs.mkdirSync(outDir, { recursive: true });
314+
}
315+
316+
for (const def of defs) {
317+
let content = getDTOsForName(def.name, def);
318+
content = await format(content, PRETTIER_SETTINGS_NEST_JS_DTO);
319+
const dtoPath = path.join(outDir, `${def.name}.dto.ts`);
320+
fs.writeFileSync(dtoPath, content);
321+
console.log(`generated ${path.relative(ROOT, dtoPath)}`);
322+
}
323+
}
324+
325+
main();
Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,3 @@ export interface StructRecord {
127127
*/
128128
params: ParamRecord[];
129129
}
130-
131-
export interface AttestationTypeProtocolSupport {
132-
attestationType: string;
133-
supportedDataSources: string[];
134-
}

0 commit comments

Comments
 (0)