diff --git a/README.md b/README.md index 5eb2df193..b4950c6ae 100755 --- a/README.md +++ b/README.md @@ -23,5 +23,29 @@ npm run test --strategy=eth-balance npm run test --strategy=eth-balance --more=200 ``` +#### Local network test strategy +Testing using a custom network (e.g. dev testing or CI) overrides the default networks, requiring a few extra steps. + +##### 1. Create your network +- JSON-RPC endpoint accessible from script run location. +- Governance contract (e.g. your ERC20 contract) deployed and populated with test data. +- Multicall contract deployed (used by snapshot to aggregate data retrieval calls). + +##### 2. Update your networks file (e.g. `network/local.json`) +- `multicall`; Multicall contract address on your network. +- `chainId`; Chain Id matching that of your created network. +- `rpc`; connection details for your JSON-RPC endpoint. + +##### 3. Update your Strategy test data (e.g. `example.json`) +- `strategy` `address`; governance contract address from deployment on your network. +- `addresses`; test accounts created earlier when creating your network. +- `snapshot`; to an appropriate block height for your network. + +##### 4. Test +```bash +# Test default strategy (erc20-balance-of) using local.json networks file +npm run test --network=../../network/local.json +``` + ### License [MIT](LICENSE). diff --git a/network/local.json b/network/local.json new file mode 100644 index 000000000..d4757093a --- /dev/null +++ b/network/local.json @@ -0,0 +1,14 @@ +{ + "1": { + "key": "1", + "name": "In Memory Ethereum Mainnet", + "chainId": 33133, + "network": "homestead", + "multicall": "0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0", + "rpc": [ + { + "url": "http://localhost:8545/" + } + ] + } +} diff --git a/src/utils.ts b/src/utils.ts index 9426918ea..1cfda6cfb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,9 @@ import _strategies from './strategies'; import snapshot from '@snapshot-labs/snapshot.js'; +import { Contract } from "@ethersproject/contracts"; +import { Interface } from "@ethersproject/abi"; +import Multicaller from './utils/multicaller'; +const networks = require('./utils/networks'); export async function getScoresDirect( space: string, @@ -32,9 +36,37 @@ export async function getScoresDirect( } } +export async function multicall( + network: string, + provider, + abi: any[], + calls: any[], + options? +) { + const multicallAbi = [ + 'function aggregate(tuple(address target, bytes callData)[] calls) view returns (uint256 blockNumber, bytes[] returnData)' + ]; + const multi = new Contract( + networks[network].multicall, + multicallAbi, + provider + ); + const itf = new Interface(abi); + try { + const [, res] = await multi.aggregate( + calls.map((call) => [ + call[0].toLowerCase(), + itf.encodeFunctionData(call[1], call[2]) + ]), + options || {} + ); + return res.map((call, i) => itf.decodeFunctionResult(calls[i][1], call)); + } catch (e) { + return Promise.reject(e); + } +} + export const { - multicall, - Multicaller, subgraphRequest, ipfsGet, call, @@ -42,6 +74,8 @@ export const { getProvider } = snapshot.utils; +export { Multicaller }; + export default { getScoresDirect, multicall, diff --git a/src/utils/multicaller.ts b/src/utils/multicaller.ts new file mode 100644 index 000000000..03f38dc61 --- /dev/null +++ b/src/utils/multicaller.ts @@ -0,0 +1,45 @@ +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import set from 'lodash.set'; +import { multicall } from '../utils'; + +export default class Multicaller { + public network: string; + public provider: StaticJsonRpcProvider; + public abi: any[]; + public options: any = {}; + public calls: any[] = []; + public paths: any[] = []; + + constructor( + network: string, + provider: StaticJsonRpcProvider, + abi: any[], + options? + ) { + this.network = network; + this.provider = provider; + this.abi = abi; + this.options = options || {}; + } + + call(path, address, fn, params?): Multicaller { + this.calls.push([address, fn, params]); + this.paths.push(path); + return this; + } + + async execute(from?: any): Promise { + const obj = from || {}; + const result = await multicall( + this.network, + this.provider, + this.abi, + this.calls, + this.options + ); + result.forEach((r, i) => set(obj, this.paths[i], r.length > 1 ? r : r[0])); + this.calls = []; + this.paths = []; + return obj; + } +} diff --git a/src/utils/networks.ts b/src/utils/networks.ts new file mode 100644 index 000000000..30a35f305 --- /dev/null +++ b/src/utils/networks.ts @@ -0,0 +1,22 @@ +function loadNetworks() { + const networksFile = + process.env['npm_config_networks_file'] || + process.argv + .find((arg) => arg.includes('--networks-file')) + ?.split('--networks-file') + ?.pop(); + + if (networksFile === undefined) { + return require('@snapshot-labs/snapshot.js/src/networks.json'); + } else { + try { + return require(networksFile); + } catch (e) { + throw new Error('Cannot find networks file: ' + networksFile); + } + } +} + +const networks = loadNetworks(); + +module.exports = networks; diff --git a/test/index.spec.ts b/test/index.spec.ts index 981b09ef9..d0bc3aa7e 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,8 +1,8 @@ const { JsonRpcProvider } = require('@ethersproject/providers'); const { getAddress } = require('@ethersproject/address'); const snapshot = require('../').default; -const networks = require('@snapshot-labs/snapshot.js/src/networks.json'); const addresses = require('./addresses.json'); +const networks = require('../src/utils/networks'); const strategyArg = process.env['npm_config_strategy'] || @@ -22,6 +22,7 @@ const moreArg = const strategy = Object.keys(snapshot.strategies).find((s) => strategyArg == s); if (!strategy) throw 'Strategy not found'; + const example = require(`../src/strategies/${strategy}/examples.json`)[0]; function callGetScores(example) {