diff --git a/src/evaluator/index.js b/src/evaluator/index.js index 288d5c3..cc71292 100644 --- a/src/evaluator/index.js +++ b/src/evaluator/index.js @@ -2,7 +2,7 @@ * @module radspec/evaluator */ -import { ethers, BigNumber } from 'ethers' +import { BigNumber, providers as ethersProvider, utils as ethersUtils } from 'ethers' import types from '../types' import HelperManager from '../helpers/HelperManager' @@ -27,10 +27,10 @@ class TypedValue { } if (this.type === 'address') { - if (!ethers.utils.isAddress(this.value)) { + if (!ethersUtils.isAddress(this.value)) { throw new Error(`Invalid address "${this.value}"`) } - this.value = ethers.utils.getAddress(this.value) + this.value = ethersUtils.getAddress(this.value) } } @@ -52,7 +52,8 @@ class TypedValue { * @param {radspec/Bindings} bindings An object of bindings and their values * @param {?Object} options An options object * @param {?Object} options.availablehelpers Available helpers - * @param {?ethers.providers.Provider} options.provider EIP 1193 provider + * @param {?Object} options.availableFunctions Available function signatures + * @param {?ethersProvider.Provider} options.provider EIP 1193 provider * @param {?string} options.to The destination address for this expression's transaction * @property {radspec/parser/AST} ast * @property {radspec/Bindings} bindings @@ -61,17 +62,18 @@ export class Evaluator { constructor ( ast, bindings, - { availableHelpers = {}, provider, from, to, value = '0', data } = {} + { availableHelpers = {}, availableFunctions = {}, provider, from, to, value = '0', data } = {} ) { this.ast = ast this.bindings = bindings this.provider = - provider || new ethers.providers.WebSocketProvider(DEFAULT_ETH_NODE) + provider || new ethersProvider.WebSocketProvider(DEFAULT_ETH_NODE) this.from = from && new TypedValue('address', from) this.to = to && new TypedValue('address', to) this.value = new TypedValue('uint', BigNumber.from(value)) this.data = data && new TypedValue('bytes', data) this.helpers = new HelperManager(availableHelpers) + this.functions = availableFunctions } /** @@ -242,7 +244,7 @@ export class Evaluator { if (target.type !== 'bytes20' && target.type !== 'address') { this.panic('Target of call expression was not an address') - } else if (!ethers.utils.isAddress(target.value)) { + } else if (!ethersUtils.isAddress(target.value)) { this.panic(`Invalid address "${this.value}"`) } @@ -271,7 +273,7 @@ export class Evaluator { stateMutability: 'view' } ] - const ethersInterface = new ethers.utils.Interface(abi) + const ethersInterface = new ethersUtils.Interface(abi) const txData = ethersInterface.encodeFunctionData( node.callee, @@ -298,7 +300,8 @@ export class Evaluator { const inputs = await this.evaluateNodes(node.inputs) const result = await this.helpers.execute(helperName, inputs, { provider: this.provider, - evaluator: this + evaluator: this, + functions: this.functions }) return new TypedValue(result.type, result.value) @@ -368,7 +371,8 @@ export class Evaluator { * @param {radspec/Bindings} bindings An object of bindings and their values * @param {?Object} options An options object * @param {?Object} options.availablehelpers Available helpers - * @param {?ethers.providers.Provider} options.provider EIP 1193 provider + * @param {?Object} options.availableFunctions Available function signatures + * @param {?ethersProvider.Provider} options.provider EIP 1193 provider * @param {?string} options.to The destination address for this expression's transaction * @return {string} */ diff --git a/src/helpers/HelperManager.js b/src/helpers/HelperManager.js index 4f8bf2f..15e924e 100644 --- a/src/helpers/HelperManager.js +++ b/src/helpers/HelperManager.js @@ -29,13 +29,14 @@ export default class HelperManager { * @param {string} helper Helper name * @param {Array} inputs * @param {Object} config Configuration for running helper - * @param {ethers.providers.Provider} config.provider Current provider + * @param {ethersProvider.Provider} config.provider Current provider * @param {radspec/evaluator/Evaluator} config.evaluator Current evaluator + * @param {Object} config.functions Current function signatures * @return {Promise} */ - execute (helper, inputs, { provider, evaluator }) { + execute (helper, inputs, { provider, evaluator, functions }) { inputs = inputs.map((input) => input.value) // pass values directly - return this.availableHelpers[helper](provider, evaluator)(...inputs) + return this.availableHelpers[helper](provider, evaluator, functions)(...inputs) } /** diff --git a/src/helpers/fromHex.js b/src/helpers/fromHex.js index 3ffd1c1..04bb1b3 100644 --- a/src/helpers/fromHex.js +++ b/src/helpers/fromHex.js @@ -1,4 +1,4 @@ -import { ethers, BigNumber } from 'ethers' +import { BigNumber, utils as ethersUtils } from 'ethers' export default () => /** @@ -13,5 +13,5 @@ export default () => value: to === 'number' ? BigNumber.from(hex).toNumber() - : ethers.utils.toUtf8String(hex) + : ethersUtils.toUtf8String(hex) }) diff --git a/src/helpers/lib/methodRegistry.js b/src/helpers/lib/methodRegistry.js index dd28e0c..b117403 100644 --- a/src/helpers/lib/methodRegistry.js +++ b/src/helpers/lib/methodRegistry.js @@ -1,5 +1,5 @@ // From: https://github.com/danfinlay/eth-method-registry -import { ethers } from 'ethers' +import { Contract, providers as ethersProviders } from 'ethers' import { DEFAULT_ETH_NODE } from '../../defaults' @@ -15,7 +15,7 @@ const REGISTRY_MAP = { export default class MethodRegistry { constructor (opts = {}) { this.provider = - opts.provider || new ethers.providers.WebSocketProvider(DEFAULT_ETH_NODE) + opts.provider || new ethersProviders.WebSocketProvider(DEFAULT_ETH_NODE) this.registryAddres = opts.registry || REGISTRY_MAP[opts.network] } @@ -24,7 +24,7 @@ export default class MethodRegistry { throw new Error('No method registry found for the network.') } - this.registry = new ethers.Contract( + this.registry = new Contract( this.registryAddres, REGISTRY_LOOKUP_ABI, this.provider diff --git a/src/helpers/radspec.js b/src/helpers/radspec.js index d02d618..6122d8b 100644 --- a/src/helpers/radspec.js +++ b/src/helpers/radspec.js @@ -1,8 +1,7 @@ -import { ethers } from 'ethers' +import { utils as ethersUtils } from 'ethers' import MethodRegistry from './lib/methodRegistry' import { evaluateRaw } from '../lib/' -import { knownFunctions } from '../data/' import { DEFAULT_API_4BYTES } from '../defaults' const makeUnknownFunctionNode = (methodId) => ({ @@ -11,7 +10,7 @@ const makeUnknownFunctionNode = (methodId) => ({ }) const parse = (signature) => { - const fragment = ethers.utils.FunctionFragment.from(signature) + const fragment = ethersUtils.FunctionFragment.from(signature) return { name: @@ -27,24 +26,24 @@ const parse = (signature) => { } // Hash signature with Ethereum Identity and silce bytes -const getSigHah = (sig) => ethers.utils.hexDataSlice(ethers.utils.id(sig), 0, 4) +const getSigHah = (sig) => ethersUtils.hexDataSlice(ethersUtils.id(sig), 0, 4) // Convert from the knownFunctions data format into the needed format // Input: { "signature(type1,type2)": "Its radspec string", ... } // Output: { "0xabcdef12": { "fragment": FunctionFragment, "source": "Its radspec string" }, ...} const processFunctions = (functions) => Object.keys(functions).reduce((acc, key) => { - const fragment = ethers.utils.FunctionFragment.from(key) + const fragment = ethersUtils.FunctionFragment.from(key) return { [getSigHah(fragment.format())]: { source: functions[key], fragment }, ...acc } }, {}) -export default (provider, evaluator) => +export default (provider, evaluator, functions) => /** * Interpret calldata using radspec recursively. If the function signature is not in the package's known - * functions, it fallbacks to looking for the function name using github.com/parity-contracts/signature-registry + * functions, it fallbacks to looking for the function name using github.com/parity-contracts/signature-registry and finally using 4bytes API * * @param {address} addr The target address of the call * @param {bytes} data The calldata of the call @@ -52,7 +51,7 @@ export default (provider, evaluator) => * @return {Promise} */ async (addr, data, registryAddress) => { - const functions = processFunctions(knownFunctions) + const processedFunctions = processFunctions(functions) if (data.length < 10) { return makeUnknownFunctionNode(data) @@ -60,7 +59,7 @@ export default (provider, evaluator) => // Get method ID const methodId = data.substr(0, 10) - const fn = functions[methodId] + const fn = processedFunctions[methodId] // If function is not a known function if (!fn) { @@ -80,25 +79,29 @@ export default (provider, evaluator) => value: name // TODO: should we decode and print the arguments as well? } } catch { + try { // Try fetching 4bytes API - const { results } = await ethers.utils.fetchJson( - `${DEFAULT_API_4BYTES}?hex_signature=${methodId}` - ) - if (Array.isArray(results) && results.length > 0) { - const { name } = parse(results[0].text_signature) - return { - type: 'string', - value: name + const { results } = await ethersUtils.fetchJson({ + url: `${DEFAULT_API_4BYTES}?hex_signature=${methodId}`, + timeout: 3000 + }) + if (Array.isArray(results) && results.length > 0) { + const { name } = parse(results[0].text_signature) + return { + type: 'string', + value: name + } } + } catch { + // Fallback to unknown function + return makeUnknownFunctionNode(methodId) } - // Fallback to unknown function - return makeUnknownFunctionNode(methodId) } } // If the function was found in local radspec registry. Decode and evaluate. const { source, fragment } = fn - const ethersInterface = new ethers.utils.Interface([fragment]) + const ethersInterface = new ethersUtils.Interface([fragment]) // Decode parameters const args = ethersInterface.decodeFunctionData(fragment.name, data) @@ -119,6 +122,7 @@ export default (provider, evaluator) => value: await evaluateRaw(source, parameters, { provider, availableHelpers: evaluator.helpers.getHelpers(), + availableFunctions: functions, to: addr }) } diff --git a/src/helpers/tokenAmount.js b/src/helpers/tokenAmount.js index 57b7dc5..120d14f 100644 --- a/src/helpers/tokenAmount.js +++ b/src/helpers/tokenAmount.js @@ -1,4 +1,4 @@ -import { ethers, BigNumber } from 'ethers' +import { BigNumber, Contract, utils as ethersUtils } from 'ethers' import { ERC20_SYMBOL_BYTES32_ABI, @@ -30,7 +30,7 @@ export default (provider) => symbol = 'ETH' } } else { - let token = new ethers.Contract( + let token = new Contract( tokenAddress, ERC20_SYMBOL_DECIMALS_ABI, provider @@ -42,13 +42,13 @@ export default (provider) => symbol = (await token.symbol()) || '' } catch (err) { // Some tokens (e.g. DS-Token) use bytes32 for their symbol() - token = new ethers.Contract( + token = new Contract( tokenAddress, ERC20_SYMBOL_BYTES32_ABI, provider ) symbol = (await token.symbol()) || '' - symbol = symbol && ethers.utils.toUtf8String(symbol) + symbol = symbol && ethersUtils.toUtf8String(symbol) } } } diff --git a/src/index.js b/src/index.js index 596be89..128d985 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,5 @@ +import { knownFunctions } from './data' + /** * @typedef {Object} Binding * @property {string} type The type of the binding (a valid Radspec type) @@ -10,7 +12,7 @@ /** * @module radspec */ -import { ethers } from 'ethers' +import { utils as ethersUtils } from 'ethers' import { defaultHelpers } from './helpers' import { evaluateRaw } from './lib' @@ -38,13 +40,14 @@ import { evaluateRaw } from './lib' * @param {string} call.transaction.to The destination address for this transaction * @param {string} call.transaction.data The transaction data * @param {?Object} options An options object - * @param {?ethers.providers.Provider} options.provider EIP 1193 provider + * @param {?ethersProvider.Provider} options.provider EIP 1193 provider * @param {?Object} options.userHelpers User defined helpers + * @param {?Object} options.userFunctions User defined function signatures * @return {Promise} The result of the evaluation */ -function evaluate (source, call, { userHelpers = {}, ...options } = {}) { +function evaluate (source, call, { userHelpers = {}, userFunctions = {}, ...options } = {}) { // Create ethers interface object - const ethersInterface = new ethers.utils.Interface(call.abi) + const ethersInterface = new ethersUtils.Interface(call.abi) // Parse as an ethers TransactionDescription const { args, functionFragment } = ethersInterface.parseTransaction( @@ -64,6 +67,8 @@ function evaluate (source, call, { userHelpers = {}, ...options } = {}) { const availableHelpers = { ...defaultHelpers, ...userHelpers } + const availableFunctions = { ...knownFunctions, ...userFunctions } + // Get additional options const { from, to, value, data } = call.transaction @@ -71,6 +76,7 @@ function evaluate (source, call, { userHelpers = {}, ...options } = {}) { return evaluateRaw(source, parameters, { ...options, availableHelpers, + availableFunctions, from, to, value, diff --git a/test/examples/examples.js b/test/examples/examples.js index 638f795..a4968c0 100644 --- a/test/examples/examples.js +++ b/test/examples/examples.js @@ -1,10 +1,10 @@ import test from 'ava' -import { BigNumber, ethers } from 'ethers' +import { BigNumber, utils as ethersUtils } from 'ethers' +import { knownFunctions } from '../../src/data' import { evaluateRaw } from '../../src/lib' import { defaultHelpers } from '../../src/helpers' import { tenPow } from '../../src/helpers/lib/formatBN' import { ETH } from '../../src/helpers/lib/token' -import knownFunctions from '../../src/data/knownFunctions' const int = (value) => ({ type: 'int256', @@ -250,7 +250,7 @@ const dataDecodeCases = [ source: 'Melonprotocol: `@radspec(addr, data)`', bindings: { addr: address(), - data: bytes('0x18e467f700000000000000000000000ec67005c4e498ec7f55e092bd1d35cbc47c918920000000000000000000000000000000000000000000000000000000000000001') // registerVersion(address,bytes32), on melonprotocol's knownFunctions + data: bytes('0x18e467f7000000000000000000000000ec67005c4e498ec7f55e092bd1d35cbc47c918920000000000000000000000000000000000000000000000000000000000000001') // registerVersion(address,bytes32), on melonprotocol's knownFunctions } }, 'Melonprotocol: Register new version 0xec67005c4E498Ec7f55E092bd1d35cbC47C91892'], [{ @@ -264,14 +264,14 @@ const dataDecodeCases = [ source: 'Melonprotocol: `@radspec(addr, data)`', bindings: { addr: address(), - data: bytes('0xbda5310700000000000000000000000ec67005c4e498ec7f55e092bd1d35cbc47c91892') // setPriceSource(address), on melonprotocol's knownFunctions + data: bytes('0xbda53100700000000000000000000000ec67005c4e498ec7f55e092bd1d35cbc47c91892') // setPriceSource(address), on melonprotocol's knownFunctions } }, 'Melonprotocol: Set price source to 0xec67005c4E498Ec7f55E092bd1d35cbC47C91892'], [{ source: 'Melonprotocol: `@radspec(addr, data)`', bindings: { addr: address(), - data: bytes('0x9e0a457000000000000000000000000ec67005c4e498ec7f55e092bd1d35cbc47c91892') // setMlnToken(address), on melonprotocol's knownFunctions + data: bytes('0x9e0a4570000000000000000000000000ec67005c4e498ec7f55e092bd1d35cbc47c91892') // setMlnToken(address), on melonprotocol's knownFunctions } }, 'Melonprotocol: Set Melon token to 0xec67005c4E498Ec7f55E092bd1d35cbC47C91892'], [{ @@ -621,7 +621,7 @@ const cases = [ [{ source: 'Performs a call to `@radspec(contract, msg.data)`', bindings: { contract: address('0x960b236A07cf122663c4303350609A66A7B288C0') }, - options: { data: ethers.utils.keccak256(ethers.utils.toUtf8Bytes(Object.keys(knownFunctions)[3])).slice(0, 10) } + options: { data: ethersUtils.keccak256(ethersUtils.toUtf8Bytes(Object.keys(knownFunctions)[3])).slice(0, 10) } }, `Performs a call to ${Object.values(knownFunctions)[3]}`], ...comparisonCases, @@ -631,13 +631,14 @@ const cases = [ cases.forEach(([input, expected], index) => { test(`${index} - ${input.source}`, async (t) => { - const { userHelpers } = input.options || {} + const { userHelpers, userFunctions } = input.options || {} const actual = await evaluateRaw( input.source, input.bindings, { ...input.options, - availableHelpers: { ...defaultHelpers, ...userHelpers } + availableHelpers: { ...defaultHelpers, ...userHelpers }, + availableFunctions: { ...knownFunctions, ...userFunctions } } ) t.is(