├── .gitignore ├── .npmignore ├── README.md ├── package.json ├── src ├── cli.ts ├── code-gen │ ├── sol-gen.ts │ └── ts-gen.ts ├── index.ts ├── lib │ ├── helpers.ts │ └── types.ts └── parser │ ├── index.ts │ ├── parser.ts │ └── types.ts ├── test ├── test-files │ ├── TestInput.sol │ ├── TestOutput.sol │ ├── TestOutputVerbose.sol │ └── test-output.json └── test.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | package-lock.json -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | node_modules/ 4 | tsconfig.json 5 | .gitignore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # abi-codegen 2 | Typescript library for generating solidity and typescript code to handle tightly packed ABI structures. 3 | 4 | # Install 5 | > npm install -g abi-gen 6 | 7 | # Command Line 8 | ## Solidity 9 | > abi-gen sol --input --output [flags] 10 | 11 | ### Flags 12 | **--verbose** 13 | Tells the code generator to explicitly define variables and construct the output rather than assigning directly to memory. 14 | 15 | ## TypeScript 16 | > abi-gen ts -i -o 17 | 18 | 19 | # Examples 20 | **Input Struct** 21 | ``` 22 | enum ABC { a, b } 23 | struct TestWrapped { 24 | uint32 a; 25 | bytes32 b; 26 | bytes32 c; 27 | uint8 d; 28 | ABC e; 29 | } 30 | ``` 31 | 32 | Save the above in `./input`. 33 | 34 | ## Solidity 35 | > abi-gen sol -i ./input -o ./output.sol 36 | 37 | *I still need to add handling to use the right library name instead of making one up.* 38 | 39 | **Output** 40 | 41 | ```cs 42 | pragma solidity ^0.6.0; 43 | 44 | library OutputCode { 45 | enum ABC { a, b } 46 | 47 | struct TestWrapped { 48 | uint32 a; 49 | bytes32 b; 50 | bytes32 c; 51 | uint8 d; 52 | ABC e; 53 | } 54 | 55 | function unpackTestWrapped(bytes memory input) 56 | internal pure returns (TestWrapped memory ret) { 57 | assembly { 58 | let ptr := add(input, 32) 59 | mstore(ret, shr(224, mload(ptr))) 60 | mstore(add(ret, 32), mload(add(ptr, 4))) 61 | mstore(add(ret, 64), mload(add(ptr, 36))) 62 | mstore(add(ret, 96), shr(248, mload(add(ptr, 68)))) 63 | mstore(add(ret, 128), shr(248, mload(add(ptr, 69)))) 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | ## TypeScript 70 | > abi-gen ts -i ./input -o ./output-dir 71 | 72 | A number of files will be created, but the main output will be: 73 | 74 | ```ts 75 | import {defineProperties} from 'ts-abi-utils'; 76 | import { ABC } from './ABC'; 77 | const TestWrappedABI = require('./TestWrappedABI.json'); 78 | 79 | export interface TestWrappedData { 80 | a: number; 81 | b: string; 82 | c: string; 83 | d: number; 84 | e: ABC; 85 | } 86 | 87 | export interface TestWrapped extends TestWrappedData { 88 | /* Encode as ABI. */ 89 | toAbi: () => Buffer; 90 | /* Encode as packed ABI - all fields will have minimal length for their type. */ 91 | toAbiPacked: () => Buffer; 92 | /* Encode as JSON object. */ 93 | toJson: () => any; 94 | } 95 | 96 | export class TestWrapped { 97 | constructor(input: TestWrappedData) { Object.assign(this, input); } 98 | /* Decode a TestWrapped from an ABI string or buffer. */ 99 | static fromAbi: (input: string | Buffer) => TestWrapped; 100 | /* Decode a TestWrapped from a packed ABI string or buffer */ 101 | static fromAbiPacked: (input: string | Buffer) => TestWrapped; 102 | /* Decode a TestWrapped from an arbitrary object with BufferLike fields of the same names (works for JSON). */ 103 | static fromObject: (input: any) => TestWrapped; 104 | } 105 | defineProperties(TestWrapped, TestWrappedABI); 106 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abi-gen", 3 | "version": "1.0.2", 4 | "description": "", 5 | "main": "index.js", 6 | "bin": "dist/cli.js", 7 | "scripts": { 8 | "mocha:ts": "mocha -r ts-node/register", 9 | "test": "yarn mocha:ts ./test/test.ts", 10 | "build": "tsc" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "homepage": "https://github.com/d1ll0n/abi-codegen", 16 | "bugs": { 17 | "url": "https://github.com/d1ll0n/abi-codegen/issues" 18 | }, 19 | "dependencies": { 20 | "solidity-parser-antlr": "^0.4.11", 21 | "ts-abi-utils": "^1.0.0", 22 | "yargs": "^15.3.1" 23 | }, 24 | "devDependencies": { 25 | "@types/chai": "^4.2.11", 26 | "@types/mocha": "^7.0.2", 27 | "@types/node": "^13.13.2", 28 | "chai": "^4.2.0", 29 | "mocha": "^7.1.1", 30 | "ts-node": "^8.9.0", 31 | "typescript": "^3.8.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { parseCode } from './parser/index'; 5 | import { UnpackerGen } from './code-gen/sol-gen'; 6 | import { buildLibrary } from './code-gen/ts-gen'; 7 | import { AbiStruct, AbiEnum } from './lib/types'; 8 | 9 | const argv = require('yargs') 10 | .usage('Usage: [options]') 11 | .command({ 12 | command: 'sol', 13 | describe: 'generate solidity library', 14 | builder: { 15 | input: { 16 | alias: ['i'], 17 | describe: 'input file or directory to read solidity structs from', 18 | demandOption: true, 19 | coerce: path.resolve 20 | }, 21 | output: { 22 | alias: ['o', 'outDir'], 23 | describe: 'file or directory to write generated code to', 24 | demandOption: true, 25 | coerce: path.resolve 26 | }, 27 | verbose: { 28 | alias: 'v', 29 | describe: 'code verbosity', 30 | default: false, 31 | type: 'boolean' 32 | } 33 | } 34 | }) 35 | .command({ 36 | command: 'ts', 37 | describe: 'generate typescript library', 38 | builder: { 39 | input: { 40 | alias: ['i'], 41 | describe: 'input file to read structs from', 42 | demandOption: true, 43 | coerce: path.resolve 44 | }, 45 | output: { 46 | alias: 'o', 47 | describe: 'directory to write generated code to', 48 | demandOption: true, 49 | coerce: path.resolve 50 | }, 51 | verbose: { 52 | alias: 'v', 53 | describe: 'code verbosity', 54 | default: false, 55 | type: 'boolean' 56 | } 57 | } 58 | }) 59 | .help('h') 60 | .fail(function(msg, err) { 61 | console.error(msg); 62 | process.exit(1); 63 | }) 64 | .argv; 65 | 66 | if (!fs.existsSync(argv.input)) throw new Error(`File not found: ${argv.input}`); 67 | const code = fs.readFileSync(argv.input, 'utf8'); 68 | const structs = > parseCode(code); 69 | 70 | if (argv._.includes('sol')) { 71 | const lib = UnpackerGen.createLibrary('OutputCode', structs, { verbose: argv.verbose }); 72 | fs.writeFileSync(argv.output, lib); 73 | } 74 | 75 | if (argv._.includes('ts')) { 76 | const dir = argv.output; 77 | if (!fs.existsSync(dir)) fs.mkdirSync(dir); 78 | const files = buildLibrary(> structs); 79 | const index = []; 80 | files.forEach(({ code, fileName, jsonFile, jsonFileName }) => { 81 | fs.writeFileSync(path.join(dir, fileName), code); 82 | fs.writeFileSync(path.join(dir, jsonFileName), jsonFile); 83 | index.push(`export * from './${fileName}';`); 84 | }); 85 | fs.writeFileSync(path.join(dir, 'index.ts'), index.join('\n').replace(/\.ts/g, '')); 86 | } -------------------------------------------------------------------------------- /src/code-gen/sol-gen.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbiStruct, AbiStructField, AbiElementaryType, AbiEnum, AbiType, AbiArray, ArrayJoinInput 3 | } from '../lib/types'; 4 | 5 | import { isStructAllowed, getFieldConstructor, toTypeName, abiStructToSol, arrJoiner } from '../lib/helpers'; 6 | 7 | export type SolGenOptions = { 8 | /* Used to determine whether vars are explicitly defined or simply assigned directly to memory. */ 9 | verbose?: boolean 10 | } 11 | 12 | export class UnpackerGen { 13 | _inputOffset: number = 0; 14 | outputOffset: number = 0; 15 | varDefs: string[] = []; 16 | codeChunks: ArrayJoinInput[] = []; 17 | asmLines: string[] = [ 18 | `let ptr := add(input, 32)`, 19 | ]; 20 | outputDef: string; 21 | struct: AbiStruct; 22 | options?: SolGenOptions; 23 | 24 | get inputOffset(): number { return this._inputOffset / 8; } 25 | get ptr(): string { return this.inputOffset > 0 ? `add(ptr, ${this.inputOffset})` : `ptr`; } 26 | get outPtr(): string { return this.outputOffset > 0 ? `add(ret, ${this.outputOffset})` : `ret`; } 27 | get mload(): string { return `mload(${this.ptr})`; } 28 | 29 | static createLibrary(libraryName: string, structs: Array, opts?: SolGenOptions): string { 30 | const arr = []; 31 | for (let struct of structs) arr.push( 32 | struct.meta == 'enum' 33 | ? abiStructToSol(struct) 34 | : new UnpackerGen(struct, opts).toUnpack(true) 35 | ) 36 | let lib = arr.map((x, i) => ([...x, (i != arr.length - 1 && '')])); 37 | return arrJoiner([ 38 | `pragma solidity ^0.6.0;`, 39 | '', 40 | `library ${libraryName} {`, 41 | ...lib, 42 | `}` 43 | ]) 44 | } 45 | 46 | constructor(struct: AbiStruct, opts: SolGenOptions = { verbose: true }) { 47 | this.options = opts; 48 | if (!isStructAllowed(struct)) { 49 | console.log(struct.name) 50 | throw new Error(`Dynamic structs must only contain dynamic fields as the last value.`) 51 | } 52 | this.struct = struct; 53 | for (let field of struct.fields) this.addUnpackField(field); 54 | } 55 | 56 | putAsm = (line: string) => this.asmLines.push(line); 57 | putCode = (line: string) => this.codeChunks.push(line); 58 | putIndentedCode = (line: string) => this.codeChunks.push([ line ]); 59 | nextCodeChunk = () => { 60 | let arr = [...this.asmLines]; 61 | this.codeChunks.push(...[`assembly {`, arr, `}`]); 62 | this.asmLines = [`let ptr := add(input, 32)`]; 63 | }; 64 | 65 | toUnpack(returnArray?: boolean): ArrayJoinInput { 66 | const { name } = this.struct; 67 | this.nextCodeChunk(); 68 | let retVar: string, retLine: string | null; 69 | if (!this.options.verbose && !this.struct.dynamic) { 70 | retVar = `${name} memory ret`; 71 | retLine = null; 72 | } else { 73 | retVar = `${name} memory`; 74 | retLine = `return ${getFieldConstructor(this.struct)};`; 75 | } 76 | const arr = [ 77 | ...abiStructToSol(this.struct), 78 | '', 79 | `function unpack${name}(bytes memory input)`, 80 | `internal pure returns (${retVar}) {`, 81 | this.varDefs, 82 | this.codeChunks, 83 | [retLine], 84 | '}' 85 | ] 86 | return returnArray ? arr : arrJoiner(arr); 87 | } 88 | 89 | getShiftedSizeReader(size: number): string { 90 | if (size == 256) return `mload(${this.ptr})`; 91 | if (size > 256) throw new Error('Size over 256 bits not supported.'); 92 | if (size % 8 != 0) throw new Error('Sizes not divisible into bytes not supported.') 93 | let shift = 256 - size; 94 | return `shr(${shift}, ${this.mload})` 95 | } 96 | 97 | putAsmReadCode(def: AbiElementaryType | AbiEnum, name: string) { 98 | let _reader = this.getShiftedSizeReader(def.size); 99 | if (!this.options.verbose && !this.struct.dynamic) { 100 | this.putAsm(`mstore(${this.outPtr}, ${_reader})`) 101 | this.outputOffset += 32; 102 | } else { 103 | this.putAsm(`${name} := ${_reader}`); 104 | this.varDefs.push(`${toTypeName(def)} ${name};`); 105 | } 106 | this._inputOffset += def.size; 107 | } 108 | 109 | putArrayReadCode(def: AbiArray, name: string) { 110 | const size = def.baseType.size; 111 | if (this.options.verbose || this.struct.dynamic) { 112 | this.varDefs.push(`${toTypeName(def)} memory ${name};`); 113 | } 114 | if (def.length && size) { 115 | for (let i = 0; i < def.length; i++) { 116 | let ptr: string; 117 | if (!this.options.verbose && !this.struct.dynamic) { 118 | ptr = this.outPtr; 119 | this.outputOffset += 32; 120 | } else { 121 | ptr = i == 0 ? name : `add(${name}, ${32 * i})`; 122 | } 123 | this.putAsm(`mstore(${ptr}, ${this.getShiftedSizeReader(size)})`) 124 | this._inputOffset += size; 125 | } 126 | } 127 | else throw new Error(`Arrays without a fixed type size and length are not supported yet.`); 128 | } 129 | 130 | addUnpackField(field: AbiStructField, scopeName?: string) { 131 | const name = `${scopeName ? scopeName + '_' : ''}${field.name}`; 132 | switch(field.type.meta) { 133 | case 'array': 134 | this.putArrayReadCode(field.type, name); 135 | break; 136 | case 'struct': 137 | for (let f of field.type.fields) this.addUnpackField(f, name); 138 | break; 139 | case 'enum': 140 | case 'elementary': 141 | this.putAsmReadCode(field.type, name); 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /src/code-gen/ts-gen.ts: -------------------------------------------------------------------------------- 1 | import { ArrayJoinInput, AbiStruct, AbiEnum, AbiType, AbiStructField, AbiArray } from "../lib/types" 2 | import { arrJoiner } from "../lib/helpers"; 3 | 4 | export function hasNestedStruct(abi: AbiArray): boolean { 5 | if (abi.baseType.meta == 'struct') return true; 6 | if (abi.baseType.meta == 'array') return hasNestedStruct(abi.baseType); 7 | } 8 | 9 | export function nestedStructFields(abi: AbiStruct): AbiStructField[] { 10 | return abi.fields.filter(({ type }) => 11 | type.meta == 'struct' || 12 | type.meta == 'array' && hasNestedStruct(type) 13 | ); 14 | } 15 | 16 | export function getStructConstructor(abi: AbiStruct | AbiArray, name: string): string { 17 | if (abi.meta == 'array') { 18 | return `${name}.map(f => ${getStructConstructor(( abi.baseType), 'f')})`; 19 | } 20 | if (abi.meta == 'struct') { 21 | return `new ${abi.name}(${name})`; 22 | } 23 | } 24 | 25 | export const toInterfaceType = (def: AbiType, input?: boolean): string => { 26 | if (def.meta == 'elementary') { 27 | switch(def.type) { 28 | case 'uint': return `${def.size > 53 ? 'BN' : 'number'}`; 29 | case 'bool': return `boolean`; 30 | case 'byte': return `string`; 31 | case 'bytes': return `string`; 32 | case 'address': return 'string'; 33 | } 34 | } 35 | if (def.meta == 'array') { 36 | return `${toInterfaceType(def.baseType, input)}[]` 37 | } 38 | if (def.meta == 'enum') return def.name; 39 | return def.name + (input ? 'Data' : ''); 40 | } 41 | 42 | export type TypeDependency = { 43 | isEnum?: boolean; 44 | name: string; 45 | } 46 | 47 | class TypeScriptBuilder { 48 | typeDependencies: TypeDependency[] = []; 49 | usesBN?: boolean; 50 | _inputFields?: ArrayJoinInput; 51 | _interfaceFields?: ArrayJoinInput; 52 | _structFields?: AbiStructField[]; 53 | 54 | constructor(public struct: AbiStruct | AbiEnum) {} 55 | 56 | get structFields(): AbiStructField[] { 57 | return this._structFields || (this._structFields = nestedStructFields(this.struct as AbiStruct)); 58 | } 59 | 60 | putDependency = (name: string, isEnum?: boolean) => { 61 | if (this.typeDependencies.filter(t => t.name == name).length) return; 62 | this.typeDependencies.push({ name, isEnum }); 63 | } 64 | 65 | getInterfaceType = (def: AbiType, input?: boolean): string => { 66 | if (def.meta == 'elementary') { 67 | switch(def.type) { 68 | case 'uint': 69 | const _type = `${def.size > 53 ? 'BN' : 'number'}`; 70 | if (!this.usesBN && _type == 'BN') this.usesBN = true; 71 | return _type; 72 | case 'bool': return `boolean`; 73 | case 'byte': return `string`; 74 | case 'bytes': return `string`; 75 | case 'address': return 'string'; 76 | } 77 | } 78 | if (def.meta == 'array') { 79 | return `${toInterfaceType(def.baseType, input)}[]` 80 | } 81 | if (def.meta == 'enum') { 82 | this.putDependency(def.name, true); 83 | return def.name; 84 | } 85 | this.putDependency(def.name); 86 | return def.name + (input ? 'Data' : ''); 87 | } 88 | 89 | get inputFields() { 90 | return this._inputFields || ( 91 | this._inputFields = (this.struct as AbiStruct) 92 | .fields.map(({name, type}) => `${name}: ${this.getInterfaceType(type, true)};`) 93 | ); 94 | } 95 | 96 | get interfaceFields() { 97 | return this._interfaceFields || ( 98 | this._interfaceFields = this.structFields 99 | .map(({name, type}) => `${name}: ${toInterfaceType(type)};`) 100 | ); 101 | } 102 | 103 | get typeConstructor() { 104 | if (this.structFields.length) { 105 | let extra = this.structFields.length < this.struct.fields.length; 106 | let destructer = `${this.structFields.map(f => f.name).join(', ')}${extra ? `, ...rest` : ''}` 107 | return [ 108 | `constructor(input: ${this.struct.name}Data) {`, 109 | [ 110 | `const { ${destructer} } = input;`, 111 | ...this.structFields.map(({name, type}) => 112 | `this.${name} = ${getStructConstructor(type as AbiStruct | AbiArray, name)};` 113 | ), 114 | extra ? `Object.assign(this, rest);` : undefined 115 | ], 116 | '}' 117 | ] 118 | } else return [`constructor(input: ${this.struct.name}Data) { Object.assign(this, input); }`]; 119 | } 120 | 121 | get imports() { 122 | const _def = `import { defineProperties } from 'ts-abi-utils';`; 123 | const _deps = this.typeDependencies.map(({ name, isEnum }) => 124 | `import { ${name} ${isEnum ? '' : `, ${name}Data`}} from './${name}';` 125 | ); 126 | const _bn = this.usesBN ? `import BN from 'bn.js';` : undefined; 127 | const _abi = `const ${this.struct.name}ABI = require('./${this.struct.name}ABI.json');`; 128 | return [ 129 | _def, 130 | _bn, 131 | ..._deps, 132 | _abi 133 | ]; 134 | } 135 | 136 | get dataInterfaceDeclaration() { 137 | return [ 138 | `export interface ${this.struct.name}Data {`, 139 | this.inputFields, 140 | `}` 141 | ]; 142 | } 143 | 144 | get interfaceDeclaration() { 145 | return [ 146 | `export interface ${this.struct.name} extends ${this.struct.name}Data {`, 147 | [ 148 | ...this.interfaceFields, 149 | `/* Encode as ABI. */`, 150 | `toAbi: () => Buffer;`, 151 | `/* Encode as packed ABI - all fields will have minimal length for their type. */`, 152 | `toAbiPacked: () => Buffer;`, 153 | `/* Encode as JSON object. */`, 154 | `toJson: () => any;` 155 | ], 156 | `}`, 157 | ]; 158 | } 159 | 160 | get classDeclaration() { 161 | const { name } = this.struct 162 | return [ 163 | `export class ${name} {`, 164 | [ 165 | ...this.typeConstructor, 166 | `/* Decode a ${name} from an ABI string or buffer. */`, 167 | `static fromAbi: (input: string | Buffer) => ${name};`, 168 | `/* Decode a ${name} from a packed ABI string or buffer */`, 169 | `static fromAbiPacked: (input: string | Buffer) => ${name};`, 170 | `/* Decode a ${name} from an arbitrary object with BufferLike fields of the same names (works for JSON). */`, 171 | `static fromObject: (input: any) => ${name};` 172 | ], 173 | `}`, 174 | ] 175 | } 176 | 177 | static buildLibrary = (struct: AbiStruct | AbiEnum): string => { 178 | const builder = new TypeScriptBuilder(struct); 179 | if (struct.meta == 'enum') { 180 | return arrJoiner([`export enum ${struct.name} {`, struct.fields, `}`]); 181 | } 182 | const _data = builder.dataInterfaceDeclaration; 183 | const _interface = builder.interfaceDeclaration; 184 | const _class = builder.classDeclaration; 185 | const _imports = builder.imports; 186 | const arr = [ 187 | ..._imports, 188 | '', 189 | ..._data, 190 | '', 191 | ..._interface, 192 | '', 193 | ..._class, 194 | `defineProperties(${struct.name}, ${struct.name}ABI);` 195 | ]; 196 | return arrJoiner(arr); 197 | } 198 | } 199 | 200 | export type BuilderResult = { 201 | jsonFileName: string; 202 | jsonFile: string; 203 | fileName: string; 204 | code: string; 205 | } 206 | 207 | export const buildLibrary = (defs: Array): Array => { 208 | const arr = []; 209 | for (let def of defs) { 210 | arr.push({ 211 | jsonFile: JSON.stringify(def, null, 2), 212 | jsonFileName: `${def.name}ABI.json`, 213 | fileName: `${def.name}.ts`, 214 | code: TypeScriptBuilder.buildLibrary(def) 215 | }); 216 | } 217 | return arr; 218 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parser'; 2 | export * from './code-gen/sol-gen'; -------------------------------------------------------------------------------- /src/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | import { AbiType, AbiStruct, ArrayJoinInput, AbiStructField, AbiEnum } from "./types"; 2 | 3 | export const bitsRequired = (n: number): number => { 4 | let a = Math.ceil(Math.log2(n + 1)); 5 | let m = a % 8; 6 | return (m == 0) ? a : a + 8 - m 7 | } 8 | 9 | export const elementaryToTypeDef = (typeName: string): AbiType => { 10 | const isBool = /bool/g.exec(typeName); 11 | if (isBool) return { 12 | meta: 'elementary', 13 | dynamic: false, 14 | size: 8, 15 | type: 'bool' 16 | } 17 | const isUint = /uint(\d{0,3})/g.exec(typeName); 18 | if (isUint) { 19 | const size = isUint[1]; 20 | return { 21 | meta: 'elementary', 22 | dynamic: !size, 23 | size: size ? +size : null, 24 | type: 'uint' 25 | } 26 | } 27 | const isBytes = /bytes(\d{0,2})/g.exec(typeName); 28 | if (isBytes) { 29 | const size = isBytes[1]; 30 | return { 31 | meta: 'elementary', 32 | dynamic: !size, 33 | size: size ? 8 * (+size) : null, 34 | type: 'bytes' 35 | } 36 | } 37 | const isAddress = /address/g.exec(typeName); 38 | if (isAddress) { 39 | return { 40 | meta: 'elementary', 41 | dynamic: false, 42 | size: 160, 43 | type: 'address' 44 | } 45 | } 46 | } 47 | 48 | export const toTypeName = (def: AbiType): string => { 49 | if (def.meta == 'elementary') { 50 | switch(def.type) { 51 | case 'uint': return `uint${def.size}`; 52 | case 'bool': return `bool`; 53 | case 'byte': return `byte`; 54 | case 'bytes': return `bytes${def.size / 8}`; 55 | case 'address': return 'address'; 56 | } 57 | } 58 | if (def.meta == 'array') return `${toTypeName(def.baseType)}[${def.length || ''}]`; 59 | return def.name; 60 | } 61 | 62 | export const abiStructToSol = (struct: AbiStruct | AbiEnum): ArrayJoinInput => { 63 | let arr: ArrayJoinInput = []; 64 | if (struct.meta == 'enum') { 65 | let size = 7 + struct.name.length + struct.fields.reduce((sum, f) => sum + f.length, 0); 66 | if (size < 60) arr = [[`enum ${struct.name} { ${struct.fields.join(', ')} }`].join('')] 67 | else arr = [ 68 | `enum ${struct.name} {`, 69 | struct.fields.map(f => `${f},`), 70 | // struct.fields.map(f => `${f},`), 71 | `}` 72 | ]; 73 | } 74 | else arr = [ 75 | `struct ${struct.name} {`, 76 | struct.fields.map(field => `${toTypeName(field.type)} ${field.name};`), 77 | `}` 78 | ]; 79 | return arr; 80 | } 81 | 82 | export const scopedName = (def: AbiStructField, scopeName?: string): string => [scopeName, def.name].filter(x => x).join('_'); 83 | 84 | export function arrJoiner(arr: ArrayJoinInput) { 85 | const ret: string[] = []; 86 | const doMap = (subArr: ArrayJoinInput, depth = 0) => { 87 | if (subArr == null || subArr == undefined) return; 88 | if (Array.isArray(subArr)) for (let x of subArr) doMap(x, depth + 1); 89 | else if (typeof subArr == 'string') { 90 | if (subArr.length > 0) ret.push(`${'\t'.repeat(depth)}${subArr}`) 91 | else ret.push(''); 92 | } 93 | } 94 | for (let x of arr) doMap(x); 95 | if (ret[ret.length - 1] == '' || ret[ret.length-1] == '\n') ret.pop(); 96 | return ret.join(`\n`); 97 | } 98 | 99 | /** 100 | * Returns the solidity code needed to build the struct, assuming the variable names are already assigned. 101 | * @example Using the structs below: 102 | * - getFieldConstructor(ABC, 'a') should yield `ABC(a_a)` 103 | * - getFieldConstructor(DEF) should yield `DEF(ABC(a_a), ABC(b_a)) 104 | * - struct ABC { uint256 a; } 105 | * - struct DEF { ABC a; ABC b;} 106 | * @param struct abi struct definition 107 | */ 108 | export const getFieldConstructor = (struct: AbiStruct, scopeName?: string): string => { 109 | let arr: string[] = []; 110 | for (let field of struct.fields) { 111 | let name = scopedName(field, scopeName); 112 | if (field.type.meta == 'struct') arr.push(getFieldConstructor(field.type, name)); 113 | else arr.push(scopedName(field, scopeName)) 114 | } 115 | return `${struct.name}(${arr.join(', ')})` 116 | } 117 | 118 | /** 119 | * If a struct has an array field, it will only be unpackable if the array 120 | * has a static size or comes at the end of the struct definition. 121 | * @param struct Struct definition. 122 | */ 123 | export const isStructAllowed = (struct: AbiStruct): boolean => { 124 | let len = struct.fields.length; 125 | // return struct.fields.filter((field, i) => field.type.dynamic && i != len - 1).length > 0 126 | for (let i = 0; i < len; i++) { 127 | const field = struct.fields[i]; 128 | if (field.type.dynamic) { 129 | /* If field is dynamic and not at the end of the struct, it is not allowed. */ 130 | if (i != len - 1) return false; 131 | /* If field is at the end of the struct but is composed of unacceptable child fields, it is not allowed. */ 132 | if (field.type.meta == 'array' && field.type.baseType.dynamic) return false; 133 | if (field.type.meta == 'struct' && !isStructAllowed(field.type)) return false; 134 | } 135 | } 136 | return true; 137 | } -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type AbiStructField = { 2 | name: string; 3 | type: AbiType; 4 | } 5 | 6 | export type AbiStruct = { 7 | meta: 'struct'; 8 | name: string; 9 | fields: AbiStructField[]; 10 | dynamic?: boolean; 11 | size?: number; 12 | } 13 | 14 | export type AbiArray = { 15 | meta: 'array'; 16 | baseType: AbiType; 17 | length?: number; 18 | dynamic?: boolean; 19 | size?: number; 20 | } 21 | 22 | export type BasicElementaryType = 'bool' | 'byte' | 'bytes' | 'uint' | 'address'; 23 | 24 | export type AbiElementaryType = { 25 | meta: 'elementary'; 26 | type: BasicElementaryType; 27 | dynamic?: boolean; 28 | size?: number; 29 | } 30 | 31 | export type AbiEnum = { 32 | meta: 'enum'; 33 | name: string; 34 | fields: string[]; 35 | dynamic: false; 36 | size?: number; 37 | } 38 | 39 | export type AbiType = AbiStruct | AbiArray | AbiElementaryType | AbiEnum; 40 | 41 | export type ArrayJoinInput = Array> | Array | T; 42 | 43 | export type SolGenState = { 44 | currentIndex: number; 45 | variableDefinitions: string[]; 46 | struct: AbiStruct; 47 | codeChunks: ArrayJoinInput; 48 | returnLine: string; 49 | } -------------------------------------------------------------------------------- /src/parser/index.ts: -------------------------------------------------------------------------------- 1 | import Parser = require('./parser'); 2 | import { DefinedType, EnumType, StructType, StructField, TypeName } from './types'; 3 | import { AbiType, AbiStructField, AbiStruct, AbiEnum } from '../lib/types'; 4 | import { bitsRequired, elementaryToTypeDef } from '../lib/helpers'; 5 | 6 | class ParserWrapper { 7 | structs: { [key: string]: AbiType } = {}; 8 | 9 | constructor(parsed: DefinedType[]) { 10 | let i = 0; 11 | let n = 0; 12 | while (n < parsed.length && i < 50) for (let p of parsed) if (this.handleType(p)) (i++ && n++); 13 | } 14 | 15 | get allStructs(): AbiType[] { 16 | return Object.keys(this.structs).reduce((arr, k) => [...arr, this.structs[k]], []); 17 | } 18 | 19 | handleType(input: DefinedType): AbiType { 20 | if (input.type == 'EnumDefinition') { 21 | const { type, name, fields, namePath } = input; 22 | const out: AbiType = { 23 | meta: 'enum', 24 | name, 25 | fields, 26 | dynamic: false, 27 | size: bitsRequired(fields.length) 28 | }; 29 | return this.putStruct(namePath, out); 30 | } 31 | if (input.type == 'StructDefinition') { 32 | const { name, fields, namePath } = input; 33 | const outFields: AbiStructField[] = []; 34 | let size = 0; 35 | for (let field of fields) { 36 | const { name, typeName } = field; 37 | const abiType = this.convertFieldType(typeName); 38 | if (!abiType) return null; 39 | outFields.push({ name, type: abiType }); 40 | if (abiType.size == null) size = null; 41 | else if (size != null) size += abiType.size; 42 | } 43 | // for (let f of outFields) 44 | // const size = outFields.reduce((sum, f)=> (sum != null && f.type.size != null) ? sum + f.type.size : null, 0); 45 | const struct: AbiType = { 46 | meta: 'struct', 47 | size, 48 | dynamic: size == null, 49 | name, 50 | fields: outFields 51 | } 52 | return this.putStruct(namePath, struct); 53 | } 54 | throw new Error(`Did not recognize type of ${input}`) 55 | } 56 | 57 | convertFieldType(typeName: TypeName): AbiType { 58 | const { type, baseTypeName, name, namePath, length } = typeName; 59 | switch(type) { 60 | case 'ArrayTypeName': 61 | const baseType = this.convertFieldType(baseTypeName); 62 | const size = (baseType.size && length) ? length.number * baseType.size : null 63 | return { 64 | meta: 'array', 65 | baseType, 66 | length: length && length.number, 67 | dynamic: size == null, 68 | size 69 | } 70 | case 'ElementaryTypeName': return elementaryToTypeDef(name); 71 | case 'UserDefinedTypeName': return this.structs[namePath] || null; 72 | } 73 | } 74 | 75 | putStruct(namePath: string, struct: AbiType) { 76 | this.structs[namePath] = struct; 77 | return struct; 78 | } 79 | } 80 | 81 | export function parseCode(sourceCode: string): AbiType[] { 82 | if (!(/pragma solidity/g.exec(sourceCode))) { 83 | sourceCode = ['pragma solidity ^0.6.0;', '', sourceCode].join('\n'); 84 | } 85 | if (!(/(library {|contract {)/g.exec(sourceCode))) { 86 | sourceCode = ['library TmpLib {', sourceCode, '}'].join('\n'); 87 | } 88 | const input = Parser.parseFile(sourceCode); 89 | const handler = new ParserWrapper(input); 90 | return handler.allStructs; 91 | } -------------------------------------------------------------------------------- /src/parser/parser.ts: -------------------------------------------------------------------------------- 1 | import parser, { 2 | ArrayTypeName, 3 | ElementaryTypeName, 4 | EnumDefinition, 5 | UserDefinedTypeName, 6 | StructDefinition, 7 | ContractDefinition, 8 | VariableDeclaration, 9 | TypeName as AntlrTypeName, 10 | NumberLiteral, 11 | SourceUnit 12 | } from 'solidity-parser-antlr'; 13 | import { EnumType, StructType, StructField, TypeName, DefinedType } from './types'; 14 | const path = require('path'); 15 | const fs = require('fs'); 16 | 17 | function parseTypeName(scopeName: string, typeName: AntlrTypeName): TypeName { 18 | if (typeName.type == 'ElementaryTypeName') return typeName; 19 | if (typeName.type == 'UserDefinedTypeName') { 20 | let namePath = (typeName.namePath.includes('.')) 21 | ? typeName.namePath : `${scopeName}.${typeName.namePath}` 22 | return {...typeName, namePath} 23 | } 24 | if (typeName.type == 'ArrayTypeName') { 25 | const { type, baseTypeName, length } = typeName; 26 | return { 27 | type, 28 | baseTypeName: parseTypeName(scopeName, baseTypeName), 29 | length: { 30 | number: +(length as NumberLiteral).number, 31 | type: 'NumberLiteral' 32 | } 33 | } 34 | } 35 | if (typeName.type == 'Mapping') { 36 | throw new Error(`Caught unencodable type Mapping in ${scopeName} -> ${name}`) 37 | } 38 | throw new Error(`Did not recognize type ${typeName.type} in ${scopeName} -> ${name}`) 39 | } 40 | 41 | /** 42 | * @param scopeName Name of the scope which contains the member (contract.struct) 43 | * @param member 44 | * @member type VariableDeclaration 45 | * @member typeName Object with data about the type 46 | * @member name Name of the field in the struct 47 | */ 48 | export function parseMember(scopeName: string, member: VariableDeclaration): StructField { 49 | const { typeName, name } = member; 50 | return { typeName: parseTypeName(scopeName, typeName), name }; 51 | } 52 | 53 | export function parseStruct(scopeName: string, subNode: StructDefinition): StructType { 54 | const {type, name, members} = subNode; 55 | const namePath = `${scopeName}.${name}`; 56 | const fields = members.map(member => parseMember(scopeName, member)); 57 | return { name, type, namePath, fields }; 58 | } 59 | 60 | export function parseEnum(scopeName: string, subNode: EnumDefinition): EnumType { 61 | const {type, name, members} = subNode; 62 | const namePath = `${scopeName}.${name}`; 63 | const fields = members.map(({ name }) => name); 64 | return { name, type, namePath, fields }; 65 | } 66 | 67 | export function parseSubNode(scopeName: string, subNode: StructDefinition | EnumDefinition): DefinedType { 68 | const {type, name} = subNode; 69 | if (type == 'StructDefinition') return parseStruct(scopeName, subNode as StructDefinition); 70 | if (type == 'EnumDefinition') return parseEnum(scopeName, subNode as EnumDefinition); 71 | } 72 | 73 | export function parseContract(contractNode: ContractDefinition): DefinedType[] { 74 | let structs = []; 75 | const { subNodes, name } = contractNode; 76 | for (let subNode of subNodes) { 77 | const node = subNode as StructDefinition | EnumDefinition; 78 | try { 79 | const parsed = parseSubNode(name, node); 80 | if (parsed) structs.push(parsed); 81 | } catch(err) { 82 | console.log(`Failed to parse subNode ${node.name} in ${name}: \n\t${err.message}`) 83 | } 84 | } 85 | return structs; 86 | } 87 | 88 | export function parseFile(file: string): DefinedType[] { 89 | let structs = []; 90 | const { children } = parser.parse(file, { tolerant: true }); 91 | for (let child of children) if (child.type == 'ContractDefinition') { 92 | let _structs = parseContract(child) 93 | if (_structs.length) structs = structs.concat(_structs) 94 | } 95 | return structs; 96 | } 97 | 98 | export function recursiveDirectorySearch(_dirPath) { 99 | const files = fs.readdirSync(_dirPath).map(name => path.join(_dirPath, name)); 100 | let filePaths = []; 101 | for (let fileName of files) { 102 | if (!fileName.includes('.')) { 103 | const subFiles = recursiveDirectorySearch(fileName); 104 | filePaths = filePaths.concat(subFiles); 105 | } else if (fileName.includes('.sol')) { 106 | filePaths.push(fileName); 107 | } 108 | } 109 | return filePaths; 110 | } 111 | 112 | export function parseDirectory(_dirPath) { 113 | const filePaths = recursiveDirectorySearch(_dirPath); 114 | let structs = []; 115 | for (let filePath of filePaths) { 116 | const data = fs.readFileSync(filePath, 'utf8'); 117 | try { 118 | const _structs = parseFile(data); 119 | if (_structs.length) { 120 | const relativePath = filePath.replace(_dirPath, ''); 121 | structs = structs.concat(_structs.map(x => ({...x, fromFile: relativePath}))) 122 | } 123 | } catch(err) { 124 | console.log(`Caught error parsing ${filePath}`) 125 | throw err; 126 | } 127 | } 128 | return structs; 129 | } -------------------------------------------------------------------------------- /src/parser/types.ts: -------------------------------------------------------------------------------- 1 | export type StructFieldType = 'ElementaryTypeName' | 'UserDefinedTypeName' | 'ArrayTypeName'; 2 | 3 | export type Length = { 4 | type: 'NumberLiteral', 5 | number: number, 6 | } 7 | 8 | export type TypeName = { 9 | type: StructFieldType; 10 | baseTypeName?: TypeName; 11 | name?: string; 12 | namePath?: string; 13 | length?: Length | null; 14 | } 15 | 16 | export type StructField = { 17 | name: string; 18 | typeName: TypeName; 19 | } 20 | 21 | export type DefinedType = EnumType | StructType; 22 | 23 | export type EnumType = { 24 | type: 'EnumDefinition'; 25 | name: string; 26 | namePath: string; 27 | fields: Array; 28 | } 29 | 30 | export type StructType = { 31 | type: 'StructDefinition'; 32 | name: string; 33 | namePath: string; 34 | fields: Array; 35 | } -------------------------------------------------------------------------------- /test/test-files/TestInput.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | library TestInput { 4 | enum ABC { a, b } 5 | 6 | struct TestWrapped { 7 | uint32 a; 8 | bytes32 b; 9 | bytes32 c; 10 | uint8 d; 11 | ABC e; 12 | } 13 | 14 | struct TestWrapper { 15 | TestWrapped a; 16 | TestWrapped b; 17 | bytes32[3] x; 18 | } 19 | } -------------------------------------------------------------------------------- /test/test-files/TestOutput.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | library TestOutput { 4 | enum ABC { a, b } 5 | 6 | struct TestWrapped { 7 | uint32 a; 8 | bytes32 b; 9 | bytes32 c; 10 | uint8 d; 11 | ABC e; 12 | } 13 | 14 | function unpackTestWrapped(bytes memory input) 15 | internal pure returns (TestWrapped memory ret) { 16 | assembly { 17 | let ptr := add(input, 32) 18 | mstore(ret, shr(224, mload(ptr))) 19 | mstore(add(ret, 32), mload(add(ptr, 4))) 20 | mstore(add(ret, 64), mload(add(ptr, 36))) 21 | mstore(add(ret, 96), shr(248, mload(add(ptr, 68)))) 22 | mstore(add(ret, 128), shr(248, mload(add(ptr, 69)))) 23 | } 24 | } 25 | 26 | struct TestWrapper { 27 | TestWrapped a; 28 | TestWrapped b; 29 | bytes32[3] x; 30 | } 31 | 32 | function unpackTestWrapper(bytes memory input) 33 | internal pure returns (TestWrapper memory ret) { 34 | assembly { 35 | let ptr := add(input, 32) 36 | mstore(ret, shr(224, mload(ptr))) 37 | mstore(add(ret, 32), mload(add(ptr, 4))) 38 | mstore(add(ret, 64), mload(add(ptr, 36))) 39 | mstore(add(ret, 96), shr(248, mload(add(ptr, 68)))) 40 | mstore(add(ret, 128), shr(248, mload(add(ptr, 69)))) 41 | mstore(add(ret, 160), shr(224, mload(add(ptr, 70)))) 42 | mstore(add(ret, 192), mload(add(ptr, 74))) 43 | mstore(add(ret, 224), mload(add(ptr, 106))) 44 | mstore(add(ret, 256), shr(248, mload(add(ptr, 138)))) 45 | mstore(add(ret, 288), shr(248, mload(add(ptr, 139)))) 46 | mstore(add(ret, 320), mload(add(ptr, 140))) 47 | mstore(add(ret, 352), mload(add(ptr, 172))) 48 | mstore(add(ret, 384), mload(add(ptr, 204))) 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /test/test-files/TestOutputVerbose.sol: -------------------------------------------------------------------------------- 1 | pragma solidity ^0.6.0; 2 | 3 | library TestOutputVerbose { 4 | enum ABC { a, b } 5 | 6 | struct TestWrapped { 7 | uint32 a; 8 | bytes32 b; 9 | bytes32 c; 10 | uint8 d; 11 | ABC e; 12 | } 13 | 14 | function unpackTestWrapped(bytes memory input) 15 | internal pure returns (TestWrapped memory) { 16 | uint32 a; 17 | bytes32 b; 18 | bytes32 c; 19 | uint8 d; 20 | ABC e; 21 | assembly { 22 | let ptr := add(input, 32) 23 | a := shr(224, mload(ptr)) 24 | b := mload(add(ptr, 4)) 25 | c := mload(add(ptr, 36)) 26 | d := shr(248, mload(add(ptr, 68))) 27 | e := shr(248, mload(add(ptr, 69))) 28 | } 29 | return TestWrapped(a, b, c, d, e); 30 | } 31 | 32 | struct TestWrapper { 33 | TestWrapped a; 34 | TestWrapped b; 35 | bytes32[3] x; 36 | } 37 | 38 | function unpackTestWrapper(bytes memory input) 39 | internal pure returns (TestWrapper memory) { 40 | uint32 a_a; 41 | bytes32 a_b; 42 | bytes32 a_c; 43 | uint8 a_d; 44 | ABC a_e; 45 | uint32 b_a; 46 | bytes32 b_b; 47 | bytes32 b_c; 48 | uint8 b_d; 49 | ABC b_e; 50 | bytes32[3] memory x; 51 | assembly { 52 | let ptr := add(input, 32) 53 | a_a := shr(224, mload(ptr)) 54 | a_b := mload(add(ptr, 4)) 55 | a_c := mload(add(ptr, 36)) 56 | a_d := shr(248, mload(add(ptr, 68))) 57 | a_e := shr(248, mload(add(ptr, 69))) 58 | b_a := shr(224, mload(add(ptr, 70))) 59 | b_b := mload(add(ptr, 74)) 60 | b_c := mload(add(ptr, 106)) 61 | b_d := shr(248, mload(add(ptr, 138))) 62 | b_e := shr(248, mload(add(ptr, 139))) 63 | mstore(x, mload(add(ptr, 140))) 64 | mstore(add(x, 32), mload(add(ptr, 172))) 65 | mstore(add(x, 64), mload(add(ptr, 204))) 66 | } 67 | return TestWrapper(TestWrapped(a_a, a_b, a_c, a_d, a_e), TestWrapped(b_a, b_b, b_c, b_d, b_e), x); 68 | } 69 | } -------------------------------------------------------------------------------- /test/test-files/test-output.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "meta": "enum", 4 | "name": "ABC", 5 | "fields": [ 6 | "a", 7 | "b" 8 | ], 9 | "dynamic": false, 10 | "size": 8 11 | }, 12 | { 13 | "meta": "struct", 14 | "size": 560, 15 | "dynamic": false, 16 | "name": "TestWrapped", 17 | "fields": [ 18 | { 19 | "name": "a", 20 | "type": { 21 | "meta": "elementary", 22 | "dynamic": false, 23 | "size": 32, 24 | "type": "uint" 25 | } 26 | }, 27 | { 28 | "name": "b", 29 | "type": { 30 | "meta": "elementary", 31 | "dynamic": false, 32 | "size": 256, 33 | "type": "bytes" 34 | } 35 | }, 36 | { 37 | "name": "c", 38 | "type": { 39 | "meta": "elementary", 40 | "dynamic": false, 41 | "size": 256, 42 | "type": "bytes" 43 | } 44 | }, 45 | { 46 | "name": "d", 47 | "type": { 48 | "meta": "elementary", 49 | "dynamic": false, 50 | "size": 8, 51 | "type": "uint" 52 | } 53 | }, 54 | { 55 | "name": "e", 56 | "type": { 57 | "meta": "enum", 58 | "name": "ABC", 59 | "fields": [ 60 | "a", 61 | "b" 62 | ], 63 | "dynamic": false, 64 | "size": 8 65 | } 66 | } 67 | ] 68 | }, 69 | { 70 | "meta": "struct", 71 | "size": 1888, 72 | "dynamic": false, 73 | "name": "TestWrapper", 74 | "fields": [ 75 | { 76 | "name": "a", 77 | "type": { 78 | "meta": "struct", 79 | "size": 560, 80 | "dynamic": false, 81 | "name": "TestWrapped", 82 | "fields": [ 83 | { 84 | "name": "a", 85 | "type": { 86 | "meta": "elementary", 87 | "dynamic": false, 88 | "size": 32, 89 | "type": "uint" 90 | } 91 | }, 92 | { 93 | "name": "b", 94 | "type": { 95 | "meta": "elementary", 96 | "dynamic": false, 97 | "size": 256, 98 | "type": "bytes" 99 | } 100 | }, 101 | { 102 | "name": "c", 103 | "type": { 104 | "meta": "elementary", 105 | "dynamic": false, 106 | "size": 256, 107 | "type": "bytes" 108 | } 109 | }, 110 | { 111 | "name": "d", 112 | "type": { 113 | "meta": "elementary", 114 | "dynamic": false, 115 | "size": 8, 116 | "type": "uint" 117 | } 118 | }, 119 | { 120 | "name": "e", 121 | "type": { 122 | "meta": "enum", 123 | "name": "ABC", 124 | "fields": [ 125 | "a", 126 | "b" 127 | ], 128 | "dynamic": false, 129 | "size": 8 130 | } 131 | } 132 | ] 133 | } 134 | }, 135 | { 136 | "name": "b", 137 | "type": { 138 | "meta": "struct", 139 | "size": 560, 140 | "dynamic": false, 141 | "name": "TestWrapped", 142 | "fields": [ 143 | { 144 | "name": "a", 145 | "type": { 146 | "meta": "elementary", 147 | "dynamic": false, 148 | "size": 32, 149 | "type": "uint" 150 | } 151 | }, 152 | { 153 | "name": "b", 154 | "type": { 155 | "meta": "elementary", 156 | "dynamic": false, 157 | "size": 256, 158 | "type": "bytes" 159 | } 160 | }, 161 | { 162 | "name": "c", 163 | "type": { 164 | "meta": "elementary", 165 | "dynamic": false, 166 | "size": 256, 167 | "type": "bytes" 168 | } 169 | }, 170 | { 171 | "name": "d", 172 | "type": { 173 | "meta": "elementary", 174 | "dynamic": false, 175 | "size": 8, 176 | "type": "uint" 177 | } 178 | }, 179 | { 180 | "name": "e", 181 | "type": { 182 | "meta": "enum", 183 | "name": "ABC", 184 | "fields": [ 185 | "a", 186 | "b" 187 | ], 188 | "dynamic": false, 189 | "size": 8 190 | } 191 | } 192 | ] 193 | } 194 | }, 195 | { 196 | "name": "x", 197 | "type": { 198 | "meta": "array", 199 | "baseType": { 200 | "meta": "elementary", 201 | "dynamic": false, 202 | "size": 256, 203 | "type": "bytes" 204 | }, 205 | "length": 3, 206 | "dynamic": false, 207 | "size": 768 208 | } 209 | } 210 | ] 211 | } 212 | ] -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { parseCode } from '../src/parser/index'; 4 | import { UnpackerGen } from '../src/code-gen/sol-gen'; 5 | import { AbiStruct, AbiEnum } from '../src/lib/types'; 6 | 7 | import { expect } from 'chai'; 8 | 9 | describe('Test ABI Solidity Codegen', () => { 10 | let testInput, testOutput, testOutputVerbose, testOutputJson; 11 | before(() => { 12 | testInput = fs.readFileSync(path.join(__dirname, 'test-files', 'TestInput.sol'), 'utf8'); 13 | testOutput = fs.readFileSync(path.join(__dirname, 'test-files', 'TestOutput.sol'), 'utf8'); 14 | testOutputVerbose = fs.readFileSync(path.join(__dirname, 'test-files', 'TestOutputVerbose.sol'), 'utf8'); 15 | testOutputJson = fs.readFileSync(path.join(__dirname, 'test-files', 'test-output.json'), 'utf8'); 16 | }) 17 | 18 | it('Should generate the correct outputs', () => { 19 | const structs = parseCode(testInput); 20 | const json = JSON.stringify(structs, null, 2); 21 | expect(json).to.eql(testOutputJson); 22 | const sol = UnpackerGen.createLibrary(`TestOutput`, > structs, { verbose: false }); 23 | expect(sol).to.eql(testOutput); 24 | const solVerbose = UnpackerGen.createLibrary(`TestOutputVerbose`, > structs); 25 | expect(solVerbose).to.eql(testOutputVerbose); 26 | }) 27 | }) 28 | 29 | // const testInput = fs.readFileSync(path.join(__dirname, 'test-files', 'TestInput.sol'), 'utf8'); 30 | // const structs = parseCode(testInput); 31 | // const sol = UnpackerGen.createLibrary(`TestOutput`, > structs, { verbose: false }); 32 | // const solVerbose = UnpackerGen.createLibrary(`TestOutputVerbose`, > structs); 33 | // const json = JSON.stringify(structs, null, 2); 34 | // fs.writeFileSync(path.join(__dirname, 'test-files', 'TestOutput.sol'), sol); 35 | // fs.writeFileSync(path.join(__dirname, 'test-files', 'TestOutputVerbose.sol'), solVerbose); 36 | // fs.writeFileSync(path.join(__dirname, 'test-files', 'test-output.json'), json) 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "dist", 7 | "noImplicitAny": false, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "resolveJsonModule": true, 11 | "sourceMap": true 12 | }, 13 | "include": ["src/**/*.ts"] 14 | } 15 | --------------------------------------------------------------------------------