From 92b7967895ac79d1126e97df88ce4c5041365427 Mon Sep 17 00:00:00 2001 From: Ashwin Phatak Date: Thu, 23 Sep 2021 16:55:46 +0530 Subject: [PATCH] Generating eth_call based lazy watcher (#249) * Add entity generation. * Add resolvers generation. * Add queries in resolvers generation. * Add indexer generation. * Extract helper code in utils. * Add server and artifacts generation. * Fix solidity-flattener issue. * Update readme and cleanup misc files. * Add queries to entity generation. * Add database generation. * Use snakecase in database. * Add readme generation. * Change template file names. * Add method descriptions. * Change mode to eth_call in readme. Co-authored-by: prathamesh --- packages/codegen/README.md | 21 +- packages/codegen/package.json | 4 +- packages/codegen/src/artifacts.ts | 35 ++++ packages/codegen/src/config.ts | 24 +++ packages/codegen/src/database.ts | 73 +++++++ packages/codegen/src/entity.ts | 97 ++++++++++ packages/codegen/src/generate-code.ts | 179 ++++++++++++++++++ packages/codegen/src/generate-schema.ts | 86 --------- packages/codegen/src/indexer.ts | 76 ++++++++ packages/codegen/src/package.ts | 24 +++ packages/codegen/src/readme.ts | 26 +++ packages/codegen/src/resolvers.ts | 66 +++++++ packages/codegen/src/schema.ts | 34 ++-- packages/codegen/src/server.ts | 21 ++ .../src/templates/config-template.handlebars | 25 +++ .../templates/database-template.handlebars | 72 +++++++ .../src/templates/entity-template.handlebars | 30 +++ .../src/templates/indexer-template.handlebars | 98 ++++++++++ .../src/templates/package-template.handlebars | 44 +++++ .../src/templates/readme-template.handlebars | 51 +++++ .../templates/resolvers-template.handlebars | 46 +++++ .../src/templates/server-template.handlebars | 97 ++++++++++ .../templates/tsconfig-template.handlebars | 74 ++++++++ packages/codegen/src/tsconfig.ts | 21 ++ packages/codegen/src/types/common/main.d.ts | 1 + .../codegen/src/types/common/package.json | 8 +- .../codegen/src/utils/handlebar-helpers.ts | 46 +++++ packages/codegen/src/utils/type-mappings.ts | 42 ++++ packages/codegen/src/utils/types.ts | 8 + packages/codegen/src/visitor.ts | 57 +++++- packages/codegen/tsconfig.json | 8 +- yarn.lock | 19 +- 32 files changed, 1388 insertions(+), 125 deletions(-) create mode 100644 packages/codegen/src/artifacts.ts create mode 100644 packages/codegen/src/config.ts create mode 100644 packages/codegen/src/database.ts create mode 100644 packages/codegen/src/entity.ts create mode 100644 packages/codegen/src/generate-code.ts delete mode 100644 packages/codegen/src/generate-schema.ts create mode 100644 packages/codegen/src/indexer.ts create mode 100644 packages/codegen/src/package.ts create mode 100644 packages/codegen/src/readme.ts create mode 100644 packages/codegen/src/resolvers.ts create mode 100644 packages/codegen/src/server.ts create mode 100644 packages/codegen/src/templates/config-template.handlebars create mode 100644 packages/codegen/src/templates/database-template.handlebars create mode 100644 packages/codegen/src/templates/entity-template.handlebars create mode 100644 packages/codegen/src/templates/indexer-template.handlebars create mode 100644 packages/codegen/src/templates/package-template.handlebars create mode 100644 packages/codegen/src/templates/readme-template.handlebars create mode 100644 packages/codegen/src/templates/resolvers-template.handlebars create mode 100644 packages/codegen/src/templates/server-template.handlebars create mode 100644 packages/codegen/src/templates/tsconfig-template.handlebars create mode 100644 packages/codegen/src/tsconfig.ts create mode 100644 packages/codegen/src/utils/handlebar-helpers.ts create mode 100644 packages/codegen/src/utils/type-mappings.ts create mode 100644 packages/codegen/src/utils/types.ts diff --git a/packages/codegen/README.md b/packages/codegen/README.md index 1da97708..72f7acc1 100644 --- a/packages/codegen/README.md +++ b/packages/codegen/README.md @@ -10,14 +10,15 @@ ## Run -* Run the following command to generate schema from a contract file: +* Run the following command to generate a watcher from a contract file: ```bash - yarn codegen:gql --input-file --output-file [output-file-path] --mode [eth_call | storage] --flatten [true | false] + yarn codegen --input-file --contract-name --output-folder [output-folder] --mode [eth_call | storage] --flatten [true | false] ``` * `input-file`(alias: `i`): Input contract file path or an URL (required). - * `output-file`(alias: `o`): Schema output file path (logs output using `stdout` if not provided). + * `contract-name`(alias: `c`): Main contract name (required). + * `output-folder`(alias: `o`): Output folder path. (logs output using `stdout` if not provided). * `mode`(alias: `m`): Code generation mode (default: `storage`). * `flatten`(alias: `f`): Flatten the input contract file (default: `true`). @@ -26,11 +27,11 @@ Examples: ```bash - yarn codegen:gql --input-file ./test/examples/contracts/ERC20.sol --output-file ./ERC20-schema.gql --mode eth_call + yarn codegen --input-file ./test/examples/contracts/ERC20.sol --contract-name ERC20 --output-folder ../my-erc20-watcher --mode eth_call ``` ```bash - yarn codegen:gql --input-file https://git.io/Jupci --output-file ./ERC721-schema.gql --mode storage + yarn codegen --input-file https://git.io/Jupci --contract-name ERC721 --output-folder ../my-erc721-watcher --mode eth_call ``` ## Demo @@ -41,16 +42,18 @@ yarn ``` -* Generate schema from a contract file: +* Generate a watcher from a contract file: ```bash - yarn codegen:gql --input-file ../../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol --output-file ./ERC20-schema.gql --mode storage + yarn codegen --input-file ../../node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol --contract-name ERC20 --output-folder ../demo-erc20-watcher --mode eth_call ``` -* Generate schema from a flattened contract file from an URL: + This will create a folder called `demo-erc20-watcher` containing the generated code at the specified path. Follow the steps in `demo-erc20-watcher/README.md` to setup and run the generated watcher. + +* Generate a watcher from a flattened contract file from an URL: ```bash - yarn codegen:gql --input-file https://git.io/Jupci --output-file ./ERC721-schema.gql --mode eth_call + yarn codegen --input-file https://git.io/Jupci --contract-name ERC721 --output-folder ../demo-erc721-watcher --mode eth_call ``` ## References diff --git a/packages/codegen/package.json b/packages/codegen/package.json index 3f993397..383b0a4b 100644 --- a/packages/codegen/package.json +++ b/packages/codegen/package.json @@ -6,7 +6,7 @@ "main": "index.js", "scripts": { "lint": "eslint .", - "codegen:gql": "ts-node src/generate-schema.ts" + "codegen": "ts-node src/generate-code.ts" }, "repository": { "type": "git", @@ -23,7 +23,9 @@ "@solidity-parser/parser": "^0.13.2", "graphql": "^15.5.0", "graphql-compose": "^9.0.3", + "handlebars": "^4.7.7", "node-fetch": "^2", + "solc": "^0.8.7-fixed", "ts-node": "^10.2.1", "typescript": "^4.3.2", "yargs": "^17.1.1" diff --git a/packages/codegen/src/artifacts.ts b/packages/codegen/src/artifacts.ts new file mode 100644 index 00000000..d6a12a37 --- /dev/null +++ b/packages/codegen/src/artifacts.ts @@ -0,0 +1,35 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import solc from 'solc'; +import { Writable } from 'stream'; + +/** + * Compiles the given contract using solc and writes the resultant artifacts to a file. + * @param outStream A writable output stream to write the artifacts file to. + * @param contractContent Contents of the contract file to be compiled. + * @param contractFileName Input contract file name. + * @param contractName Name of the main contract in the contract file. + */ +export function exportArtifacts (outStream: Writable, contractContent: string, contractFileName: string, contractName: string): void { + const input: any = { + language: 'Solidity', + sources: {}, + settings: { + outputSelection: { + '*': { + '*': ['abi', 'storageLayout'] + } + } + } + }; + + input.sources[contractFileName] = { + content: contractContent + }; + + // Get artifacts for the required contract. + const output = JSON.parse(solc.compile(JSON.stringify(input))).contracts[contractFileName][contractName]; + outStream.write(JSON.stringify(output, null, 2)); +} diff --git a/packages/codegen/src/config.ts b/packages/codegen/src/config.ts new file mode 100644 index 00000000..665bf009 --- /dev/null +++ b/packages/codegen/src/config.ts @@ -0,0 +1,24 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import fs from 'fs'; +import path from 'path'; +import Handlebars from 'handlebars'; +import { Writable } from 'stream'; + +const TEMPLATE_FILE = './templates/config-template.handlebars'; + +/** + * Writes the config file generated from a template to a stream. + * @param folderName Watcher folder name to be passed to the template. + * @param outStream A writable output stream to write the config file to. + */ +export function exportConfig (folderName: string, outStream: Writable): void { + const templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString(); + const template = Handlebars.compile(templateString); + const config = template({ + folderName + }); + outStream.write(config); +} diff --git a/packages/codegen/src/database.ts b/packages/codegen/src/database.ts new file mode 100644 index 00000000..e7108174 --- /dev/null +++ b/packages/codegen/src/database.ts @@ -0,0 +1,73 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import fs from 'fs'; +import path from 'path'; +import assert from 'assert'; +import Handlebars from 'handlebars'; +import { Writable } from 'stream'; +import _ from 'lodash'; + +import { getTsForSol } from './utils/type-mappings'; +import { Param } from './utils/types'; +import { capitalizeHelper } from './utils/handlebar-helpers'; + +const TEMPLATE_FILE = './templates/database-template.handlebars'; + +export class Database { + _queries: Array; + _templateString: string; + + constructor () { + this._queries = []; + this._templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString(); + + Handlebars.registerHelper('capitalize', capitalizeHelper); + } + + /** + * Stores the query to be passed to the template. + * @param name Name of the query. + * @param params Parameters to the query. + * @param returnType Return type for the query. + */ + addQuery (name: string, params: Array, returnType: string): void { + // Check if the query is already added. + if (this._queries.some(query => query.name === name)) { + return; + } + + const queryObject = { + name: name, + params: _.cloneDeep(params), + returnType: returnType + }; + + queryObject.params = queryObject.params.map((param) => { + const tsParamType = getTsForSol(param.type); + assert(tsParamType); + param.type = tsParamType; + return param; + }); + + const tsReturnType = getTsForSol(returnType); + assert(tsReturnType); + queryObject.returnType = tsReturnType; + + this._queries.push(queryObject); + } + + /** + * Writes the database file generated from a template to a stream. + * @param outStream A writable output stream to write the database file to. + */ + exportDatabase (outStream: Writable): void { + const template = Handlebars.compile(this._templateString); + const obj = { + queries: this._queries + }; + const database = template(obj); + outStream.write(database); + } +} diff --git a/packages/codegen/src/entity.ts b/packages/codegen/src/entity.ts new file mode 100644 index 00000000..7b455521 --- /dev/null +++ b/packages/codegen/src/entity.ts @@ -0,0 +1,97 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import fs from 'fs'; +import path from 'path'; +import assert from 'assert'; +import Handlebars from 'handlebars'; +import { Writable } from 'stream'; + +import { getTsForSol, getPgForTs } from './utils/type-mappings'; +import { Param } from './utils/types'; + +const TEMPLATE_FILE = './templates/entity-template.handlebars'; + +export class Entity { + _entities: Array; + _templateString: string; + + constructor () { + this._entities = []; + this._templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString(); + } + + /** + * Creates an entity object from the query and stores to be passed to the template. + * @param name Name of the query. + * @param params Parameters to the query. + * @param returnType Return type for the query. + */ + addQuery (name: string, params: Array, returnType: string): void { + // Check if the query is already added. + if (this._entities.some(entity => entity.className.toLowerCase() === name.toLowerCase())) { + return; + } + + const entityObject: any = { + // Capitalize the first letter of name. + className: `${name.charAt(0).toUpperCase()}${name.slice(1)}`, + indexOn: {}, + columns: [{}], + returnType: returnType + }; + + entityObject.indexOn.columns = params.map((param) => { + return param.name; + }); + entityObject.indexOn.unique = true; + + entityObject.columns = params.map((param) => { + const length = param.type === 'address' ? 42 : null; + const name = param.name; + + const tsType = getTsForSol(param.type); + assert(tsType); + + const pgType = getPgForTs(tsType); + assert(pgType); + + return { + name, + pgType, + tsType, + length + }; + }); + + const tsReturnType = getTsForSol(returnType); + assert(tsReturnType); + + const pgReturnType = getPgForTs(tsReturnType); + assert(pgReturnType); + + entityObject.columns.push({ + name: 'value', + pgType: pgReturnType, + tsType: tsReturnType + }); + + this._entities.push(entityObject); + } + + /** + * Writes the generated entity files in the given directory. + * @param entityDir Directory to write the entities to. + */ + exportEntities (entityDir: string): void { + const template = Handlebars.compile(this._templateString); + this._entities.forEach(entityObj => { + const entity = template(entityObj); + const outStream: Writable = entityDir + ? fs.createWriteStream(path.join(entityDir, `${entityObj.className}.ts`)) + : process.stdout; + outStream.write(entity); + }); + } +} diff --git a/packages/codegen/src/generate-code.ts b/packages/codegen/src/generate-code.ts new file mode 100644 index 00000000..d15dbaf0 --- /dev/null +++ b/packages/codegen/src/generate-code.ts @@ -0,0 +1,179 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import fs from 'fs'; +import fetch from 'node-fetch'; +import path from 'path'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import { flatten } from '@poanet/solidity-flattener'; + +import { parse, visit } from '@solidity-parser/parser'; + +import { Visitor } from './visitor'; +import { exportServer } from './server'; +import { exportConfig } from './config'; +import { exportArtifacts } from './artifacts'; +import { exportPackage } from './package'; +import { exportTSConfig } from './tsconfig'; +import { exportReadme } from './readme'; + +const MODE_ETH_CALL = 'eth_call'; +const MODE_STORAGE = 'storage'; + +const main = async (): Promise => { + const argv = await yargs(hideBin(process.argv)) + .option('input-file', { + alias: 'i', + demandOption: true, + describe: 'Input contract file path or an url.', + type: 'string' + }) + .option('contract-name', { + alias: 'c', + demandOption: true, + describe: 'Main contract name.', + type: 'string' + }) + .option('output-folder', { + alias: 'o', + describe: 'Output folder path.', + type: 'string' + }) + .option('mode', { + alias: 'm', + describe: 'Code generation mode.', + type: 'string', + default: MODE_STORAGE, + choices: [MODE_ETH_CALL, MODE_STORAGE] + }) + .option('flatten', { + alias: 'f', + describe: 'Flatten the input contract file.', + type: 'boolean', + default: true + }) + .argv; + + let data: string; + if (argv['input-file'].startsWith('http')) { + // Assume flattened file in case of URL. + const response = await fetch(argv['input-file']); + data = await response.text(); + } else { + data = argv.flatten + ? await flatten(path.resolve(argv['input-file'])) + : fs.readFileSync(path.resolve(argv['input-file'])).toString(); + } + + const visitor = new Visitor(); + + parseAndVisit(data, visitor, argv.mode); + + generateWatcher(data, visitor, argv); +}; + +function parseAndVisit (data: string, visitor: Visitor, mode: string) { + // Get the abstract syntax tree for the flattened contract. + const ast = parse(data); + + // Filter out library nodes. + ast.children = ast.children.filter(child => !(child.type === 'ContractDefinition' && child.kind === 'library')); + + if (mode === MODE_ETH_CALL) { + visit(ast, { + FunctionDefinition: visitor.functionDefinitionVisitor.bind(visitor), + EventDefinition: visitor.eventDefinitionVisitor.bind(visitor) + }); + } else { + visit(ast, { + StateVariableDeclaration: visitor.stateVariableDeclarationVisitor.bind(visitor), + EventDefinition: visitor.eventDefinitionVisitor.bind(visitor) + }); + } +} + +function generateWatcher (data: string, visitor: Visitor, argv: any) { + // Prepare directory structure for the watcher. + let outputDir = ''; + if (argv['output-folder']) { + outputDir = path.resolve(argv['output-folder']); + if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true }); + + const environmentsFolder = path.join(outputDir, 'environments'); + if (!fs.existsSync(environmentsFolder)) fs.mkdirSync(environmentsFolder); + + const artifactsFolder = path.join(outputDir, 'src/artifacts'); + if (!fs.existsSync(artifactsFolder)) fs.mkdirSync(artifactsFolder, { recursive: true }); + + const entitiesFolder = path.join(outputDir, 'src/entity'); + if (!fs.existsSync(entitiesFolder)) fs.mkdirSync(entitiesFolder, { recursive: true }); + } + + const inputFileName = path.basename(argv['input-file'], '.sol'); + + let outStream = outputDir + ? fs.createWriteStream(path.join(outputDir, 'src/schema.gql')) + : process.stdout; + visitor.exportSchema(outStream); + + outStream = outputDir + ? fs.createWriteStream(path.join(outputDir, 'src/resolvers.ts')) + : process.stdout; + visitor.exportResolvers(outStream); + + outStream = outputDir + ? fs.createWriteStream(path.join(outputDir, 'src/indexer.ts')) + : process.stdout; + visitor.exportIndexer(outStream, inputFileName); + + outStream = outputDir + ? fs.createWriteStream(path.join(outputDir, 'src/server.ts')) + : process.stdout; + exportServer(outStream); + + outStream = outputDir + ? fs.createWriteStream(path.join(outputDir, 'environments/local.toml')) + : process.stdout; + exportConfig(path.basename(outputDir), outStream); + + outStream = outputDir + ? fs.createWriteStream(path.join(outputDir, 'src/artifacts/', `${inputFileName}.json`)) + : process.stdout; + exportArtifacts( + outStream, + data, + `${inputFileName}.sol`, + argv['contract-name'] + ); + + outStream = outputDir + ? fs.createWriteStream(path.join(outputDir, 'src/database.ts')) + : process.stdout; + visitor.exportDatabase(outStream); + + outStream = outputDir + ? fs.createWriteStream(path.join(outputDir, 'package.json')) + : process.stdout; + exportPackage(path.basename(outputDir), outStream); + + outStream = outputDir + ? fs.createWriteStream(path.join(outputDir, 'tsconfig.json')) + : process.stdout; + exportTSConfig(outStream); + + const entityDir = outputDir + ? path.join(outputDir, 'src/entity') + : ''; + visitor.exportEntities(entityDir); + + outStream = outputDir + ? fs.createWriteStream(path.join(outputDir, 'README.md')) + : process.stdout; + exportReadme(path.basename(outputDir), argv['contract-name'], outStream); +} + +main().catch(err => { + console.error(err); +}); diff --git a/packages/codegen/src/generate-schema.ts b/packages/codegen/src/generate-schema.ts deleted file mode 100644 index bc2b83ba..00000000 --- a/packages/codegen/src/generate-schema.ts +++ /dev/null @@ -1,86 +0,0 @@ -// -// Copyright 2021 Vulcanize, Inc. -// - -import { readFileSync, createWriteStream } from 'fs'; -import fetch from 'node-fetch'; -import path from 'path'; -import yargs from 'yargs'; -import { hideBin } from 'yargs/helpers'; -import { flatten } from '@poanet/solidity-flattener'; - -import { parse, visit } from '@solidity-parser/parser'; - -import { Visitor } from './visitor'; - -const MODE_ETH_CALL = 'eth_call'; -const MODE_STORAGE = 'storage'; - -const main = async (): Promise => { - const argv = await yargs(hideBin(process.argv)) - .option('input-file', { - alias: 'i', - demandOption: true, - describe: 'Input contract file path or an url.', - type: 'string' - }) - .option('output-file', { - alias: 'o', - describe: 'Schema output file path.', - type: 'string' - }) - .option('mode', { - alias: 'm', - describe: 'Code generation mode.', - type: 'string', - default: MODE_STORAGE, - choices: [MODE_ETH_CALL, MODE_STORAGE] - }) - .option('flatten', { - alias: 'f', - describe: 'Flatten the input contract file.', - type: 'boolean', - default: true - }) - .argv; - - let data: string; - if (argv['input-file'].startsWith('http')) { - // Assume flattened file in case of URL. - const response = await fetch(argv['input-file']); - data = await response.text(); - } else { - data = argv.flatten - ? await flatten(path.resolve(argv['input-file'])) - : readFileSync(path.resolve(argv['input-file'])).toString(); - } - - // Get the abstract syntax tree for the flattened contract. - const ast = parse(data); - - // Filter out library nodes. - ast.children = ast.children.filter(child => !(child.type === 'ContractDefinition' && child.kind === 'library')); - - const visitor = new Visitor(); - - if (argv.mode === MODE_ETH_CALL) { - visit(ast, { - FunctionDefinition: visitor.functionDefinitionVisitor.bind(visitor), - EventDefinition: visitor.eventDefinitionVisitor.bind(visitor) - }); - } else { - visit(ast, { - StateVariableDeclaration: visitor.stateVariableDeclarationVisitor.bind(visitor), - EventDefinition: visitor.eventDefinitionVisitor.bind(visitor) - }); - } - - const outStream = argv['output-file'] - ? createWriteStream(path.resolve(argv['output-file'])) - : process.stdout; - visitor.exportSchema(outStream); -}; - -main().catch(err => { - console.error(err); -}); diff --git a/packages/codegen/src/indexer.ts b/packages/codegen/src/indexer.ts new file mode 100644 index 00000000..eca13168 --- /dev/null +++ b/packages/codegen/src/indexer.ts @@ -0,0 +1,76 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import fs from 'fs'; +import path from 'path'; +import assert from 'assert'; +import Handlebars from 'handlebars'; +import { Writable } from 'stream'; +import _ from 'lodash'; + +import { getTsForSol } from './utils/type-mappings'; +import { Param } from './utils/types'; +import { compareHelper, capitalizeHelper } from './utils/handlebar-helpers'; + +const TEMPLATE_FILE = './templates/indexer-template.handlebars'; + +export class Indexer { + _queries: Array; + _templateString: string; + + constructor () { + this._queries = []; + this._templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString(); + + Handlebars.registerHelper('compare', compareHelper); + Handlebars.registerHelper('capitalize', capitalizeHelper); + } + + /** + * Stores the query to be passed to the template. + * @param name Name of the query. + * @param params Parameters to the query. + * @param returnType Return type for the query. + */ + addQuery (name: string, params: Array, returnType: string): void { + // Check if the query is already added. + if (this._queries.some(query => query.name === name)) { + return; + } + + const queryObject = { + name: name, + params: _.cloneDeep(params), + returnType: returnType + }; + + queryObject.params = queryObject.params.map((param) => { + const tsParamType = getTsForSol(param.type); + assert(tsParamType); + param.type = tsParamType; + return param; + }); + + const tsReturnType = getTsForSol(returnType); + assert(tsReturnType); + queryObject.returnType = tsReturnType; + + this._queries.push(queryObject); + } + + /** + * Writes the indexer file generated from a template to a stream. + * @param outStream A writable output stream to write the indexer file to. + * @param inputFileName Input contract file name to be passed to the template. + */ + exportIndexer (outStream: Writable, inputFileName: string): void { + const template = Handlebars.compile(this._templateString); + const obj = { + inputFileName, + queries: this._queries + }; + const indexer = template(obj); + outStream.write(indexer); + } +} diff --git a/packages/codegen/src/package.ts b/packages/codegen/src/package.ts new file mode 100644 index 00000000..e0d61940 --- /dev/null +++ b/packages/codegen/src/package.ts @@ -0,0 +1,24 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import fs from 'fs'; +import path from 'path'; +import Handlebars from 'handlebars'; +import { Writable } from 'stream'; + +const TEMPLATE_FILE = './templates/package-template.handlebars'; + +/** + * Writes the package.json file generated from a template to a stream. + * @param folderName Watcher folder name to be passed to the template. + * @param outStream A writable output stream to write the package.json file to. + */ +export function exportPackage (folderName: string, outStream: Writable): void { + const templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString(); + const template = Handlebars.compile(templateString); + const packageString = template({ + folderName + }); + outStream.write(packageString); +} diff --git a/packages/codegen/src/readme.ts b/packages/codegen/src/readme.ts new file mode 100644 index 00000000..cf4c912e --- /dev/null +++ b/packages/codegen/src/readme.ts @@ -0,0 +1,26 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import fs from 'fs'; +import path from 'path'; +import Handlebars from 'handlebars'; +import { Writable } from 'stream'; + +const TEMPLATE_FILE = './templates/readme-template.handlebars'; + +/** + * Writes the README.md file generated from a template to a stream. + * @param folderName Watcher folder name to be passed to the template. + * @param contractName Input contract name given as title of the README. + * @param outStream A writable output stream to write the README.md file to. + */ +export function exportReadme (folderName: string, contractName: string, outStream: Writable): void { + const templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString(); + const template = Handlebars.compile(templateString); + const readmeString = template({ + folderName, + contractName + }); + outStream.write(readmeString); +} diff --git a/packages/codegen/src/resolvers.ts b/packages/codegen/src/resolvers.ts new file mode 100644 index 00000000..e3dba792 --- /dev/null +++ b/packages/codegen/src/resolvers.ts @@ -0,0 +1,66 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import fs from 'fs'; +import path from 'path'; +import { Writable } from 'stream'; +import Handlebars from 'handlebars'; +import assert from 'assert'; +import _ from 'lodash'; + +import { getTsForSol } from './utils/type-mappings'; +import { Param } from './utils/types'; + +const TEMPLATE_FILE = './templates/resolvers-template.handlebars'; + +export class Resolvers { + _queries: Array; + _templateString: string; + + constructor () { + this._queries = []; + this._templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString(); + } + + /** + * Stores the query to be passed to the template. + * @param name Name of the query. + * @param params Parameters to the query. + * @param returnType Return type for the query. + */ + addQuery (name: string, params: Array, returnType: string): void { + // Check if the query is already added. + if (this._queries.some(query => query.name === name)) { + return; + } + + const queryObject = { + name: name, + params: _.cloneDeep(params), + returnType: returnType + }; + + queryObject.params = queryObject.params.map((param) => { + const tsParamType = getTsForSol(param.type); + assert(tsParamType); + param.type = tsParamType; + return param; + }); + + this._queries.push(queryObject); + } + + /** + * Writes the resolvers file generated from a template to a stream. + * @param outStream A writable output stream to write the resolvers file to. + */ + exportResolvers (outStream: Writable): void { + const template = Handlebars.compile(this._templateString); + const obj = { + queries: this._queries + }; + const resolvers = template(obj); + outStream.write(resolvers); + } +} diff --git a/packages/codegen/src/schema.ts b/packages/codegen/src/schema.ts index 208684d5..19eee6f1 100644 --- a/packages/codegen/src/schema.ts +++ b/packages/codegen/src/schema.ts @@ -2,23 +2,20 @@ // Copyright 2021 Vulcanize, Inc. // +import assert from 'assert'; import { GraphQLSchema, printSchema } from 'graphql'; import { SchemaComposer } from 'graphql-compose'; import { Writable } from 'stream'; -export interface Param { - name: string; - type: string; -} +import { getTsForSol, getGqlForTs } from './utils/type-mappings'; +import { Param } from './utils/types'; export class Schema { _composer: SchemaComposer; - _typeMapping: Map; _events: Array; constructor () { this._composer = new SchemaComposer(); - this._typeMapping = new Map(); this._events = []; this._addBasicTypes(); @@ -32,10 +29,13 @@ export class Schema { */ addQuery (name: string, params: Array, returnType: string): void { // TODO: Handle cases where returnType/params type is an array. + const tsReturnType = getTsForSol(returnType); + assert(tsReturnType); + const queryObject: { [key: string]: any; } = {}; queryObject[name] = { // Get type composer object for return type from the schema composer. - type: this._composer.getOTC(`Result${this._typeMapping.get(returnType)}`).NonNull, + type: this._composer.getOTC(`Result${getGqlForTs(tsReturnType)}`).NonNull, args: { blockHash: 'String!', contractAddress: 'String!' @@ -44,7 +44,9 @@ export class Schema { if (params.length > 0) { queryObject[name].args = params.reduce((acc, curr) => { - acc[curr.name] = this._typeMapping.get(curr.type) + '!'; + const tsCurrType = getTsForSol(curr.type); + assert(tsCurrType); + acc[curr.name] = `${getGqlForTs(tsCurrType)}!`; return acc; }, queryObject[name].args); } @@ -67,7 +69,9 @@ export class Schema { if (params.length > 0) { typeObject.fields = params.reduce((acc, curr) => { - acc[curr.name] = this._typeMapping.get(curr.type) + '!'; + const tsCurrType = getTsForSol(curr.type); + assert(tsCurrType); + acc[curr.name] = `${getGqlForTs(tsCurrType)}!`; return acc; }, typeObject.fields); } @@ -99,8 +103,8 @@ export class Schema { */ exportSchema (outStream: Writable): void { // Get schema as a string from GraphQLSchema. - const schema = printSchema(this.buildSchema()); - outStream.write(schema); + const schemaString = printSchema(this.buildSchema()); + outStream.write(schemaString); } /** @@ -152,14 +156,6 @@ export class Schema { proof: () => this._composer.getOTC('Proof') } }); - - // TODO Get typemapping from ethersjs. - this._typeMapping.set('string', 'String'); - this._typeMapping.set('uint8', 'Int'); - this._typeMapping.set('uint256', 'BigInt'); - this._typeMapping.set('address', 'String'); - this._typeMapping.set('bool', 'Boolean'); - this._typeMapping.set('bytes4', 'String'); } /** diff --git a/packages/codegen/src/server.ts b/packages/codegen/src/server.ts new file mode 100644 index 00000000..a15327d4 --- /dev/null +++ b/packages/codegen/src/server.ts @@ -0,0 +1,21 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import fs from 'fs'; +import path from 'path'; +import Handlebars from 'handlebars'; +import { Writable } from 'stream'; + +const TEMPLATE_FILE = './templates/server-template.handlebars'; + +/** + * Writes the server file generated from a template to a stream. + * @param outStream A writable output stream to write the server file to. + */ +export function exportServer (outStream: Writable): void { + const templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString(); + const template = Handlebars.compile(templateString); + const server = template({}); + outStream.write(server); +} diff --git a/packages/codegen/src/templates/config-template.handlebars b/packages/codegen/src/templates/config-template.handlebars new file mode 100644 index 00000000..549ac581 --- /dev/null +++ b/packages/codegen/src/templates/config-template.handlebars @@ -0,0 +1,25 @@ +[server] + host = "127.0.0.1" + port = 3008 + mode = "eth_call" + +[database] + type = "postgres" + host = "localhost" + port = 5432 + database = "{{folderName}}" + username = "postgres" + password = "postgres" + synchronize = true + logging = false + +[upstream] + [upstream.ethServer] + gqlApiEndpoint = "http://127.0.0.1:8082/graphql" + gqlPostgraphileEndpoint = "http://127.0.0.1:5000/graphql" + rpcProviderEndpoint = "http://127.0.0.1:8081" + + [upstream.cache] + name = "requests" + enabled = false + deleteOnStart = false diff --git a/packages/codegen/src/templates/database-template.handlebars b/packages/codegen/src/templates/database-template.handlebars new file mode 100644 index 00000000..72131ef1 --- /dev/null +++ b/packages/codegen/src/templates/database-template.handlebars @@ -0,0 +1,72 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import { Connection, ConnectionOptions, DeepPartial } from 'typeorm'; +import path from 'path'; + +import { Database as BaseDatabase } from '@vulcanize/util'; + +{{#each queries as | query |}} +import { {{capitalize query.name tillIndex=1}} } from './entity/{{capitalize query.name tillIndex=1}}'; +{{/each}} + +export class Database { + _config: ConnectionOptions + _conn!: Connection + _baseDatabase: BaseDatabase; + + constructor (config: ConnectionOptions) { + assert(config); + + this._config = { + ...config, + entities: [path.join(__dirname, 'entity/*')] + }; + + this._baseDatabase = new BaseDatabase(this._config); + } + + async init (): Promise { + this._conn = await this._baseDatabase.init(); + } + + async close (): Promise { + return this._baseDatabase.close(); + } + + {{#each queries as | query |}} + async get{{capitalize query.name tillIndex=1}} ({ blockHash, contractAddress + {{~#each query.params}}, {{this.name~}} {{/each}} }: { blockHash: string, contractAddress: string + {{~#each query.params}}, {{this.name~}}: {{this.type~}} {{/each}} }): Promise<{{capitalize query.name tillIndex=1}} | undefined> { + return this._conn.getRepository({{capitalize query.name tillIndex=1}}) + .createQueryBuilder('{{query.name}}') + .where(`${this._getColumn('{{capitalize query.name tillIndex=1}}', 'blockHash')} = :blockHash AND ${this._getColumn('{{capitalize query.name tillIndex=1}}', 'contractAddress')} = :contractAddress + {{~#each query.params}} AND ${this._getColumn('{{capitalize query.name tillIndex=1}}', '{{this.name}}')} = :{{this.name~}} {{/each}}`, { + blockHash, + contractAddress + {{~#each query.params}}, + {{this.name}} + {{~/each}} + + }) + .getOne(); + } + + {{/each}} + + {{~#each queries as | query |}} + async save{{capitalize query.name tillIndex=1}} ({ blockHash, contractAddress + {{~#each query.params}}, {{this.name~}} {{/each}}, value, proof}: DeepPartial<{{capitalize query.name tillIndex=1}}>): Promise<{{capitalize query.name tillIndex=1}}> { + const repo = this._conn.getRepository({{capitalize query.name tillIndex=1}}); + const entity = repo.create({ blockHash, contractAddress + {{~#each query.params}}, {{this.name~}} {{/each}}, value, proof }); + return repo.save(entity); + } + + {{/each}} + _getColumn (entityName: string, propertyName: string) { + return this._conn.getMetadata(entityName).findColumnWithPropertyName(propertyName)?.databaseName + } +} diff --git a/packages/codegen/src/templates/entity-template.handlebars b/packages/codegen/src/templates/entity-template.handlebars new file mode 100644 index 00000000..f57d0751 --- /dev/null +++ b/packages/codegen/src/templates/entity-template.handlebars @@ -0,0 +1,30 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm'; + +@Entity() +{{#if indexOn.columns}} +@Index(['blockHash', 'contractAddress' +{{~#each indexOn.columns}}, '{{this}}' +{{~/each}}], { unique: {{indexOn.unique}} }) +{{/if}} +export class {{className}} { + @PrimaryGeneratedColumn() + id!: number; + + @Column('varchar', { length: 66 }) + blockHash!: string; + + @Column('varchar', { length: 42 }) + contractAddress!: string; + + {{#each columns as | column |}} + @Column('{{column.pgType}}' {{~#if column.length}}, { length: {{column.length}} } {{~/if}}) + {{column.name}}!: {{column.tsType}}; + + {{/each}} + @Column('text', { nullable: true }) + proof!: string; +} diff --git a/packages/codegen/src/templates/indexer-template.handlebars b/packages/codegen/src/templates/indexer-template.handlebars new file mode 100644 index 00000000..70c801e6 --- /dev/null +++ b/packages/codegen/src/templates/indexer-template.handlebars @@ -0,0 +1,98 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import debug from 'debug'; +import { JsonFragment } from '@ethersproject/abi'; +import JSONbig from 'json-bigint'; +import { BigNumber, ethers, Contract } from 'ethers'; +import { BaseProvider } from '@ethersproject/providers'; + +import { EthClient } from '@vulcanize/ipld-eth-client'; +import { StorageLayout } from '@vulcanize/solidity-mapper'; +import { ValueResult } from '@vulcanize/util'; + +import { Database } from './database'; +import artifacts from './artifacts/{{inputFileName}}.json'; + +const log = debug('vulcanize:indexer'); + +export class Indexer { + _db: Database + _ethClient: EthClient + _ethProvider: BaseProvider + + _abi: JsonFragment[] + _storageLayout: StorageLayout + _contract: ethers.utils.Interface + _serverMode: string + + constructor (db: Database, ethClient: EthClient, ethProvider: BaseProvider, serverMode: string) { + assert(db); + assert(ethClient); + + this._db = db; + this._ethClient = ethClient; + this._ethProvider = ethProvider; + this._serverMode = serverMode; + + const { abi, storageLayout } = artifacts; + + assert(abi); + assert(storageLayout); + + this._abi = abi; + this._storageLayout = storageLayout; + + this._contract = new ethers.utils.Interface(this._abi); + } + + {{#each queries as | query |}} + async {{query.name}} (blockHash: string, contractAddress: string + {{~#each query.params}}, {{this.name~}}: {{this.type~}} {{/each}}): Promise { + const entity = await this._db.get{{capitalize query.name tillIndex=1}}({ blockHash, contractAddress + {{~#each query.params}}, {{this.name~}} {{~/each}} }); + if (entity) { + log('{{query.name}}: db hit.'); + + return { + value: entity.value, + proof: JSON.parse(entity.proof) + }; + } + + log('{{query.name}}: db miss, fetching from upstream server'); + + const contract = new Contract(contractAddress, this._abi, this._ethProvider); + let value = null; + + {{~#if query.params}} + + const { block: { number } } = await this._ethClient.getBlockByHash(blockHash); + const blockNumber = BigNumber.from(number).toNumber(); + value = await contract.{{query.name}}( + {{~#each query.params}}{{this.name}}, {{/each}}{ blockTag: blockNumber }); + {{else}} + + value = await contract.{{query.name}}(); + {{/if}} + + {{~#if (compare query.returnType 'bigint')}} + + value = value.toString(); + value = BigInt(value); + {{/if}} + + const result: ValueResult = { value }; + const { proof } = result; + await this._db.save{{capitalize query.name tillIndex=1}}({ blockHash, contractAddress + {{~#each query.params}}, {{this.name~}} {{/each}}, value, proof: JSONbig.stringify(proof) }); + + return result; + } + {{#unless @last}} + + {{/unless}} + {{/each}} +} diff --git a/packages/codegen/src/templates/package-template.handlebars b/packages/codegen/src/templates/package-template.handlebars new file mode 100644 index 00000000..42cb817c --- /dev/null +++ b/packages/codegen/src/templates/package-template.handlebars @@ -0,0 +1,44 @@ +{ + "name": "@vulcanize/{{folderName}}", + "version": "0.1.0", + "description": "{{folderName}}", + "private": true, + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "server": "DEBUG=vulcanize:* ts-node src/server.ts" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vulcanize/watcher-ts.git" + }, + "author": "", + "license": "AGPL-3.0", + "bugs": { + "url": "https://github.com/vulcanize/watcher-ts/issues" + }, + "homepage": "https://github.com/vulcanize/watcher-ts#readme", + "dependencies": { + "@apollo/client": "^3.3.19", + "@vulcanize/cache": "^0.1.0", + "@vulcanize/ipld-eth-client": "^0.1.0", + "@vulcanize/solidity-mapper": "^0.1.0", + "@vulcanize/util": "^0.1.0", + "apollo-server-express": "^2.25.0", + "apollo-type-bigint": "^0.1.3", + "debug": "^4.3.1", + "ethers": "^5.2.0", + "express": "^4.17.1", + "graphql": "^15.5.0", + "graphql-import-node": "^0.0.4", + "reflect-metadata": "^0.1.13", + "yargs": "^17.0.1" + }, + "devDependencies": { + "@ethersproject/abi": "^5.3.0", + "@types/express": "^4.17.11", + "@types/yargs": "^17.0.0", + "ts-node": "^10.0.0", + "typescript": "^4.3.2" + } +} diff --git a/packages/codegen/src/templates/readme-template.handlebars b/packages/codegen/src/templates/readme-template.handlebars new file mode 100644 index 00000000..d8ba4722 --- /dev/null +++ b/packages/codegen/src/templates/readme-template.handlebars @@ -0,0 +1,51 @@ +# {{contractName}} Watcher + +## Setup + +* Run the following command to install required packages: + + ```bash + yarn + ``` + +* Create a postgres12 database for the watcher: + + ```bash + sudo su - postgres + createdb {{folderName}} + ``` + +* Update `environments/local.toml` with database connection settings. + +* Update the `upstream` config in `environments/local.toml` and provide the `ipld-eth-server` GQL API and the `indexer-db` postgraphile endpoints. + +## Run + +* Run the watcher: + + ```bash + yarn server + ``` + +GQL console: http://localhost:3008/graphql + +## Demo + +* Install required packages: + + ```bash + yarn + ``` + +* Create the database: + + ```bash + sudo su - postgres + createdb {{folderName}} + ``` + +* Run the watcher: + + ```bash + yarn server + ``` diff --git a/packages/codegen/src/templates/resolvers-template.handlebars b/packages/codegen/src/templates/resolvers-template.handlebars new file mode 100644 index 00000000..2bb65c43 --- /dev/null +++ b/packages/codegen/src/templates/resolvers-template.handlebars @@ -0,0 +1,46 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; +import BigInt from 'apollo-type-bigint'; +import debug from 'debug'; + +import { ValueResult } from '@vulcanize/util'; + +import { Indexer } from './indexer'; + +const log = debug('vulcanize:resolver'); + +export const createResolvers = async (indexer: Indexer): Promise => { + assert(indexer); + + return { + BigInt: new BigInt('bigInt'), + + Event: { + __resolveType: (obj: any) => { + assert(obj.__typename); + + return obj.__typename; + } + }, + + Query: { + {{#each queries}} + {{this.name}}: (_: any, { blockHash, contractAddress + {{~#each this.params}}, {{this.name~}} {{/each}} }: { blockHash: string, contractAddress: string + {{~#each this.params}}, {{this.name}}: {{this.type~}} {{/each}} }): Promise => { + log('{{this.name}}', blockHash, contractAddress + {{~#each this.params}}, {{this.name~}} {{/each}}); + return indexer.{{this.name}}(blockHash, contractAddress + {{~#each this.params}}, {{this.name~}} {{/each}}); + } + {{~#unless @last}}, + + {{/unless}} + {{/each}} + + } + }; +}; diff --git a/packages/codegen/src/templates/server-template.handlebars b/packages/codegen/src/templates/server-template.handlebars new file mode 100644 index 00000000..04748d42 --- /dev/null +++ b/packages/codegen/src/templates/server-template.handlebars @@ -0,0 +1,97 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import fs from 'fs'; +import path from 'path'; +import assert from 'assert'; +import 'reflect-metadata'; +import express, { Application } from 'express'; +import { ApolloServer, PubSub } from 'apollo-server-express'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import debug from 'debug'; +import 'graphql-import-node'; +import { createServer } from 'http'; +import { getDefaultProvider } from 'ethers'; + +import { getCache } from '@vulcanize/cache'; +import { EthClient } from '@vulcanize/ipld-eth-client'; +import { DEFAULT_CONFIG_PATH, getConfig } from '@vulcanize/util'; + +import { createResolvers } from './resolvers'; +import { Indexer } from './indexer'; +import { Database } from './database'; + +const log = debug('vulcanize:server'); + +export const main = async (): Promise => { + const argv = await yargs(hideBin(process.argv)) + .option('f', { + alias: 'config-file', + demandOption: true, + describe: 'configuration file path (toml)', + type: 'string', + default: DEFAULT_CONFIG_PATH + }) + .argv; + + const config = await getConfig(argv.f); + + assert(config.server, 'Missing server config'); + + const { host, port, mode } = config.server; + + const { upstream, database: dbConfig } = config; + + assert(dbConfig, 'Missing database config'); + + const db = new Database(dbConfig); + await db.init(); + + assert(upstream, 'Missing upstream config'); + const { ethServer: { gqlApiEndpoint, gqlPostgraphileEndpoint, rpcProviderEndpoint }, cache: cacheConfig } = upstream; + assert(gqlApiEndpoint, 'Missing upstream ethServer.gqlApiEndpoint'); + assert(gqlPostgraphileEndpoint, 'Missing upstream ethServer.gqlPostgraphileEndpoint'); + + const cache = await getCache(cacheConfig); + const ethClient = new EthClient({ + gqlEndpoint: gqlApiEndpoint, + gqlSubscriptionEndpoint: gqlPostgraphileEndpoint, + cache + }); + + const ethProvider = getDefaultProvider(rpcProviderEndpoint); + + // Note: In-memory pubsub works fine for now, as each watcher is a single process anyway. + // Later: https://www.apollographql.com/docs/apollo-server/data/subscriptions/#production-pubsub-libraries + const pubsub = new PubSub(); + const indexer = new Indexer(db, ethClient, ethProvider, mode); + + const resolvers = await createResolvers(indexer); + + const app: Application = express(); + const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.gql')).toString(); + const server = new ApolloServer({ + typeDefs, + resolvers + }); + + await server.start(); + server.applyMiddleware({ app }); + + const httpServer = createServer(app); + server.installSubscriptionHandlers(httpServer); + + httpServer.listen(port, host, () => { + log(`Server is listening on host ${host} port ${port}`); + }); + + return { app, server }; +}; + +main().then(() => { + log('Starting server...'); +}).catch(err => { + log(err); +}); diff --git a/packages/codegen/src/templates/tsconfig-template.handlebars b/packages/codegen/src/templates/tsconfig-template.handlebars new file mode 100644 index 00000000..99712bdf --- /dev/null +++ b/packages/codegen/src/templates/tsconfig-template.handlebars @@ -0,0 +1,74 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "dist", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "resolveJsonModule": true /* Enabling the option allows importing JSON, and validating the types in that JSON file. */ + }, + "include": ["src/**/*"] +} diff --git a/packages/codegen/src/tsconfig.ts b/packages/codegen/src/tsconfig.ts new file mode 100644 index 00000000..6a746934 --- /dev/null +++ b/packages/codegen/src/tsconfig.ts @@ -0,0 +1,21 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import fs from 'fs'; +import path from 'path'; +import Handlebars from 'handlebars'; +import { Writable } from 'stream'; + +const TEMPLATE_FILE = './templates/tsconfig-template.handlebars'; + +/** + * Writes the tsconfig.json file generated from a template to a stream. + * @param outStream A writable output stream to write the tsconfig.json file to. + */ +export function exportTSConfig (outStream: Writable): void { + const templateString = fs.readFileSync(path.resolve(__dirname, TEMPLATE_FILE)).toString(); + const template = Handlebars.compile(templateString); + const tsconfig = template({}); + outStream.write(tsconfig); +} diff --git a/packages/codegen/src/types/common/main.d.ts b/packages/codegen/src/types/common/main.d.ts index b894dc88..deba863f 100644 --- a/packages/codegen/src/types/common/main.d.ts +++ b/packages/codegen/src/types/common/main.d.ts @@ -3,3 +3,4 @@ // declare module '@poanet/solidity-flattener'; +declare module 'solc'; diff --git a/packages/codegen/src/types/common/package.json b/packages/codegen/src/types/common/package.json index 37718261..5861d0f0 100644 --- a/packages/codegen/src/types/common/package.json +++ b/packages/codegen/src/types/common/package.json @@ -1,6 +1,6 @@ { - "name": "common", - "version": "0.1.0", - "license": "AGPL-3.0", - "typings": "main.d.ts" + "name": "common", + "version": "0.1.0", + "license": "AGPL-3.0", + "typings": "main.d.ts" } diff --git a/packages/codegen/src/utils/handlebar-helpers.ts b/packages/codegen/src/utils/handlebar-helpers.ts new file mode 100644 index 00000000..3dc08a87 --- /dev/null +++ b/packages/codegen/src/utils/handlebar-helpers.ts @@ -0,0 +1,46 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +import assert from 'assert'; + +/** + * Helper function to compare two values using the given operator. + * @param lvalue Left hand side value. + * @param rvalue Right hasd side value. + * @param options Handlebars options parameter. `options.hash.operator`: operator to be used for comparison. + * @returns Result of the comparison. + */ +export function compareHelper (lvalue: string, rvalue: string, options: any): boolean { + assert(lvalue && rvalue, "Handlerbars Helper 'compare' needs at least 2 parameters"); + + const operator = options.hash.operator || '==='; + + const operators: Map boolean> = new Map(); + + operators.set('===', function (l: any, r: any) { return l === r; }); + operators.set('!==', function (l: any, r: any) { return l !== r; }); + operators.set('<', function (l: any, r: any) { return l < r; }); + operators.set('>', function (l: any, r: any) { return l > r; }); + operators.set('<=', function (l: any, r: any) { return l <= r; }); + operators.set('>=', function (l: any, r: any) { return l >= r; }); + + const operatorFunction = operators.get(operator); + assert(operatorFunction, "Handlerbars Helper 'compare' doesn't know the operator " + operator); + const result = operatorFunction(lvalue, rvalue); + + return result; +} + +/** + * Helper function that capitalized string till given index. + * @param value String of which content is to be capitalized. + * @param options Handlebars options parameter. `options.hash.tillIndex`: index till which to capitalize the string. + * @returns The modified string. + */ +export function capitalizeHelper (value: string, options: any): string { + const tillIndex = options.hash.tillIndex || value.length; + const result = `${value.slice(0, tillIndex).toUpperCase()}${value.slice(tillIndex, value.length)}`; + + return result; +} diff --git a/packages/codegen/src/utils/type-mappings.ts b/packages/codegen/src/utils/type-mappings.ts new file mode 100644 index 00000000..5488485f --- /dev/null +++ b/packages/codegen/src/utils/type-mappings.ts @@ -0,0 +1,42 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +const _solToTs: Map = new Map(); +const _tsToGql: Map = new Map(); +const _tsToPg: Map = new Map(); + +// TODO Get typemapping from ethersjs. +// Solidity to Typescript type-mapping. +_solToTs.set('string', 'string'); +_solToTs.set('uint8', 'number'); +_solToTs.set('uint256', 'bigint'); +_solToTs.set('address', 'string'); +_solToTs.set('bool', 'boolean'); +_solToTs.set('bytes4', 'string'); + +// Typescript to Graphql type-mapping. +_tsToGql.set('string', 'String'); +_tsToGql.set('number', 'Int'); +_tsToGql.set('bigint', 'BigInt'); +_tsToGql.set('boolean', 'Boolean'); + +// Typescript to Postgres type-mapping. +_tsToPg.set('string', 'varchar'); +_tsToPg.set('number', 'numeric'); +_tsToPg.set('bigint', 'numeric'); +_tsToPg.set('boolean', 'boolean'); + +function getTsForSol (solType: string): string | undefined { + return _solToTs.get(solType); +} + +function getGqlForTs (tsType: string): string | undefined { + return _tsToGql.get(tsType); +} + +function getPgForTs (tsType: string): string | undefined { + return _tsToPg.get(tsType); +} + +export { getTsForSol, getGqlForTs, getPgForTs }; diff --git a/packages/codegen/src/utils/types.ts b/packages/codegen/src/utils/types.ts new file mode 100644 index 00000000..2a99ad6b --- /dev/null +++ b/packages/codegen/src/utils/types.ts @@ -0,0 +1,8 @@ +// +// Copyright 2021 Vulcanize, Inc. +// + +export interface Param { + name: string; + type: string; +} diff --git a/packages/codegen/src/visitor.ts b/packages/codegen/src/visitor.ts index f51183bb..ba089737 100644 --- a/packages/codegen/src/visitor.ts +++ b/packages/codegen/src/visitor.ts @@ -3,13 +3,27 @@ // import { Writable } from 'stream'; -import { Schema, Param } from './schema'; + +import { Database } from './database'; +import { Entity } from './entity'; +import { Indexer } from './indexer'; +import { Resolvers } from './resolvers'; +import { Schema } from './schema'; +import { Param } from './utils/types'; export class Visitor { _schema: Schema; + _resolvers: Resolvers; + _indexer: Indexer; + _entity: Entity; + _database: Database; constructor () { this._schema = new Schema(); + this._resolvers = new Resolvers(); + this._indexer = new Indexer(); + this._entity = new Entity(); + this._database = new Database(); } /** @@ -27,6 +41,10 @@ export class Visitor { const returnType = node.returnParameters[0].typeName.name; this._schema.addQuery(name, params, returnType); + this._resolvers.addQuery(name, params, returnType); + this._indexer.addQuery(name, params, returnType); + this._entity.addQuery(name, params, returnType); + this._database.addQuery(name, params, returnType); } } @@ -56,6 +74,10 @@ export class Visitor { const returnType = typeName.name; this._schema.addQuery(name, params, returnType); + this._resolvers.addQuery(name, params, returnType); + this._indexer.addQuery(name, params, returnType); + this._entity.addQuery(name, params, returnType); + this._database.addQuery(name, params, returnType); } /** @@ -78,4 +100,37 @@ export class Visitor { exportSchema (outStream: Writable): void { this._schema.exportSchema(outStream); } + + /** + * Writes the resolvers file generated from a template to a stream. + * @param outStream A writable output stream to write the resolvers file to. + */ + exportResolvers (outStream: Writable): void { + this._resolvers.exportResolvers(outStream); + } + + /** + * Writes the indexer file generated from a template to a stream. + * @param outStream A writable output stream to write the indexer file to. + * @param inputFileName Input contract file name to be passed to the template. + */ + exportIndexer (outStream: Writable, inputFileName: string): void { + this._indexer.exportIndexer(outStream, inputFileName); + } + + /** + * Writes the generated entity files in the given directory. + * @param entityDir Directory to write the entities to. + */ + exportEntities (entityDir: string): void { + this._entity.exportEntities(entityDir); + } + + /** + * Writes the database file generated from a template to a stream. + * @param outStream A writable output stream to write the database file to. + */ + exportDatabase (outStream: Writable): void { + this._database.exportDatabase(outStream); + } } diff --git a/packages/codegen/tsconfig.json b/packages/codegen/tsconfig.json index cc12c6f3..111448d7 100644 --- a/packages/codegen/tsconfig.json +++ b/packages/codegen/tsconfig.json @@ -44,12 +44,13 @@ // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ /* Module Resolution Options */ - // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ "typeRoots": [ - "./src/types" + "./src/types", + "node_modules/@types" ], /* List of folders to include type definitions from. */ // "types": [], /* Type declaration files to be included in compilation. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ @@ -69,7 +70,8 @@ /* Advanced Options */ "skipLibCheck": true, /* Skip type checking of declaration files. */ - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "resolveJsonModule": true /* Enabling the option allows importing JSON, and validating the types in that JSON file. */ }, "include": ["src/**/*"] } diff --git a/yarn.lock b/yarn.lock index b2b2b051..da8adca3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1991,7 +1991,7 @@ "@poanet/solidity-flattener@https://github.com/vulcanize/solidity-flattener.git": version "3.0.6" - resolved "https://github.com/vulcanize/solidity-flattener.git#397f7295acff78eb039e479a85e8c3a9d78a96dd" + resolved "https://github.com/vulcanize/solidity-flattener.git#9c8a22b9c43515be306646a177a43636fd95aaae" dependencies: bunyan "^1.8.12" decomment "^0.9.1" @@ -7578,7 +7578,7 @@ growl@1.10.5: resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== -handlebars@^4.7.6: +handlebars@^4.7.6, handlebars@^4.7.7: version "4.7.7" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1" integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== @@ -12421,6 +12421,21 @@ solc@^0.6.3: semver "^5.5.0" tmp "0.0.33" +solc@^0.8.7-fixed: + version "0.8.7-fixed" + resolved "https://registry.yarnpkg.com/solc/-/solc-0.8.7-fixed.tgz#76eb37d33637ad278ad858e2633e9da597d877ac" + integrity sha512-nWZRkdPwfBpimAelO30Bz7/hxoj+mylb30gEpBL8hhEWR4xqu2ezQAxWK1Hz5xx1NqesbgGjSgnGul49tRHWgQ== + dependencies: + command-exists "^1.2.8" + commander "3.0.2" + follow-redirects "^1.12.1" + fs-extra "^0.30.0" + js-sha3 "0.8.0" + memorystream "^0.3.1" + require-from-string "^2.0.0" + semver "^5.5.0" + tmp "0.0.33" + sort-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128"