diff --git a/package.json b/package.json index 6cc799b..3353b2f 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "coveralls": "^3.0.0", "documentation": "^9.0.0-alpha.0", "nyc": "^11.3.0", - "standard": "^12.0.1" + "standard": "^12.0.1", + "yargs": "^13.1.0" }, "dependencies": { "@babel/runtime": "^7.1.2", diff --git a/scripts/inspect b/scripts/inspect new file mode 100755 index 0000000..3c01125 --- /dev/null +++ b/scripts/inspect @@ -0,0 +1,55 @@ +#!/usr/bin/env node + +const yargs = require('yargs') +const fs = require('fs') +const path = require('path') +const Eth = require('web3-eth') + +const { metadataDAOEvaluate } = require('../dist') + +const NETWORKS = { + rinkeby: 'https://rinkeby.eth.aragon.network', + mainnet: 'https://mainnet.eth.aragon.network', +} + +const exec = async (argv) => { + const { network } = argv + const [ txHash ] = argv._ + + const rpc = NETWORKS[network] + + if (!rpc) { + throw new Error(`Unsupported network '${network}'`) + } + + const eth = new Eth(rpc) + + console.log(`Fetching ${network} for ${txHash}`) + const { + to, + from, + blockNumber, + input: data + } = await eth.getTransaction(txHash) + + const description = await metadataDAOEvaluate({ transaction: { to, data }}) + + console.log(`Transaction from ${from} to ${to} in block ${blockNumber}:\n`) + console.log(description ? `🔥 ${description} 🔥` : 'Unknown 😢') +} + +exec( + yargs + .usage('Usage: $0 [txid]') + .option('network', { + alias: 'n', + default: 'mainnet', + describe: 'Output path to radspec db file', + type: 'string', + }) + .argv + ) + .catch(err => { + console.error(err) + process.exit(1) + }) \ No newline at end of file diff --git a/src/extract.js b/src/extract.js new file mode 100644 index 0000000..2868754 --- /dev/null +++ b/src/extract.js @@ -0,0 +1,91 @@ +const fs = require('fs') +const { promisify } = require('util') +const readFile = promisify(fs.readFile) + +const modifiesStateAndIsPublic = declaration => + !declaration.match(/\b(internal|private|view|pure|constant)\b/) + +const typeOrAddress = type => { + const types = ['address', 'byte', 'uint', 'int', 'bool', 'string'] + + // check if the type starts with any of the above types, otherwise it is probably + // a typed contract, so we need to return address for the signature + return types.filter(t => type.indexOf(t) === 0).length > 0 ? type : 'address' +} + +// extracts function signature from function declaration +const getSignature = declaration => { + let [name, params] = declaration.match(/function ([^]*?)\)/)[1].split('(') + + if (!name) { + return 'fallback' + } + + let argumentNames = [] + + if (params) { + // Has parameters + const inputs = params + .replace(/\n/gm, '') + .replace(/\t/gm, '') + .split(',') + + params = inputs + .map(param => param.split(' ').filter(s => s.length > 0)[0]) + .map(type => typeOrAddress(type)) + .join(',') + + argumentNames = inputs.map(param => param.split(' ').filter(s => s.length > 0)[1] || '') + } + + return { sig: `${name}(${params})`, argumentNames } +} + +const getNotice = declaration => { + // capture from @notice to either next '* @' or end of comment '*/' + const notices = declaration.match(/(@notice)([^]*?)(\* @|\*\/)/m) + if (!notices || notices.length === 0) return null + + return notices[0] + .replace('*/', '') + .replace('* @', '') + .replace('@notice ', '') + .replace(/\n/gm, '') + .replace(/\t/gm, '') + .split(' ') + .filter(x => x.length > 0) + .join(' ') +} + +// extracts required role from function declaration +const getRoles = declaration => { + const auths = declaration.match(/auth.?\(([^]*?)\)/gm) + if (!auths) return [] + + return auths.map( + authStatement => + authStatement + .split('(')[1] + .split(',')[0] + .split(')')[0] + ) +} + +// Takes the path to a solidity file and extracts public function signatures, +// its auth role if any and its notice statement +module.exports = async sourceCodePath => { + const sourceCode = await readFile(sourceCodePath, 'utf8') + + // everything between every 'function' and '{' and its @notice + const funcDecs = sourceCode.match(/(@notice|^\s*function)(?:[^]*?){/gm) + + if (!funcDecs) return [] + + return funcDecs + .filter(dec => modifiesStateAndIsPublic(dec)) + .map(dec => ({ + roles: getRoles(dec), + notice: getNotice(dec), + ...getSignature(dec), + })) +} diff --git a/src/index.js b/src/index.js index e99fd75..40dfe8e 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,8 @@ * @module radspec */ const ABI = require('web3-eth-abi') +const MetadataDAO = require('metadata-dao') + const scanner = require('./scanner') const parser = require('./parser') const evaluator = require('./evaluator') @@ -103,10 +105,51 @@ function evaluate (source, call, options = {}) { return evaluateRaw(source, parameters, { ...options, to: call.transaction.to }) } +async function metadataDAOEvaluate (call, options = {}) { + const metadataDAO = new MetadataDAO() + + // Get method ID + const { to, data } = call.transaction + const methodId = data.substr(0, 10) + + const fn = await metadataDAO.query('radspec', 'sig', methodId) + + if (!fn) { + return null + } + + // If the function was found in local radspec registry. Decode and evaluate. + const { notice: source, signature: sig } = fn + + // get the array of input types from the function signature + const inputString = sig.replace(')', '').split('(')[1] + + let parameters = [] + + // If the function has parameters + if (inputString !== '') { + const inputs = inputString.split(',') + + // Decode parameters + const parameterValues = ABI.decodeParameters(inputs, '0x' + data.substr(10)) + parameters = inputs.reduce((acc, input, i) => ( + { + [`$${i + 1}`]: { + type: input, + value: parameterValues[i] + }, + ...acc + }), {}) + } + + return await evaluateRaw(source, parameters, { ...options, to }) +} + module.exports = { scan: scanner.scan, parse: parser.parse, evaluateRaw, - evaluate + evaluate, + metadataDAOEvaluate }