-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsolidityGenerator.js
More file actions
519 lines (455 loc) · 21.5 KB
/
solidityGenerator.js
File metadata and controls
519 lines (455 loc) · 21.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
'use strict';
import fs from 'fs';
/**
* Reads and parses the JSON file containing contract definitions.
* @param {string} filename - The JSON file name.
* @returns {Object} The parsed contracts object.
*/
function loadContracts(filename) {
const rawData = fs.readFileSync(filename, 'utf8');
const program = JSON.parse(rawData);
return program.contracts;
}
/**
* Processes the field types for each contract.
* – Converts a field type of 'image' to 'string'
* – If a field’s type matches another contract name then converts it to 'address'
* and records the reference.
*
* @param {Object} contracts - All contract definitions.
* @returns {Object} An object containing:
* - contractReferences: mapping of a contract name to an array of referenced contract names.
* - fieldLookup: mapping to later find the field name by parent/reference.
*/
function processFieldTypes(contracts) {
const contractReferences = {};
const fieldLookup = {};
for (const contractName in contracts) {
const contract = contracts[contractName];
const fields = contract.fields;
for (const fieldName in fields) {
let fieldType = fields[fieldName];
// Convert 'image' types to 'string'
if (fieldType === 'image') {
fields[fieldName] = 'string';
}
// If the field type is one of the contract names, treat it as a reference.
if (Object.keys(contracts).includes(fieldType)) {
fields[fieldName] = 'address';
if (!contractReferences[contractName]) {
contractReferences[contractName] = [];
}
contractReferences[contractName].push(fieldType);
if (!fieldLookup[contractName]) {
fieldLookup[contractName] = {};
}
fieldLookup[contractName][fieldType] = fieldName;
}
}
}
return { contractReferences, fieldLookup };
}
/**
* Creates an additional mapping (fields_types) for each contract
* that adds "memory" to string types (and to 'image' if still present).
*
* @param {Object} contracts - All contract definitions.
*/
function applyMemoryToStringFields(contracts) {
for (const contractName in contracts) {
const contract = contracts[contractName];
contract.fields_types = {};
for (const fieldName in contract.fields) {
const fieldType = contract.fields[fieldName];
if (fieldType === 'image' || fieldType === 'string') {
contract.fields_types[fieldName] = 'string memory';
} else {
contract.fields_types[fieldName] = fieldType;
}
}
}
}
/**
* Generates the complete Solidity source code based on the contracts.
*
* @param {Object} contracts - All contract definitions.
* @param {Object} contractReferences - References detected between contracts.
* @param {Object} fieldLookup - Lookup for referenced field names.
* @returns {string} The Solidity source code.
*/
function generateSolidityCode(contracts, contractReferences, fieldLookup) {
// Updated header with Solidity 0.8.20 (ABIEncoderV2 not needed anymore).
let code = `
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
`;
// Generate individual contract definitions.
for (const contractName in contracts) {
code += generateContractDefinition(contractName, contracts[contractName]);
}
// Generate the App contract that ties all contracts together.
code += generateAppContract(contracts, contractReferences, fieldLookup);
return code;
}
/**
* Generates the Solidity code for an individual contract.
*
* This version:
* - Declares state variables (with auto–generated getters as public if desired).
* - Uses a constructor that accepts parameters with proper data locations (e.g. "string memory").
* - Declares a bundled struct (ContractNameData) that contains:
* • address self;
* • Each field from readRules.gets (using its original type, without a location).
* - Provides a single getAll() function returning that struct.
*
* @param {string} contractName - The contract name.
* @param {Object} contract - The contract definition.
* @returns {string} The Solidity code for this contract.
*/
function generateContractDefinition(contractName, contract) {
let contractCode = `contract ${contractName}_contract {\n\n`;
// Declare state variables.
for (const field in contract.fields) {
const fieldType = contract.fields[field];
contractCode += `\t${fieldType} public ${field};\n`;
}
contractCode += `\n`;
// Auto fields that were previously implemented via tx.origin inside the child
// contract must be set by the App (caller) to avoid using tx.origin and to
// preserve the originating caller address.
const auto = contract.initRules?.auto || {};
const callerAutoFields = Object.entries(auto)
.filter(([, expr]) => expr === 'tx.origin' || expr === 'msg.sender' || expr === '_msgSender()')
.map(([field]) => field)
// Avoid collisions if a field is also listed in passIn.
.filter(field => !contract.initRules.passIn.includes(field));
// Build the constructor parameters based on initRules.passIn.
// Use "string memory" for strings (or images) instead of "image memory".
const callerAutoParams = callerAutoFields
.map(field => {
const fieldType = contract.fields[field];
const type = (fieldType === 'string' || fieldType === 'image') ? 'string memory' : contract.fields_types[field];
return `${type} _${field}`;
})
.join(', ');
const passInParams = contract.initRules.passIn
.map(field => {
let type;
if (contract.fields[field] === 'string' || contract.fields[field] === 'image') {
type = 'string memory';
} else {
type = contract.fields_types[field];
}
return `${type} _${field}`;
})
.join(', ');
const ctorParams = [callerAutoParams, passInParams].filter(Boolean).join(', ');
contractCode += `\tconstructor(${ctorParams}) {\n`;
// Initialize auto-assigned fields.
for (const autoField in contract.initRules.auto) {
const value = contract.initRules.auto[autoField];
if (callerAutoFields.includes(autoField)) {
contractCode += `\t\t${autoField} = _${autoField};\n`;
} else if (value === 'tx.origin') {
// Defensive fallback: never emit tx.origin even if the schema still has it.
contractCode += `\t\t${autoField} = msg.sender;\n`;
} else {
contractCode += `\t\t${autoField} = ${value};\n`;
}
}
// Assign pass-in parameters to state variables.
contract.initRules.passIn.forEach(field => {
contractCode += `\t\t${field} = _${field};\n`;
});
contractCode += `\t}\n\n`;
// Generate bundled struct for getters.
contractCode += `\tstruct ${contractName}Data {\n`;
contractCode += `\t\taddress self;\n`;
contract.readRules.gets.forEach(field => {
const fieldType = contract.fields[field];
contractCode += `\t\t${fieldType} ${field};\n`;
});
contractCode += `\t}\n\n`;
// Generate a getAll() function returning the struct.
contractCode += `\tfunction getAll() external view returns (${contractName}Data memory) {\n`;
contractCode += `\t\treturn ${contractName}Data({\n`;
contractCode += `\t\t\tself: address(this),\n`;
contract.readRules.gets.forEach((field, index) => {
const comma = index === contract.readRules.gets.length - 1 ? '' : ',';
contractCode += `\t\t\t${field}: ${field}${comma}\n`;
});
contractCode += `\t\t});\n`;
contractCode += `\t}\n\n`;
contractCode += `}\n\n`;
return contractCode;
}
/**
* Generates the main App contract which:
* – Declares arrays and getter functions for each contract type.
* – Uses the bundled getAll() getters from the child contracts.
* – Declares user–related functions and reference mappings.
*
* In the getters, the return type is referenced as:
* ContractName_contract.ContractNameData
*
* @param {Object} contracts - All contract definitions.
* @param {Object} contractReferences - Contract reference mapping.
* @param {Object} fieldLookup - Field lookup for references.
* @returns {string} The Solidity code for the App contract.
*/
function generateAppContract(contracts, contractReferences, fieldLookup) {
let appCode = `contract App {\n\n`;
// For each contract, create list variables and getter functions.
for (const contractName in contracts) {
appCode += `\taddress[] public ${contractName}_list;\n\n`;
// Single-instance getter using getAll().
appCode += `\tfunction get_${contractName}_N(uint256 index) public view returns (${contractName}_contract.${contractName}Data memory) {\n`;
appCode += `\t\treturn ${contractName}_contract(${contractName}_list[index]).getAll();\n`;
appCode += `\t}\n\n`;
// Function to get the first N instances.
appCode += `\tfunction get_first_${contractName}_N(uint256 count, uint256 offset) public view returns (${contractName}_contract.${contractName}Data[] memory) {\n`;
appCode += `\t\trequire(offset + count <= ${contractName}_list.length, "Offset + count out of bounds");\n`;
appCode += `\t\t${contractName}_contract.${contractName}Data[] memory results = new ${contractName}_contract.${contractName}Data[](count);\n`;
appCode += `\t\tfor (uint i = 0; i < count; i++) {\n`;
appCode += `\t\t\tresults[i] = ${contractName}_contract(${contractName}_list[i + offset]).getAll();\n`;
appCode += `\t\t}\n`;
appCode += `\t\treturn results;\n`;
appCode += `\t}\n\n`;
// Function to get the last N instances.
appCode += `\tfunction get_last_${contractName}_N(uint256 count, uint256 offset) public view returns (${contractName}_contract.${contractName}Data[] memory) {\n`;
appCode += `\t\trequire(count + offset <= ${contractName}_list.length, "Count + offset out of bounds");\n`;
appCode += `\t\t${contractName}_contract.${contractName}Data[] memory results = new ${contractName}_contract.${contractName}Data[](count);\n`;
appCode += `\t\tuint len = ${contractName}_list.length;\n`;
appCode += `\t\tfor (uint i = 0; i < count; i++) {\n`;
appCode += `\t\t\tuint idx = len - i - offset - 1;\n`;
appCode += `\t\t\tresults[i] = ${contractName}_contract(${contractName}_list[idx]).getAll();\n`;
appCode += `\t\t}\n`;
appCode += `\t\treturn results;\n`;
appCode += `\t}\n\n`;
// Return length Number of instances
appCode += `\tfunction get_${contractName}_list_length() public view returns (uint256) { return ${contractName}_list.length; }\n`;
// User–related functions.
appCode += `\tfunction get_${contractName}_user_length(address user) public view returns (uint256) {\n`;
appCode += `\t\treturn user_map[user].${contractName}_list.length;\n`;
appCode += `\t}\n\n`;
appCode += `\tfunction get_${contractName}_user_N(address user, uint256 index) public view returns (${contractName}_contract.${contractName}Data memory) {\n`;
appCode += `\t\treturn ${contractName}_contract(user_map[user].${contractName}_list[index]).getAll();\n`;
appCode += `\t}\n\n`;
appCode += `\tfunction get_last_${contractName}_user_N(address user, uint256 count, uint256 offset) public view returns (${contractName}_contract.${contractName}Data[] memory) {\n`;
appCode += `\t\trequire(count + offset <= user_map[user].${contractName}_list.length, "Count + offset out of bounds");\n`;
appCode += `\t\t${contractName}_contract.${contractName}Data[] memory results = new ${contractName}_contract.${contractName}Data[](count);\n`;
appCode += `\t\tuint len = user_map[user].${contractName}_list.length;\n`;
appCode += `\t\tfor (uint i = 0; i < count; i++) {\n`;
appCode += `\t\t\tuint idx = len - i - offset - 1;\n`;
appCode += `\t\t\tresults[i] = ${contractName}_contract(user_map[user].${contractName}_list[idx]).getAll();\n`;
appCode += `\t\t}\n`;
appCode += `\t\treturn results;\n`;
appCode += `\t}\n\n`;
}
// Generate mapping structures and functions for contract references.
appCode += generateReferenceMappings(contractReferences, contracts, fieldLookup);
// Generate the UserInfo struct and mapping.
appCode += generateUserInfo(contracts);
// Generate functions to create new contracts.
const allContractNames = Object.keys(contracts);
for (const contractName in contracts) {
appCode += generateNewContractFunction(
contractName,
contracts[contractName],
contractReferences,
fieldLookup,
allContractNames
);
}
appCode += `}\n`;
return appCode;
}
// The following helper functions (generateFirstNGetter, generateLastNGetter, generateUserFunctions)
// are now deprecated since their functionality is integrated into generateAppContract.
function generateFirstNGetter(contractName, contractData) {
return '';
}
function generateLastNGetter(contractName, contractData) {
return '';
}
function generateUserFunctions(contractName, contractData) {
return '';
}
/**
* Generates mapping structures and functions for contract references.
*
* @param {Object} contractReferences - Mapping from parent contract to array of referenced contracts.
* @param {Object} contracts - All contract definitions.
* @param {Object} fieldLookup - Lookup for field names used in references.
* @returns {string} The Solidity code for reference mappings.
*/
function generateReferenceMappings(contractReferences, contracts, fieldLookup) {
let code = '';
for (const parentContract in contractReferences) {
contractReferences[parentContract].forEach(referenceContract => {
code += `\tstruct ${parentContract}_${referenceContract} {\n`;
code += `\t\tbool exists;\n`;
code += `\t\taddress[] ${parentContract}_list;\n`;
code += `\t}\n`;
code += `\tmapping(address => ${parentContract}_${referenceContract}) public ${parentContract}_${referenceContract}_map;\n\n`;
code += `\tfunction get_length_${parentContract}_${referenceContract}_map(address hash) public view returns (uint256) {\n`;
code += `\t\treturn ${parentContract}_${referenceContract}_map[hash].${parentContract}_list.length;\n`;
code += `\t}\n\n`;
code += `\tfunction get_last_${parentContract}_${referenceContract}_map_N(address hash, uint256 count, uint256 offset) public view returns (${parentContract}_contract.${parentContract}Data[] memory) {\n`;
code += `\t\t${parentContract}_contract.${parentContract}Data[] memory results = new ${parentContract}_contract.${parentContract}Data[](count);\n`;
code += `\t\tfor (uint i = 0; i < count; i++) {\n`;
code += `\t\t\t${parentContract}_contract instance = ${parentContract}_contract(${parentContract}_${referenceContract}_map[hash].${parentContract}_list[${parentContract}_${referenceContract}_map[hash].${parentContract}_list.length - i - offset - 1]);\n`;
code += `\t\t\tresults[i] = instance.getAll();\n`;
code += `\t\t}\n`;
code += `\t\treturn results;\n`;
code += `\t}\n\n`;
});
}
return code;
}
/**
* Generates the UserInfo struct and associated mappings.
*
* @param {Object} contracts - All contract definitions.
* @returns {string} The Solidity code for user info.
*/
function generateUserInfo(contracts) {
let code = `\tstruct UserInfo {\n`;
code += `\t\taddress owner;\n`;
code += `\t\tbool exists;\n`;
for (const contractName in contracts) {
code += `\t\taddress[] ${contractName}_list;\n`;
code += `\t\tuint256 ${contractName}_list_length;\n`;
}
code += `\t}\n`;
code += `\tmapping(address => UserInfo) public user_map;\n`;
code += `\taddress[] public UserInfoList;\n`;
code += `\tuint256 public UserInfoListLength;\n\n`;
return code;
}
function generateNewContractFunction(contractName, contract, contractReferences, fieldLookup, allContractNames) {
let code = '';
// Declare the event.
code += `\tevent New${contractName}(address indexed sender, address indexed contractAddress);\n\n`;
// --- UNIQUE INDEX SETUP ---
// If the writeRules contain a "unique" field, create a mapping and a getter function for it.
// TODO handle multiple unique fields in a contract
// TODO handle multiple contracts with same named unique field ie namespace field by contract name
if (contract.writeRules.unique && contract.writeRules.unique.length > 0) {
contract.writeRules.unique.forEach(uniqueField => {
code += `\tmapping(bytes32 => address) unique_map_${uniqueField};\n\n`;
code += `\tfunction get_unique_map_${contractName}(string memory ${uniqueField}) public view returns (address) {\n`;
code += `\t\tbytes32 hash = keccak256(abi.encodePacked(${uniqueField}));\n`;
code += `\t\treturn unique_map_${uniqueField}[hash];\n`;
code += `\t}\n\n`;
});
}
// New contract function header.
code += `\tfunction new_${contractName}(`;
code += contract.initRules.passIn
.map(field => `${contract.fields_types[field]} ${field}`)
.join(', ');
code += `) public returns (address) {\n`;
// --- UNIQUE INDEX CHECKS ---
// For each unique field, compute the hash and require that it hasn't been used already.
if (contract.writeRules.unique && contract.writeRules.unique.length > 0) {
contract.writeRules.unique.forEach(uniqueField => {
code += `\t\tbytes32 hash_${uniqueField} = keccak256(abi.encodePacked(${uniqueField}));\n`;
code += `\t\trequire(unique_map_${uniqueField}[hash_${uniqueField}] == address(0), "Unique constraint violation for ${uniqueField}");\n`;
});
}
// Instantiate the new contract.
code += `\t\taddress mynew = address(new ${contractName}_contract({\n`;
const auto = contract.initRules?.auto || {};
const callerAutoFields = Object.entries(auto)
.filter(([, expr]) => expr === 'tx.origin' || expr === 'msg.sender' || expr === '_msgSender()')
.map(([field]) => field)
.filter(field => !contract.initRules.passIn.includes(field));
const ctorArgs = [
...callerAutoFields.map(field => `\t\t\t_${field} : msg.sender`),
...contract.initRules.passIn.map(field => `\t\t\t_${field} : ${field}`)
];
code += ctorArgs.join(',\n');
code += `\n\t\t}));\n\n`;
// --- UPDATE UNIQUE INDEX MAPPINGS ---
// For each unique field, update the mapping with the new instance.
if (contract.writeRules.unique && contract.writeRules.unique.length > 0) {
contract.writeRules.unique.forEach(uniqueField => {
code += `\t\tunique_map_${uniqueField}[hash_${uniqueField}] = mynew;\n\n`;
});
}
// Continue with the rest of the function (reference mappings, user data, etc.)
if (contractReferences[contractName] && contractReferences[contractName].length > 0) {
contractReferences[contractName].forEach(referenceContract => {
const fieldName = fieldLookup[contractName][referenceContract];
code += `\t\tif(!${contractName}_${referenceContract}_map[${fieldName}].exists) {\n`;
code += `\t\t\t${contractName}_${referenceContract}_map[${fieldName}] = create_index_on_new_${contractName}_${referenceContract}();\n`;
code += `\t\t}\n`;
code += `\t\t${contractName}_${referenceContract}_map[${fieldName}].${contractName}_list.push(mynew);\n\n`;
});
}
code += `\t\tif(!user_map[msg.sender].exists) {\n`;
code += `\t\t\tuser_map[msg.sender] = create_user_on_new_${contractName}(mynew);\n`;
code += `\t\t}\n`;
code += `\t\tuser_map[msg.sender].${contractName}_list.push(mynew);\n`;
code += `\t\tuser_map[msg.sender].${contractName}_list_length += 1;\n\n`;
code += `\t\t${contractName}_list.push(mynew);\n`;
code += `\t\t// The length of ${contractName}_list is tracked by the array length\n\n`;
code += `\t\temit New${contractName}(msg.sender, mynew);\n\n`;
code += `\t\treturn mynew;\n`;
code += `\t}\n\n`;
code += `\tfunction create_user_on_new_${contractName}(address addr) private returns (UserInfo memory) {\n`;
const initLines = allContractNames.map(name => `\t\taddress[] memory ${name}_list_ = new address[](0);`);
code += initLines.join('\n') + '\n';
code += `\t\tUserInfoList.push(addr);\n`;
code += `\t\treturn UserInfo({\n`;
code += `\t\t\texists: true,\n\t\t\towner: addr,\n`;
const userFields = allContractNames
.map(name => `\t\t\t${name}_list: ${name}_list_,\n\t\t\t${name}_list_length: 0`)
.join(',\n');
code += userFields + `\n\t\t});\n`;
code += `\t}\n\n`;
if (contractReferences[contractName] && contractReferences[contractName].length > 0) {
contractReferences[contractName].forEach(referenceContract => {
code += `\tfunction create_index_on_new_${contractName}_${referenceContract}() private pure returns (${contractName}_${referenceContract} memory) {\n`;
code += `\t\taddress[] memory tmp = new address[](0);\n`;
code += `\t\treturn ${contractName}_${referenceContract}({exists: true, ${contractName}_list: tmp});\n`;
code += `\t}\n\n`;
});
}
return code;
}
// Main execution: only run when invoked directly (avoid side effects during tests/imports).
function main() {
try {
const args = process.argv.slice(2);
const jsonFile = args[0] || 'contracts.json';
const contracts = loadContracts(jsonFile);
const { contractReferences, fieldLookup } = processFieldTypes(contracts);
applyMemoryToStringFields(contracts);
const solidityCode = generateSolidityCode(contracts, contractReferences, fieldLookup);
console.log(solidityCode);
} catch (error) {
console.error('Error generating Solidity code:', error);
process.exitCode = 1;
}
}
// Node ESM equivalent of "require.main === module"
import { pathToFileURL } from 'url';
import path from 'path';
const isMain = import.meta.url === pathToFileURL(path.resolve(process.argv[1] || '')).href;
if (isMain) main();
export {
loadContracts,
processFieldTypes,
applyMemoryToStringFields,
generateSolidityCode,
generateContractDefinition,
generateAppContract,
generateFirstNGetter,
generateLastNGetter,
generateUserFunctions,
generateReferenceMappings,
generateUserInfo,
generateNewContractFunction
};