mirror of https://github.com/embarklabs/embark.git
feat(scaffold): allow association/file
- Refactor everything to TS - Add missing types - Declare __ everywhere
This commit is contained in:
parent
5b3d8943cd
commit
f68f1fc9b6
21
package.json
21
package.json
|
@ -72,6 +72,7 @@
|
||||||
"@babel/preset-react": "7.0.0",
|
"@babel/preset-react": "7.0.0",
|
||||||
"@babel/preset-typescript": "7.1.0",
|
"@babel/preset-typescript": "7.1.0",
|
||||||
"@babel/runtime-corejs2": "7.1.2",
|
"@babel/runtime-corejs2": "7.1.2",
|
||||||
|
"ajv": "6.5.5",
|
||||||
"ascii-table": "0.0.9",
|
"ascii-table": "0.0.9",
|
||||||
"async": "2.6.1",
|
"async": "2.6.1",
|
||||||
"babel-loader": "8.0.4",
|
"babel-loader": "8.0.4",
|
||||||
|
@ -164,6 +165,25 @@
|
||||||
"uuid": "3.3.2",
|
"uuid": "3.3.2",
|
||||||
"viz.js": "1.8.2",
|
"viz.js": "1.8.2",
|
||||||
"web3": "1.0.0-beta.34",
|
"web3": "1.0.0-beta.34",
|
||||||
|
"web3-bzz": "1.0.0-beta.34",
|
||||||
|
"web3-core-helpers": "1.0.0-beta.34",
|
||||||
|
"web3-core-method": "1.0.0-beta.34",
|
||||||
|
"web3-core-promievent": "1.0.0-beta.34",
|
||||||
|
"web3-core-requestmanager": "1.0.0-beta.34",
|
||||||
|
"web3-core-subscriptions": "1.0.0-beta.34",
|
||||||
|
"web3-core": "1.0.0-beta.34",
|
||||||
|
"web3-eth-abi": "1.0.0-beta.34",
|
||||||
|
"web3-eth-accounts": "1.0.0-beta.34",
|
||||||
|
"web3-eth-contract": "1.0.0-beta.34",
|
||||||
|
"web3-eth-iban": "1.0.0-beta.34",
|
||||||
|
"web3-eth-personal": "1.0.0-beta.34",
|
||||||
|
"web3-eth": "1.0.0-beta.34",
|
||||||
|
"web3-net": "1.0.0-beta.34",
|
||||||
|
"web3-providers-http": "1.0.0-beta.34",
|
||||||
|
"web3-providers-ipc": "1.0.0-beta.34",
|
||||||
|
"web3-providers-ws": "1.0.0-beta.34",
|
||||||
|
"web3-shh": "1.0.0-beta.34",
|
||||||
|
"web3-utils": "1.0.0-beta.34",
|
||||||
"webpack": "4.19.0",
|
"webpack": "4.19.0",
|
||||||
"webpack-bundle-analyzer": "2.13.1",
|
"webpack-bundle-analyzer": "2.13.1",
|
||||||
"websocket": "1.0.28",
|
"websocket": "1.0.28",
|
||||||
|
@ -174,6 +194,7 @@
|
||||||
"@babel/cli": "7.1.2",
|
"@babel/cli": "7.1.2",
|
||||||
"@babel/plugin-proposal-optional-chaining": "7.0.0",
|
"@babel/plugin-proposal-optional-chaining": "7.0.0",
|
||||||
"@types/async": "2.0.50",
|
"@types/async": "2.0.50",
|
||||||
|
"@types/handlebars": "4.0.39",
|
||||||
"@types/i18n": "0.8.3",
|
"@types/i18n": "0.8.3",
|
||||||
"@types/node": "10.11.7",
|
"@types/node": "10.11.7",
|
||||||
"@types/os-locale": "2.1.0",
|
"@types/os-locale": "2.1.0",
|
||||||
|
|
|
@ -311,39 +311,12 @@ class Cmd {
|
||||||
|
|
||||||
scaffold() {
|
scaffold() {
|
||||||
program
|
program
|
||||||
.command('scaffold [contract] [fields...]')
|
.command('scaffold [contractOrFile] [fields...]')
|
||||||
.option('--framework <framework>', 'UI framework to use. (default: react)')
|
.option('--framework <framework>', 'UI framework to use. (default: react)')
|
||||||
.option('--contract-language <language>', 'Language used for the smart contract generation (default: solidity)')
|
.option('--contract-language <language>', 'Language used for the smart contract generation (default: solidity)')
|
||||||
.option('--overwrite', 'Overwrite existing files. (default: false)')
|
.option('--overwrite', 'Overwrite existing files. (default: false)')
|
||||||
.description(__('Generates a contract and a function tester for you\nExample: ContractName field1:uint field2:address --contract-language solidity --framework react'))
|
.description(__('Generates a contract and a function tester for you\nExample: ContractName field1:uint field2:address --contract-language solidity --framework react'))
|
||||||
.action(function(contract, fields, options) {
|
.action(function(contractOrFile, fields, options) {
|
||||||
if (contract === undefined) {
|
|
||||||
console.log("contract name is required");
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let fieldMapping = {};
|
|
||||||
if (fields.length > 0) {
|
|
||||||
const typeRegex = /^(u?int[0-9]{0,3}(\[\])?|string|bool|address|bytes[0-9]{0,3})(\[\])?$/;
|
|
||||||
const varRegex = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
||||||
fieldMapping = fields.reduce((acc, curr) => {
|
|
||||||
const c = curr.split(':');
|
|
||||||
|
|
||||||
if (!varRegex.test(c[0])) {
|
|
||||||
console.log("Invalid variable name: " + c[0]);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!typeRegex.test(c[1])) {
|
|
||||||
console.log("Invalid datatype: " + c[1] + " - The dApp generator might not support this type at the moment");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
acc[c[0]] = c[1];
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
i18n.setOrDetectLocale(options.locale);
|
i18n.setOrDetectLocale(options.locale);
|
||||||
options.env = 'development';
|
options.env = 'development';
|
||||||
options.logFile = options.logfile; // fix casing
|
options.logFile = options.logfile; // fix casing
|
||||||
|
@ -351,11 +324,8 @@ class Cmd {
|
||||||
options.onlyCompile = options.contracts;
|
options.onlyCompile = options.contracts;
|
||||||
options.client = options.client || 'geth';
|
options.client = options.client || 'geth';
|
||||||
options.webpackConfigName = options.pipeline || 'development';
|
options.webpackConfigName = options.pipeline || 'development';
|
||||||
options.contract = contract;
|
options.contractOrFile = contractOrFile;
|
||||||
options.framework = options.framework || 'react';
|
options.fields = fields;
|
||||||
options.contractLanguage = options.contractLanguage || 'solidity';
|
|
||||||
options.overwrite = options.overwrite || false;
|
|
||||||
options.fields = fieldMapping;
|
|
||||||
|
|
||||||
embark.scaffold(options);
|
embark.scaffold(options);
|
||||||
});
|
});
|
||||||
|
|
|
@ -471,9 +471,8 @@ class EmbarkController {
|
||||||
callback();
|
callback();
|
||||||
},
|
},
|
||||||
function generateContract(callback) {
|
function generateContract(callback) {
|
||||||
engine.events.request('scaffolding:generate:contract', options, function(err, file) {
|
engine.events.request('scaffolding:generate:contract', options, function(files) {
|
||||||
// Add contract file to the manager
|
files.forEach(file => engine.events.request('config:contractsFiles:add', file));
|
||||||
engine.events.request('config:contractsFiles:add', file);
|
|
||||||
callback();
|
callback();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,8 +9,6 @@ import Web3 from "web3";
|
||||||
import { Embark, Events } from "../../../typings/embark";
|
import { Embark, Events } from "../../../typings/embark";
|
||||||
import Suggestions from "./suggestions";
|
import Suggestions from "./suggestions";
|
||||||
|
|
||||||
declare const __: any;
|
|
||||||
|
|
||||||
class Console {
|
class Console {
|
||||||
private embark: Embark;
|
private embark: Embark;
|
||||||
private events: Events;
|
private events: Events;
|
||||||
|
|
|
@ -1,125 +0,0 @@
|
||||||
const Handlebars = require('handlebars');
|
|
||||||
const fs = require('../../core/fs');
|
|
||||||
let utils = require('../../utils/utils.js');
|
|
||||||
|
|
||||||
|
|
||||||
Handlebars.registerHelper('capitalize', function(word) {
|
|
||||||
return word.charAt(0).toUpperCase() + word.slice(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
Handlebars.registerHelper('ifview', function(stateMutability, options) {
|
|
||||||
let result = stateMutability === 'view' || stateMutability === 'pure' || stateMutability === 'constant';
|
|
||||||
if (result) {
|
|
||||||
return options.fn(this);
|
|
||||||
}
|
|
||||||
return options.inverse(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
Handlebars.registerHelper('ifeq', function(elem, value, options) {
|
|
||||||
if (elem === value) {
|
|
||||||
return options.fn(this);
|
|
||||||
}
|
|
||||||
return options.inverse(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
Handlebars.registerHelper('ifarr', function(elem, options) {
|
|
||||||
if (elem.indexOf('[]') > -1) {
|
|
||||||
return options.fn(this);
|
|
||||||
}
|
|
||||||
return options.inverse(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
Handlebars.registerHelper('iflengthgt', function(arr, val, options) {
|
|
||||||
if (arr.length > val) {
|
|
||||||
return options.fn(this);
|
|
||||||
}
|
|
||||||
return options.inverse(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
Handlebars.registerHelper('emptyname', function(name, index) {
|
|
||||||
return name ? name : 'output' + index;
|
|
||||||
});
|
|
||||||
|
|
||||||
Handlebars.registerHelper('trim', function(name) {
|
|
||||||
return name.replace('[]', '');
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
Handlebars.registerHelper('methodname', function(abiDefinition, functionName, inputs) {
|
|
||||||
let funCount = abiDefinition.filter(x => x.name === functionName).length;
|
|
||||||
if (funCount === 1) {
|
|
||||||
return '.' + functionName;
|
|
||||||
}
|
|
||||||
return new Handlebars.SafeString(`['${functionName}(${inputs !== null ? inputs.map(input => input.type).join(',') : ''})']`);
|
|
||||||
});
|
|
||||||
|
|
||||||
class ScaffoldingReact {
|
|
||||||
constructor(embark, options) {
|
|
||||||
this.embark = embark;
|
|
||||||
this.options = options;
|
|
||||||
this.embark.registerDappGenerator('react', this.build.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
_generateFile(contract, templateFilename, extension, data, overwrite) {
|
|
||||||
const filename = contract.className.toLowerCase() + '.' + extension;
|
|
||||||
const filePath = './app/' + filename;
|
|
||||||
if (!overwrite && fs.existsSync(filePath)) {
|
|
||||||
throw new Error("file '" + filePath + "' already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
const templatePath = utils.joinPath(__dirname, 'templates/' + templateFilename);
|
|
||||||
const source = fs.readFileSync(templatePath).toString();
|
|
||||||
const template = Handlebars.compile(source);
|
|
||||||
|
|
||||||
// Write template
|
|
||||||
const result = template(data);
|
|
||||||
fs.writeFileSync(filePath, result);
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
build(contract, overwrite, cb) {
|
|
||||||
const packageInstallCmd = 'npm install react react-bootstrap react-dom';
|
|
||||||
utils.runCmd(packageInstallCmd, null, (err) => {
|
|
||||||
if (err) {
|
|
||||||
this.embark.logger.error(err.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const filename = contract.className.toLowerCase();
|
|
||||||
|
|
||||||
this._generateFile(contract, 'index.html.hbs', 'html',
|
|
||||||
{
|
|
||||||
'title': contract.className,
|
|
||||||
filename
|
|
||||||
}, overwrite);
|
|
||||||
|
|
||||||
const filePath = this._generateFile(contract, 'dapp.js.hbs', 'js',
|
|
||||||
{
|
|
||||||
'title': contract.className,
|
|
||||||
'contractName': contract.className,
|
|
||||||
'functions': contract.abiDefinition.filter(x => x.type === 'function')
|
|
||||||
}, overwrite);
|
|
||||||
|
|
||||||
// Update config
|
|
||||||
const contents = fs.readFileSync("./embark.json");
|
|
||||||
let embarkJson = JSON.parse(contents);
|
|
||||||
embarkJson.app["js/" + filename + ".js"] = "app/" + filename + '.js';
|
|
||||||
embarkJson.app[filename + ".html"] = "app/" + filename + '.html';
|
|
||||||
|
|
||||||
fs.writeFileSync("./embark.json", JSON.stringify(embarkJson, null, 4));
|
|
||||||
|
|
||||||
this.embark.logger.info('app/' + filename + ".html generated");
|
|
||||||
this.embark.logger.info('app/' + filename + ".js generated");
|
|
||||||
|
|
||||||
cb(null, filePath);
|
|
||||||
} catch (error) {
|
|
||||||
this.embark.logger.error(error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ScaffoldingReact;
|
|
|
@ -1,57 +0,0 @@
|
||||||
|
|
||||||
const Handlebars = require('handlebars');
|
|
||||||
const fs = require('../../core/fs');
|
|
||||||
const utils = require('../../utils/utils');
|
|
||||||
|
|
||||||
const capitalize = string => string.charAt(0).toUpperCase() + string.slice(1);
|
|
||||||
|
|
||||||
class ScaffoldingSolidity {
|
|
||||||
constructor(embark, options){
|
|
||||||
this.embark = embark;
|
|
||||||
this.options = options;
|
|
||||||
this.embark.registerDappGenerator('solidity', this.build.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
_generateFile(contract, templateFilename, extension, data, overwrite){
|
|
||||||
const filename = capitalize(contract.className.toLowerCase()) + '.' + extension;
|
|
||||||
const contractDirs = this.embark.config.embarkConfig.contracts;
|
|
||||||
const contractDir = Array.isArray(contractDirs) ? contractDirs[0] : contractDirs;
|
|
||||||
const filePath = fs.dappPath(contractDir.replace(/\*/g, ''), filename);
|
|
||||||
if (!overwrite && fs.existsSync(filePath)){
|
|
||||||
throw new Error("file '" + filePath + "' already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
const templatePath = utils.joinPath(__dirname, 'templates/' + templateFilename);
|
|
||||||
const source = fs.readFileSync(templatePath).toString();
|
|
||||||
const template = Handlebars.compile(source);
|
|
||||||
|
|
||||||
// Write template
|
|
||||||
const result = template(data);
|
|
||||||
fs.writeFileSync(filePath, result);
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
build(contract, overwrite, cb){
|
|
||||||
try {
|
|
||||||
contract.className = capitalize(contract.className);
|
|
||||||
|
|
||||||
const filename = contract.className;
|
|
||||||
|
|
||||||
const filePath = this._generateFile(contract, 'contract.sol.hbs', 'sol', {
|
|
||||||
'contractName': contract.className,
|
|
||||||
'structName': contract.className + "Struct",
|
|
||||||
'fields': Object.keys(contract.fields).map(f => {
|
|
||||||
return {name:f, type: contract.fields[f]};
|
|
||||||
})
|
|
||||||
}, overwrite);
|
|
||||||
this.embark.logger.info("contracts/" + filename + ".sol generated");
|
|
||||||
|
|
||||||
cb(null, filePath);
|
|
||||||
} catch(error) {
|
|
||||||
this.embark.logger.error(error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ScaffoldingSolidity;
|
|
|
@ -1,50 +0,0 @@
|
||||||
pragma solidity ^0.5.0;
|
|
||||||
|
|
||||||
contract {{contractName}} {
|
|
||||||
|
|
||||||
struct {{structName}} {
|
|
||||||
{{#each fields}}
|
|
||||||
{{type}} {{name}};
|
|
||||||
{{/each}}
|
|
||||||
}
|
|
||||||
|
|
||||||
{{structName}}[] items;
|
|
||||||
|
|
||||||
event ItemCreated(uint id, address createdBy);
|
|
||||||
event ItemDeleted(uint id, address deletedBy);
|
|
||||||
event ItemUpdated(uint id, address updatedBy);
|
|
||||||
|
|
||||||
function add({{#each fields}}{{type}} _{{name}}{{#unless @last}}, {{/unless}}{{/each}}) public {
|
|
||||||
uint id = items.length++;
|
|
||||||
items[id] = {{structName}}({
|
|
||||||
{{#each fields}}
|
|
||||||
{{name}}: _{{name}}{{#unless @last}},{{/unless}}
|
|
||||||
{{/each}}
|
|
||||||
});
|
|
||||||
|
|
||||||
emit ItemCreated(id, msg.sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
function edit(uint _id, {{#each fields}}{{type}} _{{name}}{{#unless @last}}, {{/unless}}{{/each}}) public {
|
|
||||||
require(_id < items.length, "Invalid {{structName}} id");
|
|
||||||
|
|
||||||
{{#each fields}}
|
|
||||||
items[_id].{{name}} = _{{name}};
|
|
||||||
{{/each}}
|
|
||||||
|
|
||||||
emit ItemUpdated(_id, msg.sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove(uint _id) public {
|
|
||||||
require(_id < items.length, "Invalid {{structName}} id");
|
|
||||||
|
|
||||||
delete items[_id];
|
|
||||||
emit ItemDeleted(_id, msg.sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
function get(uint _id) public view returns ({{#each fields}}{{type}}{{#unless @last}},{{/unless}}{{/each}}) {
|
|
||||||
require(_id < items.length, "Invalid ArrayContractStruct id");
|
|
||||||
return ({{#each fields}}items[_id].{{name}}{{#unless @last}},{{/unless}}{{/each}});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface Builder {
|
||||||
|
build(): Promise<string[]>;
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import Ajv from "ajv";
|
||||||
|
|
||||||
|
import { Logger } from "../../../typings/logger";
|
||||||
|
|
||||||
|
export enum Framework {
|
||||||
|
React = "react",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ContractLanguage {
|
||||||
|
Solidity = "solidity",
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CommandOptions {
|
||||||
|
constructor(private readonly logger: Logger,
|
||||||
|
public readonly framework: Framework = Framework.React,
|
||||||
|
public readonly contractLanguage: ContractLanguage = ContractLanguage.Solidity,
|
||||||
|
public readonly overwrite: boolean = false,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public validate() {
|
||||||
|
if (!Object.values(Framework).includes(this.framework)) {
|
||||||
|
this.logger.error(__("Selected framework not supported"));
|
||||||
|
this.logger.error(__("Supported Frameworks are: %s", Object.values(Framework).join(", ")));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
if (!Object.values(ContractLanguage).includes(this.contractLanguage)) {
|
||||||
|
this.logger.error(__("Selected contract language not supported"));
|
||||||
|
this.logger.error(__("Supported Contract Languages are: %s", Object.values(ContractLanguage).join(", ")));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
import Handlebars from "handlebars";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
import { Embark } from "../../../../../typings/embark";
|
||||||
|
import { Builder } from "../../builder";
|
||||||
|
import { CommandOptions } from "../../commandOptions";
|
||||||
|
import { SmartContractsRecipe } from "../../smartContractsRecipe";
|
||||||
|
|
||||||
|
const fs = require("../../../../core/fs");
|
||||||
|
require("../../handlebarHelpers");
|
||||||
|
|
||||||
|
const templatePath = path.join(__dirname, "templates", "contract.sol.hbs");
|
||||||
|
|
||||||
|
export class SolidityBuilder implements Builder {
|
||||||
|
constructor(private embark: Embark,
|
||||||
|
private description: SmartContractsRecipe,
|
||||||
|
private options: CommandOptions) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public async build() {
|
||||||
|
return Object.keys(this.description.data).map((contractName) => {
|
||||||
|
const code = this.generateCode(contractName);
|
||||||
|
const file = this.saveFile(contractName, code);
|
||||||
|
this.printInstructions(contractName);
|
||||||
|
return file;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateCode(contractName: string) {
|
||||||
|
const source = fs.readFileSync(templatePath, "utf-8");
|
||||||
|
const template = Handlebars.compile(source);
|
||||||
|
|
||||||
|
const attributes = this.description.standardAttributes(contractName);
|
||||||
|
const ipfs = this.description.ipfsAttributes(contractName);
|
||||||
|
const associations = this.description.associationAttributes(contractName);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
associations,
|
||||||
|
attributes,
|
||||||
|
contractName,
|
||||||
|
ipfs,
|
||||||
|
structName: `${contractName}Struct`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return template(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveFile(contractName: string, code: string) {
|
||||||
|
const filename = `${contractName}.sol`;
|
||||||
|
const contractDirs = this.embark.config.embarkConfig.contracts;
|
||||||
|
const contractDir = Array.isArray(contractDirs) ? contractDirs[0] : contractDirs;
|
||||||
|
const filePath = fs.dappPath(contractDir.replace(/\*/g, ""), filename);
|
||||||
|
if (!this.options.overwrite && fs.existsSync(filePath)) {
|
||||||
|
this.embark.logger.error(__(`The contract ${contractName} already exists, skipping.`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(filePath, code);
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private printInstructions(contractName: string) {
|
||||||
|
const associations = Object.keys(this.description.associationAttributes(contractName));
|
||||||
|
if (!associations.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = associations.map((name) => `"$${name}"`).join(", ");
|
||||||
|
this.embark.logger.info(`In order to deploy your contracts, you will have to specify the dependencies.`);
|
||||||
|
this.embark.logger.info(`You can do it by adding to your contracts config the following snippets:`);
|
||||||
|
this.embark.logger.info(`${contractName}: { args: [${args}] }`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
pragma solidity ^0.5.0;
|
||||||
|
|
||||||
|
{{#each associations}}
|
||||||
|
import "./{{@key}}.sol";
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
contract {{contractName}} {
|
||||||
|
|
||||||
|
uint constant IMPOSSIBLE_INDEX = 99999999999;
|
||||||
|
|
||||||
|
struct {{structName}} {
|
||||||
|
{{#each attributes}}
|
||||||
|
{{this}} {{@key}};
|
||||||
|
{{/each}}
|
||||||
|
}
|
||||||
|
|
||||||
|
{{structName}}[] items;
|
||||||
|
|
||||||
|
{{#each associations}}
|
||||||
|
{{#ifeq this 'hasMany'}}
|
||||||
|
mapping(uint => uint[]) {{lowercase @key}}Mapping;
|
||||||
|
{{/ifeq}}
|
||||||
|
{{#ifeq this 'belongsTo'}}
|
||||||
|
mapping(uint => uint) {{lowercase @key}}Mapping;
|
||||||
|
{{/ifeq}}
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
{{#each ipfs}}
|
||||||
|
mapping(uint => string) {{@key}};
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
{{#each associations}}
|
||||||
|
{{@key}} {{lowercase @key}};
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
event ItemCreated(uint id, address createdBy);
|
||||||
|
event ItemDeleted(uint id, address deletedBy);
|
||||||
|
event ItemUpdated(uint id, address updatedBy);
|
||||||
|
|
||||||
|
constructor({{#each associations}}address _{{@key}}{{#unless @last}}, {{/unless}}{{/each}}) public {
|
||||||
|
{{#each associations}}
|
||||||
|
{{lowercase @key}} = {{@key}}(_{{@key}});
|
||||||
|
{{/each}}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLength() public view returns(uint count) {
|
||||||
|
return items.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function add({{#each attributes}}{{this}}{{#ifstring this}} memory{{/ifstring}} _{{@key}}{{#unless @last}}, {{/unless}}{{/each}}) public {
|
||||||
|
uint id = items.length++;
|
||||||
|
items[id] = {{structName}}({
|
||||||
|
{{#each attributes}}
|
||||||
|
{{@key}}: _{{@key}}{{#unless @last}},{{/unless}}
|
||||||
|
{{/each}}
|
||||||
|
});
|
||||||
|
|
||||||
|
{{#each associations}}
|
||||||
|
{{#ifeq this 'hasMany'}}
|
||||||
|
{{lowercase @key}}Mapping[id] = new uint[](0);
|
||||||
|
{{/ifeq}}
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
emit ItemCreated(id, msg.sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
function edit(uint _id, {{#each attributes}}{{this}}{{#ifstring this}} memory{{/ifstring}} _{{@key}}{{#unless @last}}, {{/unless}}{{/each}}) public {
|
||||||
|
require(_id < items.length, "Invalid {{structName}} id");
|
||||||
|
|
||||||
|
{{#each attributes}}
|
||||||
|
items[_id].{{@key}} = _{{@key}};
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
emit ItemUpdated(_id, msg.sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(uint _id) public {
|
||||||
|
require(_id < items.length, "Invalid {{structName}} id");
|
||||||
|
|
||||||
|
delete items[_id];
|
||||||
|
emit ItemDeleted(_id, msg.sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(uint _id) public view returns ({{#each attributes}}{{this}}{{#ifstring this}} memory{{/ifstring}}{{#unless @last}}, {{/unless}}{{/each}}) {
|
||||||
|
require(_id < items.length, "Invalid {{structName}} id");
|
||||||
|
return ({{#each attributes}}items[_id].{{@key}}{{#unless @last}},{{/unless}}{{/each}});
|
||||||
|
}
|
||||||
|
|
||||||
|
{{#each ipfs}}
|
||||||
|
function add{{capitalize @key}}(uint _id, string memory _{{@key}}) public {
|
||||||
|
require(_id < items.length, "Invalid {{structName}} id");
|
||||||
|
|
||||||
|
{{@key}}[_id] = _{{@key}};
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove{{capitalize @key}}(uint _id) public {
|
||||||
|
require(_id < items.length, "Invalid {{structName}} id");
|
||||||
|
|
||||||
|
{{@key}}[_id] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function get{{capitalize @key}}(uint _id) public view returns(string memory) {
|
||||||
|
require(_id < items.length, "Invalid {{structName}} id");
|
||||||
|
|
||||||
|
return {{@key}}[_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/each}}
|
||||||
|
{{#each associations}}
|
||||||
|
function add{{capitalize @key}}(uint _id, uint _{{@key}}Id) public {
|
||||||
|
require(_id < items.length, "Invalid {{structName}} id");
|
||||||
|
require(_{{@key}}Id < {{lowercase @key}}.getLength(), "Invalid {{@key}} id");
|
||||||
|
|
||||||
|
{{#ifeq this 'hasMany'}}
|
||||||
|
uint index = indexOf({{lowercase @key}}Mapping[_id], _{{@key}}Id);
|
||||||
|
require(index == IMPOSSIBLE_INDEX, "_{{@key}}Id already added");
|
||||||
|
|
||||||
|
{{lowercase @key}}Mapping[_id].push(_{{@key}}Id);
|
||||||
|
{{/ifeq}}
|
||||||
|
{{#ifeq this 'belongsTo'}}
|
||||||
|
{{lowercase @key}}Mapping[_id] = _{{@key}}Id;
|
||||||
|
{{/ifeq}}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove{{capitalize @key}}(uint _id, uint _{{@key}}Id) public {
|
||||||
|
require(_id < items.length, "Invalid {{structName}} id");
|
||||||
|
require(_{{@key}}Id < {{lowercase @key}}.getLength(), "Invalid {{@key}} id");
|
||||||
|
|
||||||
|
{{#ifeq this 'hasMany'}}
|
||||||
|
uint index = indexOf({{lowercase @key}}Mapping[_id], _{{@key}}Id);
|
||||||
|
require(index != IMPOSSIBLE_INDEX, "_{{@key}}Id not found");
|
||||||
|
|
||||||
|
delete {{lowercase @key}}Mapping[_id][index];
|
||||||
|
{{/ifeq}}
|
||||||
|
{{#ifeq this 'belongsTo'}}
|
||||||
|
{{lowercase @key}}Mapping[_id] = 0;
|
||||||
|
{{/ifeq}}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get{{capitalize @key}}(uint _id) public view returns(uint{{#ifeq this 'hasMany'}}[] memory{{/ifeq}}) {
|
||||||
|
require(_id < items.length, "Invalid {{structName}} id");
|
||||||
|
|
||||||
|
return {{lowercase @key}}Mapping[_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
function indexOf(uint[] storage values, uint value) private view returns(uint) {
|
||||||
|
for (uint i = 0; i < values.length; i++) {
|
||||||
|
if (values[i] == value) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return IMPOSSIBLE_INDEX;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
import Handlebars from "handlebars";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
import { Contract } from "../../../../../typings/contract";
|
||||||
|
import { Embark } from "../../../../../typings/embark";
|
||||||
|
import { Builder } from "../../builder";
|
||||||
|
import { CommandOptions } from "../../commandOptions";
|
||||||
|
import { SmartContractsRecipe } from "../../smartContractsRecipe";
|
||||||
|
|
||||||
|
const fs = require("../../../../core/fs");
|
||||||
|
const utils = require("../../../../utils/utils");
|
||||||
|
require("../../handlebarHelpers");
|
||||||
|
|
||||||
|
const indexTemplatePath = path.join(__dirname, "templates", "index.html.hbs");
|
||||||
|
const dappTemplatePath = path.join(__dirname, "templates", "dapp.js.hbs");
|
||||||
|
|
||||||
|
export class ReactBuilder implements Builder {
|
||||||
|
constructor(private embark: Embark,
|
||||||
|
private description: SmartContractsRecipe,
|
||||||
|
private contracts: Contract[],
|
||||||
|
private options: CommandOptions) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public async build() {
|
||||||
|
await this.installDependencies();
|
||||||
|
|
||||||
|
return [].concat.apply([], Object.keys(this.description.data).map((contractName) => {
|
||||||
|
const [indexCode, dappCode] = this.generateCodes(contractName);
|
||||||
|
if (indexCode && dappCode) {
|
||||||
|
const files = this.saveFiles(contractName, indexCode, dappCode);
|
||||||
|
this.updateEmbarkJson(contractName, files);
|
||||||
|
return files;
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateEmbarkJson(contractName: string, files: string[]) {
|
||||||
|
const embarkJsonPath = path.join(fs.dappPath(), "embark.json");
|
||||||
|
const embarkJson = fs.readJSONSync(embarkJsonPath);
|
||||||
|
embarkJson.app[`js/${contractName}.js`] = `app/${contractName}.js`;
|
||||||
|
embarkJson.app[`${contractName}.html`] = `app/${contractName}.html`;
|
||||||
|
|
||||||
|
fs.writeFileSync(embarkJsonPath, JSON.stringify(embarkJson, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateCodes(contractName: string) {
|
||||||
|
const indexSource = fs.readFileSync(indexTemplatePath, "utf-8");
|
||||||
|
const dappSource = fs.readFileSync(dappTemplatePath, "utf-8");
|
||||||
|
|
||||||
|
const indexTemplate = Handlebars.compile(indexSource);
|
||||||
|
const dappTemplate = Handlebars.compile(dappSource);
|
||||||
|
|
||||||
|
const indexData = {
|
||||||
|
filename: contractName.toLowerCase(),
|
||||||
|
title: contractName,
|
||||||
|
};
|
||||||
|
|
||||||
|
const contract = this.contracts.find((c) => c.className === contractName);
|
||||||
|
if (!contract) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const dappData = {
|
||||||
|
contractName,
|
||||||
|
functions: contract.abiDefinition.filter((entry) => entry.type === "function"),
|
||||||
|
};
|
||||||
|
|
||||||
|
return [indexTemplate(indexData), dappTemplate(dappData)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private installDependencies() {
|
||||||
|
const cmd = "npm install react react-bootstrap react-dom";
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
utils.runCmd(cmd, null, (error: string) => {
|
||||||
|
if (error) {
|
||||||
|
return reject(new Error(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveFiles(contractName: string, indexCode: string, dappCode: string) {
|
||||||
|
const indexFilePath = path.join(fs.dappPath(), "app", `${contractName}.html`);
|
||||||
|
const dappFilePath = path.join(fs.dappPath(), "app", `${contractName}.js`);
|
||||||
|
|
||||||
|
if (!this.options.overwrite && (fs.existsSync(indexFilePath) || fs.existsSync(dappFilePath))) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(indexFilePath, indexCode);
|
||||||
|
fs.writeFileSync(dappFilePath, dappCode);
|
||||||
|
|
||||||
|
this.embark.logger.info(__(`${indexFilePath} generated`));
|
||||||
|
this.embark.logger.info(__(`${dappFilePath} generated`));
|
||||||
|
return [indexFilePath, dappFilePath];
|
||||||
|
}
|
||||||
|
}
|
|
@ -171,7 +171,7 @@ class {{capitalize name}}Form{{@index}} extends Component {
|
||||||
|
|
||||||
function {{contractName}}UI(props) {
|
function {{contractName}}UI(props) {
|
||||||
return (<div>
|
return (<div>
|
||||||
<h1>{{title}}</h1>
|
<h1>{{contractName}}</h1>
|
||||||
{{#each functions}}
|
{{#each functions}}
|
||||||
<{{capitalize name}}Form{{@index}} />
|
<{{capitalize name}}Form{{@index}} />
|
||||||
{{/each}}
|
{{/each}}
|
|
@ -0,0 +1,62 @@
|
||||||
|
import Handlebars from "handlebars";
|
||||||
|
import { ABIDefinition } from "web3/eth/abi";
|
||||||
|
|
||||||
|
Handlebars.registerHelper("capitalize", (word: string) => {
|
||||||
|
return word.charAt(0).toUpperCase() + word.slice(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
Handlebars.registerHelper("lowercase", (word: string) => {
|
||||||
|
return word.toLowerCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
Handlebars.registerHelper("ifview", function(stateMutability: string, options: Handlebars.HelperOptions) {
|
||||||
|
const isView = stateMutability === "view" || stateMutability === "pure" || stateMutability === "constant";
|
||||||
|
if (isView) {
|
||||||
|
return options.fn(this);
|
||||||
|
}
|
||||||
|
return options.inverse(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
Handlebars.registerHelper("ifstring", function(value: string, options: Handlebars.HelperOptions) {
|
||||||
|
if (value === "string") {
|
||||||
|
return options.fn(this);
|
||||||
|
}
|
||||||
|
return options.inverse(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
Handlebars.registerHelper("ifeq", function(elem: string, value: string, options: Handlebars.HelperOptions) {
|
||||||
|
if (elem === value) {
|
||||||
|
return options.fn(this);
|
||||||
|
}
|
||||||
|
return options.inverse(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
Handlebars.registerHelper("ifarr", function(elem: string, options: Handlebars.HelperOptions) {
|
||||||
|
if (elem.indexOf("[]") > -1) {
|
||||||
|
return options.fn(this);
|
||||||
|
}
|
||||||
|
return options.inverse(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
Handlebars.registerHelper("iflengthgt", function(arr, val, options: Handlebars.HelperOptions) {
|
||||||
|
if (arr.length > val) {
|
||||||
|
return options.fn(this);
|
||||||
|
}
|
||||||
|
return options.inverse(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
Handlebars.registerHelper("emptyname", (name: string, index: string) => {
|
||||||
|
return name ? name : "output" + index;
|
||||||
|
});
|
||||||
|
|
||||||
|
Handlebars.registerHelper("trim", (name: string) => {
|
||||||
|
return name.replace("[]", "");
|
||||||
|
});
|
||||||
|
|
||||||
|
Handlebars.registerHelper("methodname", (abiDefinition: ABIDefinition[], functionName: string, inputs: any[]) => {
|
||||||
|
const funCount = abiDefinition.filter((x) => x.name === functionName).length;
|
||||||
|
if (funCount === 1) {
|
||||||
|
return "." + functionName;
|
||||||
|
}
|
||||||
|
return new Handlebars.SafeString(`["${functionName}(${inputs !== null ? inputs.map((input) => input.type).join(",") : ""})"]`);
|
||||||
|
});
|
|
@ -1,96 +0,0 @@
|
||||||
class Scaffolding {
|
|
||||||
constructor(embark, _options) {
|
|
||||||
this.embark = embark;
|
|
||||||
this.options = _options;
|
|
||||||
this.plugins = _options.plugins;
|
|
||||||
|
|
||||||
embark.events.setCommandHandler("scaffolding:generate:contract", (options, cb) => {
|
|
||||||
this.framework = options.contractLanguage;
|
|
||||||
this.fields = options.fields;
|
|
||||||
this.generate(options.contract, options.overwrite, true, cb);
|
|
||||||
});
|
|
||||||
|
|
||||||
embark.events.setCommandHandler("scaffolding:generate:ui", (options, cb) => {
|
|
||||||
this.framework = options.framework;
|
|
||||||
this.fields = options.fields;
|
|
||||||
this.generate(options.contract, options.overwrite, false, cb);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getScaffoldPlugin(framework) {
|
|
||||||
let dappGenerators = this.plugins.getPluginsFor('dappGenerator');
|
|
||||||
let builder = null;
|
|
||||||
dappGenerators.forEach((plugin) => {
|
|
||||||
plugin.dappGenerators.forEach((d) => {
|
|
||||||
if (d.framework === framework) {
|
|
||||||
builder = d.cb;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return builder;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadFrameworkModule() {
|
|
||||||
switch (this.framework) {
|
|
||||||
case 'react':
|
|
||||||
this.plugins.loadInternalPlugin('scaffolding-react', this.options);
|
|
||||||
break;
|
|
||||||
case 'solidity':
|
|
||||||
this.plugins.loadInternalPlugin('scaffolding-solidity', this.options);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.embark.logger.error(__('Selected framework not supported'));
|
|
||||||
this.embark.logger.error(__('Supported Frameworks are: %s', 'react, solidity'));
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generate(contractName, overwrite, isContractGeneration, cb) {
|
|
||||||
this.loadFrameworkModule();
|
|
||||||
|
|
||||||
const build = this.getScaffoldPlugin(this.framework);
|
|
||||||
if (!build) {
|
|
||||||
this.embark.logger.error("Could not find plugin for framework '" + this.framework + "'");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasFields = Object.getOwnPropertyNames(this.fields).length !== 0;
|
|
||||||
|
|
||||||
if (isContractGeneration && !hasFields) {
|
|
||||||
// This happens when you execute "scaffold ContractName",
|
|
||||||
// assuming the contract already exists in a .sol file
|
|
||||||
cb();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let contract;
|
|
||||||
if (isContractGeneration && hasFields) {
|
|
||||||
contract = {className: contractName, fields: this.fields};
|
|
||||||
try {
|
|
||||||
build(contract, overwrite, cb);
|
|
||||||
} catch (err) {
|
|
||||||
this.embark.logger.error(err.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Contract already exists
|
|
||||||
this.embark.events.request("contracts:list", (_err, contractsList) => {
|
|
||||||
if (_err) throw new Error(_err);
|
|
||||||
const contract = contractsList.find(x => x.className === contractName);
|
|
||||||
if (!contract) {
|
|
||||||
this.embark.logger.error("contract '" + contractName + "' does not exist");
|
|
||||||
cb();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
build(contract, overwrite, cb);
|
|
||||||
} catch (err) {
|
|
||||||
this.embark.logger.error(err.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = Scaffolding;
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { Contract } from "../../../typings/contract";
|
||||||
|
import { Embark } from "../../../typings/embark";
|
||||||
|
import { CommandOptions, ContractLanguage, Framework } from "./commandOptions";
|
||||||
|
import { SolidityBuilder } from "./contractLanguage/solidityBuilder";
|
||||||
|
import { ReactBuilder } from "./framework/reactBuilder";
|
||||||
|
import { SmartContractsRecipe } from "./smartContractsRecipe";
|
||||||
|
|
||||||
|
export default class Scaffolding {
|
||||||
|
|
||||||
|
constructor(private embark: Embark, private options: any) {
|
||||||
|
this.embark.events.setCommandHandler("scaffolding:generate:contract", (cmdLineOptions: any, cb: (files: string[]) => void) => {
|
||||||
|
this.generateContract(cmdLineOptions).then(cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.embark.events.setCommandHandler("scaffolding:generate:ui", (cmdLineOptions: any, cb: (files: string[]) => void) => {
|
||||||
|
this.generateUi(cmdLineOptions).then(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private contractLanguageStrategy(recipe: SmartContractsRecipe, options: CommandOptions) {
|
||||||
|
switch (options.contractLanguage) {
|
||||||
|
case ContractLanguage.Solidity: {
|
||||||
|
return new SolidityBuilder(this.embark, recipe, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private frameworkStrategy(recipe: SmartContractsRecipe, contracts: Contract[], options: CommandOptions) {
|
||||||
|
switch (options.framework) {
|
||||||
|
case Framework.React: {
|
||||||
|
return new ReactBuilder(this.embark, recipe, contracts, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseAndValidate(cmdLineOptions: any) {
|
||||||
|
const options = new CommandOptions(this.embark.logger, cmdLineOptions.framework, cmdLineOptions.contractLanguage, cmdLineOptions.overwrite);
|
||||||
|
const recipe = new SmartContractsRecipe(this.embark.logger, cmdLineOptions.contractOrFile, cmdLineOptions.fields);
|
||||||
|
options.validate();
|
||||||
|
recipe.validate();
|
||||||
|
|
||||||
|
return {recipe, options};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateContract(cmdLineOptions: any) {
|
||||||
|
const {recipe, options} = this.parseAndValidate(cmdLineOptions);
|
||||||
|
return await this.contractLanguageStrategy(recipe, options).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateUi(cmdLineOptions: any) {
|
||||||
|
const {recipe, options} = this.parseAndValidate(cmdLineOptions);
|
||||||
|
const contracts = await this.getContracts();
|
||||||
|
return await this.frameworkStrategy(recipe, contracts, options).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getContracts() {
|
||||||
|
return new Promise<Contract[]>((resolve) => {
|
||||||
|
this.embark.events.request("contracts:list", (_: null, contracts: Contract[]) => {
|
||||||
|
resolve(contracts);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
export const schema = {
|
||||||
|
minProperties: 1,
|
||||||
|
patternProperties: {
|
||||||
|
".*": {
|
||||||
|
minProperties: 1,
|
||||||
|
patternProperties: {
|
||||||
|
"^[a-zA-Z][a-zA-Z0-9_]*$": {
|
||||||
|
pattern: "^(u?int[0-9]{0,3}(\[\])?|string|bool|address|belongsTo|hasMany|ipfsText|ipfsImage|bytes[0-9]{0,3})(\[\])?$",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: ["object"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: "Scafold Schema",
|
||||||
|
type: "object",
|
||||||
|
};
|
|
@ -0,0 +1,106 @@
|
||||||
|
import Ajv from "ajv";
|
||||||
|
|
||||||
|
import { Logger } from "../../../typings/logger";
|
||||||
|
import { schema } from "./schema";
|
||||||
|
|
||||||
|
const fs = require("../../core/fs");
|
||||||
|
|
||||||
|
const ajv = new Ajv();
|
||||||
|
const scaffoldingSchema = ajv.compile(schema);
|
||||||
|
|
||||||
|
interface Properties {
|
||||||
|
[propertyName: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Data {
|
||||||
|
[contractName: string]: Properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ASSOCIATIONS = ["belongsTo", "hasMany"];
|
||||||
|
const IPFS = ["ipfsText", "ipfsImage"];
|
||||||
|
|
||||||
|
export class SmartContractsRecipe {
|
||||||
|
public data: Data;
|
||||||
|
constructor(private readonly logger: Logger,
|
||||||
|
private readonly contractOrFile: string,
|
||||||
|
private readonly fields: string[],
|
||||||
|
) {
|
||||||
|
if (fs.existsSync(contractOrFile)) {
|
||||||
|
this.data = fs.readJSONSync(contractOrFile);
|
||||||
|
} else {
|
||||||
|
this.data = this.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public standardAttributes(contractName: string): Properties {
|
||||||
|
return Object.keys(this.data[contractName]).reduce((acc: Properties, propertyName: string) => {
|
||||||
|
const type = this.data[contractName][propertyName];
|
||||||
|
if (!ASSOCIATIONS.includes(type) && !IPFS.includes(type)) {
|
||||||
|
acc[propertyName] = type;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ipfsAttributes(contractName: string): Properties {
|
||||||
|
return Object.keys(this.data[contractName]).reduce((acc: Properties, propertyName: string) => {
|
||||||
|
const type = this.data[contractName][propertyName];
|
||||||
|
if (IPFS.includes(type)) {
|
||||||
|
acc[propertyName] = type;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public associationAttributes(contractName: string): Properties {
|
||||||
|
return Object.keys(this.data[contractName]).reduce((acc: Properties, propertyName: string) => {
|
||||||
|
const type = this.data[contractName][propertyName];
|
||||||
|
if (ASSOCIATIONS.includes(type)) {
|
||||||
|
acc[propertyName] = type;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public validate() {
|
||||||
|
if (!scaffoldingSchema(this.data)) {
|
||||||
|
this.logger.error(__("The scaffolding schema is not valid:"));
|
||||||
|
this.logger.error(ajv.errorsText(scaffoldingSchema.errors));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contractNames = Object.keys(this.data);
|
||||||
|
contractNames.forEach((contractName) => {
|
||||||
|
if (contractName[0] !== contractName[0].toUpperCase()) {
|
||||||
|
this.logger.error(__(`${contractName} must be capitalized.`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
Object.keys(this.associationAttributes(contractName)).forEach((associationName) => {
|
||||||
|
if (associationName === contractName) {
|
||||||
|
this.logger.error(__(`${contractName} is referring to himself.`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contractNames.includes(associationName)) {
|
||||||
|
this.logger.error(__(`${contractName} not found. Please make sure it is in the description.`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(this.data[associationName]).includes(contractName)) {
|
||||||
|
this.logger.error(__(`${associationName} has a cyclic dependencies with ${contractName}.`));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private build() {
|
||||||
|
return {
|
||||||
|
[this.contractOrFile]: this.fields.reduce((acc: Properties, property) => {
|
||||||
|
const [name, value] = property.split(":");
|
||||||
|
acc[name] = value;
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { ABIDefinition } from "web3/eth/abi";
|
||||||
|
|
||||||
|
export interface Contract {
|
||||||
|
abiDefinition: ABIDefinition[];
|
||||||
|
className: string;
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ export interface Events {
|
||||||
request: any;
|
request: any;
|
||||||
emit: any;
|
emit: any;
|
||||||
once: any;
|
once: any;
|
||||||
setCommandHandler: any;
|
setCommandHandler(name: string, callback: (options: any, cb: () => void) => void): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Embark {
|
export interface Embark {
|
||||||
|
@ -13,6 +13,13 @@ export interface Embark {
|
||||||
registerAPICall: any;
|
registerAPICall: any;
|
||||||
registerConsoleCommand: any;
|
registerConsoleCommand: any;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
config: {};
|
config: {
|
||||||
|
embarkConfig: {
|
||||||
|
contracts: string[] | string;
|
||||||
|
config: {
|
||||||
|
contracts: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
registerActionForEvent(name: string, action: (callback: () => void) => void): void;
|
registerActionForEvent(name: string, action: (callback: () => void) => void): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
declare function __(...values: string[]): string;
|
|
@ -0,0 +1,8 @@
|
||||||
|
export interface Plugin {
|
||||||
|
dappGenerators: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Plugins {
|
||||||
|
getPluginsFor(name: string): [Plugin];
|
||||||
|
loadInternalPlugin(name: string, options: any): void;
|
||||||
|
}
|
|
@ -13,7 +13,8 @@
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"*": ["./typings/*"]
|
"*": ["./typings/*"]
|
||||||
}
|
},
|
||||||
|
"noImplicitThis": false
|
||||||
},
|
},
|
||||||
"include": ["./src/**/*"]
|
"include": ["./src/**/*"]
|
||||||
}
|
}
|
25
yarn.lock
25
yarn.lock
|
@ -825,6 +825,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/handlebars@4.0.39":
|
||||||
|
version "4.0.39"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.39.tgz#961fb54db68030890942e6aeffe9f93a957807bd"
|
||||||
|
integrity sha512-vjaS7Q0dVqFp85QhyPSZqDKnTTCemcSHNHFvDdalO1s0Ifz5KuE64jQD5xoUkfdWwF4WpqdJEl7LsWH8rzhKJA==
|
||||||
|
|
||||||
"@types/i18n@0.8.3":
|
"@types/i18n@0.8.3":
|
||||||
version "0.8.3"
|
version "0.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/i18n/-/i18n-0.8.3.tgz#f602164f2fae486ea87590f6be5d6dd5db1664e6"
|
resolved "https://registry.yarnpkg.com/@types/i18n/-/i18n-0.8.3.tgz#f602164f2fae486ea87590f6be5d6dd5db1664e6"
|
||||||
|
@ -1187,6 +1192,16 @@ ajv-keywords@^3.0.0, ajv-keywords@^3.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
|
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
|
||||||
integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=
|
integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=
|
||||||
|
|
||||||
|
ajv@6.5.5, ajv@^6.0.1, ajv@^6.1.0, ajv@^6.5.3, ajv@^6.5.5:
|
||||||
|
version "6.5.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.5.tgz#cf97cdade71c6399a92c6d6c4177381291b781a1"
|
||||||
|
integrity sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==
|
||||||
|
dependencies:
|
||||||
|
fast-deep-equal "^2.0.1"
|
||||||
|
fast-json-stable-stringify "^2.0.0"
|
||||||
|
json-schema-traverse "^0.4.1"
|
||||||
|
uri-js "^4.2.2"
|
||||||
|
|
||||||
ajv@^4.7.0:
|
ajv@^4.7.0:
|
||||||
version "4.11.8"
|
version "4.11.8"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
|
||||||
|
@ -1205,16 +1220,6 @@ ajv@^5.0.0, ajv@^5.1.0, ajv@^5.1.5, ajv@^5.2.0:
|
||||||
fast-json-stable-stringify "^2.0.0"
|
fast-json-stable-stringify "^2.0.0"
|
||||||
json-schema-traverse "^0.3.0"
|
json-schema-traverse "^0.3.0"
|
||||||
|
|
||||||
ajv@^6.0.1, ajv@^6.1.0, ajv@^6.5.3, ajv@^6.5.5:
|
|
||||||
version "6.5.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.5.tgz#cf97cdade71c6399a92c6d6c4177381291b781a1"
|
|
||||||
integrity sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==
|
|
||||||
dependencies:
|
|
||||||
fast-deep-equal "^2.0.1"
|
|
||||||
fast-json-stable-stringify "^2.0.0"
|
|
||||||
json-schema-traverse "^0.4.1"
|
|
||||||
uri-js "^4.2.2"
|
|
||||||
|
|
||||||
align-text@^0.1.1, align-text@^0.1.3:
|
align-text@^0.1.1, align-text@^0.1.3:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
|
resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
|
||||||
|
|
Loading…
Reference in New Issue