diff --git a/package-lock.json b/package-lock.json index 226e2a2..c94cb0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vulcanize/eth-watcher-ts", - "version": "0.0.14", + "version": "0.0.16", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@vulcanize/eth-watcher-ts", - "version": "0.0.14", + "version": "0.0.16", "dependencies": { "@apollo/client": "^3.2.0", "@graphile/pg-pubsub": "^4.11.0", diff --git a/src/services/contractService.ts b/src/services/contractService.ts index 2d9749d..fa6233f 100644 --- a/src/services/contractService.ts +++ b/src/services/contractService.ts @@ -1,5 +1,3 @@ - -import { StateVariableDeclaration, StructDefinition } from 'solidity-parser-diligence'; import { getConnection } from 'typeorm'; import Contract from '../models/contract/contract'; import Event from '../models/contract/event'; @@ -12,11 +10,10 @@ import MethodRepository from '../repositories/contract/methodRepository'; import StateRepository from '../repositories/contract/stateRepository'; import AddressRepository from '../repositories/data/addressRepository'; import { ABI } from "../types"; -import { structureToSignatureType } from './dataTypeParser'; import ApplicationError from "../errors/applicationError"; +import { getStatesFromSourceCode } from '../utils/contract'; const childProcess = require('child_process'); // eslint-disable-line -const parser = require('@solidity-parser/parser'); // eslint-disable-line type ContractParam = { address: string; @@ -95,7 +92,7 @@ export default class ContractService { // prepare states const stateIds: number[] = []; - const stateObjects = await this.getStatesFromSourceCode(contractObj.sourceCode); + const stateObjects = await getStatesFromSourceCode(contractObj.sourceCode); for (const stateObject of stateObjects) { const state = await stateRepository.add({ slot: stateObject.slot, @@ -157,30 +154,6 @@ export default class ContractService { }) } - private async getStatesFromSourceCode(sourceCode: string): Promise { - const ast = parser.parse(sourceCode, { - tolerant: true, - }); - - let list = []; - const contractDefinitions = ast?.children?.filter((item) => item.type === 'ContractDefinition'); - for (const contractDefinition of contractDefinitions) { - const states = contractDefinition?.subNodes.filter(n => n.type == 'StateVariableDeclaration') as StateVariableDeclaration[]; - const structs = contractDefinition?.subNodes.filter(n => n.type == 'StructDefinition') as StructDefinition[]; - - list = list.concat(states?.map((item, slot) => { - const type: string = structureToSignatureType(item.variables[0]?.name, item.variables[0]?.typeName, structs).signature; - return { - slot, - type, - variable: item.variables[0]?.name, - } - })); - } - - return list as State[]; - } - private getEventsFromABI(abi: ABI): string[] { if (!abi) { return []; diff --git a/src/services/dataTypeParser.ts b/src/services/dataTypeParser.ts index 9c2f7f8..f45f1b9 100644 --- a/src/services/dataTypeParser.ts +++ b/src/services/dataTypeParser.ts @@ -97,46 +97,6 @@ function parseStructure(name: string, typeName: TypeName, structs: StructDefinit } } -export function structureToSignatureType(name: string, typeName: TypeName, structs: StructDefinition[], level = 0, isArray = false): {signature: string; type: string; hasStruct: boolean} { - let structsDef = ''; - if (level === 0 && structs && structs.length) { - for (const struct of structs) { - structsDef += ` struct ${struct.name} {` + struct.members.map(m => structureToSignatureType(m.name, m.typeName, structs, level + 1).signature).join('') + '}'; - } - } - - switch (typeName.type) { - case 'ElementaryTypeName': - return { - signature: `${typeName.name}${isArray ? '[]' : ''} ${name};`, - type: typeName.name, - hasStruct: false, - } - case 'ArrayTypeName': { - const res = structureToSignatureType(name, typeName.baseTypeName, structs, level + 1, true); - return { - signature: res.signature + (level === 0 && res.hasStruct ? structsDef : ''), - type: typeName.baseTypeName?.type, - hasStruct: res.hasStruct, - } - } - case 'Mapping': { - const res = structureToSignatureType(name, typeName.valueType, structs, level + 1); - return { - signature: `mapping (${typeName.keyType.name} => ${res.type}) ${name};` + (level === 0 && res.hasStruct ? structsDef : ''), - type: `mapping (${typeName.keyType.name} => ${res.type})`, // for mapping => mapping case - hasStruct: res.hasStruct, - } - } - case 'UserDefinedTypeName': - return { - signature: `${typeName.namePath}${isArray ? '[]' : ''} ${name};` + (level === 0 ? structsDef : ''), - type: typeName.type, - hasStruct: true, - }; - } -} - /** * Parse structure from list of variables and types * ```typescript diff --git a/src/utils/contract.test.ts b/src/utils/contract.test.ts new file mode 100644 index 0000000..9d407c5 --- /dev/null +++ b/src/utils/contract.test.ts @@ -0,0 +1,187 @@ +import {getStatesFromSourceCode} from "./contract"; + +describe('getStatesfromSourceCode', () => { + test('elementary types', async () => { + const sourceCode = ` + contract Test { + string public name = "Uniswap"; + + uint8 public decimals = 18; + + int256 public integers = -21; + + bool internal boolean = true; + + address private plainAddress = '0x1ca7c995f8eF0A2989BbcE08D5B7Efe50A584aa1' + + address payable payableAddress = '0xbb38B6F181541a6cEdfDac0b94175B2431Aa1A02' + + bytes32 byteText = "ByteText"; + } + `; + + const states = await getStatesFromSourceCode(sourceCode); + expect(states).toStrictEqual([ + { slot: 0, type: 'string name;', variable: 'name' }, + { slot: 1, type: 'uint8 decimals;', variable: 'decimals' }, + { slot: 2, type: 'int256 integers;', variable: 'integers' }, + { slot: 3, type: 'bool boolean;', variable: 'boolean' }, + { slot: 4, type: 'address plainAddress;', variable: 'plainAddress' }, + { + slot: 5, + type: 'address payableAddress;', + variable: 'payableAddress' + }, + { slot: 6, type: 'bytes32 byteText;', variable: 'byteText' } + ]) + }) + + test('array types', async () => { + const sourceCode = ` + contract Test { + string[] public names = ["Uniswap"]; + + string[][] public nestedNames; + + uint[] public decimalList; + } + `; + + const states = await getStatesFromSourceCode(sourceCode); + expect(states).toStrictEqual([ + { slot: 0, type: 'string[] names;', variable: 'names' }, + { slot: 1, type: 'string[] nestedNames;', variable: 'nestedNames' }, + { slot: 2, type: 'uint[] decimalList;', variable: 'decimalList' } + ]) + }) + + test('mapping types', async () => { + const sourceCode = ` + contract Test { + mapping (address => uint96) internal balances; + + mapping (address => mapping (address => uint96)) internal allowances; + } + `; + + const states = await getStatesFromSourceCode(sourceCode); + expect(states).toStrictEqual([ + { + slot: 0, + type: 'mapping (address => uint96) balances;', + variable: 'balances' + }, + { + slot: 1, + type: 'mapping (address => mapping (address => uint96)) allowances;', + variable: 'allowances' + } + ]) + }) + + test('user defined types', async () => { + const sourceCode = ` + contract Test { + enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill } + + ActionChoices choice; + + struct Checkpoint { + uint32 fromBlock; + uint96 votes; + } + + Checkpoint checkpoint; + + Checkpoint[] public checkpointList; + + mapping(address => Checkpoint) public checkpointMap; + + mapping(address => mapping(uint => Checkpoint)) public checkpointNestedMap; + + struct NestedCheckpoint { + uint32 fromBlock; + SomeVotes votes; + } + + struct SomeVotes { + uint96 votes; + } + + mapping(address => mapping(uint => NestedCheckpoint)) public nestedCheckpointNestedMap; + + struct KeyFlag { + uint key; + bool deleted; + } + + struct ComplexCheckpoint { + uint32 fromBlock; + mapping(uint => SomeVotes) votes; + KeyFlag[] keys; + } + + mapping(address => ComplexCheckpoint) public complexCheckpointMap; + } + `; + + const states = await getStatesFromSourceCode(sourceCode); + + expect(states).toStrictEqual([ + { + slot: 0, + type: 'ActionChoices choice; struct Checkpoint {uint32 fromBlock;uint96 votes;} struct NestedCheckpoint {uint32 fromBlock;SomeVotes votes;} struct SomeVotes {uint96 votes;} struct KeyFlag {uint key;bool deleted;} struct ComplexCheckpoint {uint32 fromBlock;mapping (uint => SomeVotes) votes;KeyFlag[] keys;}', + variable: 'choice' + }, + { + slot: 1, + type: 'Checkpoint checkpoint; struct Checkpoint {uint32 fromBlock;uint96 votes;} struct NestedCheckpoint {uint32 fromBlock;SomeVotes votes;} struct SomeVotes {uint96 votes;} struct KeyFlag {uint key;bool deleted;} struct ComplexCheckpoint {uint32 fromBlock;mapping (uint => SomeVotes) votes;KeyFlag[] keys;}', + variable: 'checkpoint' + }, + { + slot: 2, + type: 'Checkpoint[] checkpointList; struct Checkpoint {uint32 fromBlock;uint96 votes;} struct NestedCheckpoint {uint32 fromBlock;SomeVotes votes;} struct SomeVotes {uint96 votes;} struct KeyFlag {uint key;bool deleted;} struct ComplexCheckpoint {uint32 fromBlock;mapping (uint => SomeVotes) votes;KeyFlag[] keys;}', + variable: 'checkpointList' + }, + { + slot: 3, + type: 'mapping (address => Checkpoint) checkpointMap; struct Checkpoint {uint32 fromBlock;uint96 votes;} struct NestedCheckpoint {uint32 fromBlock;SomeVotes votes;} struct SomeVotes {uint96 votes;} struct KeyFlag {uint key;bool deleted;} struct ComplexCheckpoint {uint32 fromBlock;mapping (uint => SomeVotes) votes;KeyFlag[] keys;}', + variable: 'checkpointMap' + }, + { + slot: 4, + type: 'mapping (address => mapping (uint => Checkpoint)) checkpointNestedMap; struct Checkpoint {uint32 fromBlock;uint96 votes;} struct NestedCheckpoint {uint32 fromBlock;SomeVotes votes;} struct SomeVotes {uint96 votes;} struct KeyFlag {uint key;bool deleted;} struct ComplexCheckpoint {uint32 fromBlock;mapping (uint => SomeVotes) votes;KeyFlag[] keys;}', + variable: 'checkpointNestedMap' + }, + { + slot: 5, + type: 'mapping (address => mapping (uint => NestedCheckpoint)) nestedCheckpointNestedMap; struct Checkpoint {uint32 fromBlock;uint96 votes;} struct NestedCheckpoint {uint32 fromBlock;SomeVotes votes;} struct SomeVotes {uint96 votes;} struct KeyFlag {uint key;bool deleted;} struct ComplexCheckpoint {uint32 fromBlock;mapping (uint => SomeVotes) votes;KeyFlag[] keys;}', + variable: 'nestedCheckpointNestedMap' + }, + { + slot: 6, + type: 'mapping (address => ComplexCheckpoint) complexCheckpointMap; struct Checkpoint {uint32 fromBlock;uint96 votes;} struct NestedCheckpoint {uint32 fromBlock;SomeVotes votes;} struct SomeVotes {uint96 votes;} struct KeyFlag {uint key;bool deleted;} struct ComplexCheckpoint {uint32 fromBlock;mapping (uint => SomeVotes) votes;KeyFlag[] keys;}', + variable: 'complexCheckpointMap' + } + ]) + }) + + test('constant state variables', async () => { + const sourceCode = ` + contract Test { + string public name = "Uniswap"; + + uint public constant decimals = 18; + + bool internal boolean = true; + } + `; + + const states = await getStatesFromSourceCode(sourceCode); + + expect(states).toStrictEqual([ + { slot: 0, type: 'string name;', variable: 'name' }, + { slot: 1, type: 'bool boolean;', variable: 'boolean' } + ]) + }) +}); diff --git a/src/utils/contract.ts b/src/utils/contract.ts new file mode 100644 index 0000000..b8bdcb7 --- /dev/null +++ b/src/utils/contract.ts @@ -0,0 +1,74 @@ +import { StateVariableDeclaration, StructDefinition, TypeName } from 'solidity-parser-diligence'; + +import State from '../models/contract/state'; + +const parser = require('@solidity-parser/parser'); // eslint-disable-line + +const structureToSignatureType = (name: string, typeName: TypeName, structs: StructDefinition[], level = 0, isArray = false): {signature: string; type: string; hasStruct: boolean} => { + let structsDef = ''; + if (level === 0 && structs && structs.length) { + for (const struct of structs) { + structsDef += ` struct ${struct.name} {` + struct.members.map(m => structureToSignatureType(m.name, m.typeName, structs, level + 1).signature).join('') + '}'; + } + } + + switch (typeName.type) { + case 'ElementaryTypeName': + return { + signature: `${typeName.name}${isArray ? '[]' : ''} ${name};`, + type: typeName.name, + hasStruct: false, + } + case 'ArrayTypeName': { + const res = structureToSignatureType(name, typeName.baseTypeName, structs, level + 1, true); + return { + signature: res.signature + (level === 0 && res.hasStruct ? structsDef : ''), + type: typeName.baseTypeName?.type, + hasStruct: res.hasStruct, + } + } + case 'Mapping': { + const res = structureToSignatureType(name, typeName.valueType, structs, level + 1); + return { + signature: `mapping (${typeName.keyType.name} => ${res.type}) ${name};` + (level === 0 && res.hasStruct ? structsDef : ''), + type: `mapping (${typeName.keyType.name} => ${res.type})`, // for mapping => mapping case + hasStruct: res.hasStruct, + } + } + case 'UserDefinedTypeName': + return { + signature: `${typeName.namePath}${isArray ? '[]' : ''} ${name};` + (level === 0 ? structsDef : ''), + type: typeName.namePath, + hasStruct: true, + }; + } +} + +export const getStatesFromSourceCode = async(sourceCode: string): Promise => { + const ast = parser.parse(sourceCode, { + tolerant: true, + }); + + let list = []; + const contractDefinitions = ast?.children?.filter((item) => item.type === 'ContractDefinition'); + for (const contractDefinition of contractDefinitions) { + const states = contractDefinition?.subNodes.filter(n => n.type == 'StateVariableDeclaration') as StateVariableDeclaration[]; + const structs = contractDefinition?.subNodes.filter(n => n.type == 'StructDefinition') as StructDefinition[]; + // TODO: Handle EnumDefinition type subnodes. + + // isImmutable property of state variable is not present in type StateVariableDeclaration. + // TODO: Filter immutable variables after support added in https://github.com/ConsenSys/solidity-parser-antlr + list = list.concat(states?.filter((item) => !item.variables[0]?.isDeclaredConst) + .map((item, slot) => { + const type: string = structureToSignatureType(item.variables[0]?.name, item.variables[0]?.typeName, structs).signature; + return { + slot, + type, + variable: item.variables[0]?.name, + } + }) + ); + } + + return list as State[]; +}