├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github ├── CODEOWNERS └── workflows │ ├── build-lint-test.yml │ └── security-code-scanner.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package.json ├── subproviders ├── cache.js ├── default-fixture.js ├── etherscan.js ├── fetch.js ├── filters.js ├── fixture.js ├── gasprice.js ├── hooked-wallet-ethtx.js ├── hooked-wallet.js ├── inflight-cache.js ├── infura.js ├── ipc.js ├── json-rpc-engine-middleware.js ├── nonce-tracker.js ├── provider.js ├── rpc.js ├── sanitizer.js ├── stream.js ├── subprovider.js ├── subscriptions.js ├── vm.js ├── wallet.js ├── websocket.js └── whitelist.js ├── test ├── basic.js ├── cache-utils.js ├── cache.js ├── filters.js ├── index.js ├── inflight-cache.js ├── nonce.js ├── solc.js ├── subproviders │ ├── etherscan.js │ ├── ipc.js │ ├── sanitizer.js │ └── vm.js ├── subscriptions.js ├── util │ ├── block.js │ ├── ganache.js │ ├── inject-metrics.js │ ├── mock-subprovider.js │ ├── mock_block.json │ └── passthrough.js └── wallet.js ├── util ├── assert.js ├── async.js ├── create-payload.js ├── estimate-gas.js ├── random-id.js ├── rpc-cache-utils.js ├── rpc-hex-encoding.js └── stoplight.js ├── yarn.lock └── zero.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "rules": { 10 | "no-extra-semi": "warn", 11 | "no-unused-vars": "warn", 12 | "no-console": "warn", 13 | "no-mixed-spaces-and-tabs": "warn" 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | yarn.lock linguist-generated=false 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | * @MetaMask/devs 5 | -------------------------------------------------------------------------------- /.github/workflows/build-lint-test.yml: -------------------------------------------------------------------------------- 1 | name: Build, Lint, and Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | build-lint-test: 10 | name: Build, Lint, and Test 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [16.x, 18.x, 20.x] 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js ${{ matrix.node-version }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node-version }} 21 | - run: yarn --frozen-lockfile --ignore-scripts 22 | - run: yarn build 23 | - run: yarn bundle 24 | - run: yarn lint 25 | - run: yarn test 26 | all-jobs-pass: 27 | name: All jobs pass 28 | runs-on: ubuntu-latest 29 | needs: 30 | - build-lint-test 31 | steps: 32 | - uses: actions/checkout@v2 33 | - run: echo "Great success!" 34 | -------------------------------------------------------------------------------- /.github/workflows/security-code-scanner.yml: -------------------------------------------------------------------------------- 1 | name: 'MetaMask Security Code Scanner' 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | pull_request: 7 | branches: ['main'] 8 | 9 | jobs: 10 | run-security-scan: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | actions: read 14 | contents: read 15 | security-events: write 16 | steps: 17 | - name: MetaMask Security Code Scanner 18 | uses: MetaMask/Security-Code-Scanner@main 19 | with: 20 | repo: ${{ github.repository }} 21 | paths_ignored: | 22 | .storybook/ 23 | '**/__snapshots__/' 24 | '**/*.snap' 25 | '**/*.stories.js' 26 | '**/*.stories.tsx' 27 | '**/*.test.browser.ts*' 28 | '**/*.test.js*' 29 | '**/*.test.ts*' 30 | '**/fixtures/' 31 | '**/jest.config.js' 32 | '**/jest.environment.js' 33 | '**/mocks/' 34 | '**/test*/' 35 | docs/ 36 | e2e/ 37 | merged-packages/ 38 | node_modules 39 | storybook/ 40 | test*/ 41 | rules_excluded: example 42 | project_metrics_token: ${{ secrets.SECURITY_SCAN_METRICS_TOKEN }} 43 | slack_webhook: ${{ secrets.APPSEC_BOT_SLACK_WEBHOOK }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | package-lock.json 3 | 4 | # Created by https://www.gitignore.io/api/osx,node 5 | 6 | ### OSX ### 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | 15 | # Thumbnails 16 | ._* 17 | 18 | # Files that might appear in the root of a volume 19 | .DocumentRevisions-V100 20 | .fseventsd 21 | .Spotlight-V100 22 | .TemporaryItems 23 | .Trashes 24 | .VolumeIcon.icns 25 | .idea 26 | 27 | # Directories potentially created on remote AFP share 28 | .AppleDB 29 | .AppleDesktop 30 | Network Trash Folder 31 | Temporary Items 32 | .apdisk 33 | 34 | 35 | ### Node ### 36 | # Logs 37 | logs 38 | *.log 39 | npm-debug.log* 40 | 41 | # Runtime data 42 | pids 43 | *.pid 44 | *.seed 45 | 46 | # Directory for instrumented libs generated by jscoverage/JSCover 47 | lib-cov 48 | 49 | # Coverage directory used by tools like istanbul 50 | coverage 51 | 52 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 53 | .grunt 54 | 55 | # node-waf configuration 56 | .lock-wscript 57 | 58 | # Compiled binary addons (http://nodejs.org/api/addons.html) 59 | build/Release 60 | 61 | # Dependency directory 62 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 63 | node_modules 64 | 65 | # Optional npm cache directory 66 | .npm 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetaMask/web3-provider-engine/472426ae0460e7482330d970a2a1e17c7b5b5157/.npmignore -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [17.0.1] 10 | 11 | ### Fixed 12 | 13 | - Properly return signed transaction object in `signTransaction` of `HookedWalletEthTxSubprovider` ([#465](https://github.com/MetaMask/web3-provider-engine/pull/465)) 14 | 15 | ## [16.0.8] 16 | 17 | ### Changed 18 | 19 | - Update dependencies ([#477](https://github.com/MetaMask/web3-provider-engine/pull/477)) 20 | - `eth-rpc-errors@^3.0.0->^4.0.3` 21 | - `ethereumjs-block@^1.2.2->^2.2.2` 22 | - `ethereumjs-util@^5.1.5->^7.1.5` 23 | - `ethereumjs-vm@^2.3.4->^2.6.0` 24 | - `eth-json-rpc-filters@^4.2.1->~5.0.0` 25 | - `eth-json-rpc-infura@^5.1.0` -> `@metamask/eth-json-rpc-infura@^6.0.0` 26 | - `eth-json-rpc-middleware@^6.0.0->^8.1.0` 27 | - `eth-sig-util@^1.4.2` -> `@metamask/eth-sig-util@^4.0.1` 28 | 29 | ### Fixed 30 | 31 | - Properly return signed transaction object in `signTransaction` of `HookedWalletEthTxSubprovider` ([#465](https://github.com/MetaMask/web3-provider-engine/pull/465)) 32 | 33 | ## [17.0.0] 34 | 35 | #### Changed 36 | 37 | - Add deprecation notice ([#469](https://github.com/MetaMask/web3-provider-engine/pull/469)) 38 | - **BREAKING**: Increase minimum Node.js version to 16 ([#447](https://github.com/MetaMask/web3-provider-engine/pull/447)) 39 | - Bump ethereumjs and metamask dependencies ([#453](https://github.com/MetaMask/web3-provider-engine/pull/453)) ([#471](https://github.com/MetaMask/web3-provider-engine/pull/471)) 40 | - babelify dependencies ([#454](https://github.com/MetaMask/web3-provider-engine/pull/454)) 41 | - Update dependency `readable-stream` from `^2.2.9` to `^3.6.2` ([#452](https://github.com/MetaMask/web3-provider-engine/pull/452)) 42 | - Update devDependency `browserify` from `16.5.0` to `17.0.0` ([#456](https://github.com/MetaMask/web3-provider-engine/pull/456)) 43 | 44 | ## [16.0.7] 45 | 46 | ### Fixed 47 | 48 | - Properly replace vulnerable dependency `request` with patched `@cypress/request` ([#459](https://github.com/MetaMask/web3-provider-engine/pull/459)) 49 | 50 | ## [16.0.6] 51 | 52 | ### Fixed 53 | 54 | - Replace vulnerable dependency `request` with patched `@cypress/request` ([#441](https://github.com/MetaMask/web3-provider-engine/pull/441)) 55 | - Update `ws` from `^5.1.1` to `^7.5.9` ([#446](https://github.com/MetaMask/web3-provider-engine/pull/446)) 56 | 57 | ## [16.0.5] 58 | 59 | ### Changed 60 | 61 | - Update `eth-block-tracker` to 5.0.1 to remove unintentional dependency on Babel, which produced warning locally when not installed ([#409](https://github.com/MetaMask/web3-provider-engine/pull/409)) 62 | 63 | ## [16.0.4] - 2022-04-29 64 | 65 | ### Fixed 66 | 67 | - Remove vulnerable version of `cross-fetch` ([#404](https://github.com/MetaMask/web3-provider-engine/pull/404)) 68 | 69 | ## [16.0.3] - 2021-07-15 70 | 71 | ### Changed 72 | 73 | - Remove zero prefix from address. ([#380](https://github.com/MetaMask/web3-provider-engine/pull/380)) 74 | - The previously published version `v16.0.2` (now deprecated) included an upgrade that didn't take into account that `tx.getSenderAddress().toString('hex')` now _includes_ the leading `0x` prefix. 75 | 76 | ## [16.0.2] - 2021-07-14 77 | 78 | ### Changed 79 | 80 | - Update `ethereumjs-tx` to `@ethereumjs/tx` to support EIP1559 transactions ([#356](https://github.com/MetaMask/web3-provider-engine/pull/377)) 81 | 82 | ## [16.0.1] - 2020-09-23 83 | 84 | ### Changed 85 | 86 | - Fix broken publish files ([#356](https://github.com/MetaMask/web3-provider-engine/pull/356)) 87 | 88 | ## [16.0.0] - 2020-09-22 89 | 90 | ### Changed 91 | 92 | - **Breaking:** Use Infura V3 API ([#352](https://github.com/MetaMask/web3-provider-engine/pull/352)) 93 | - The previously used Infura API is deprecated and will be (or is already) removed. 94 | - Using the Infura Provider now requires an API key. 95 | See [`eth-json-rpc-infura`](https://npmjs.com/package/eth-json-rpc-infura) and [infura.io](https://infura.io) for details. 96 | - Update various dependencies 97 | - eth-json-rpc-middleware@6.0.0 ([#350](https://github.com/MetaMask/web3-provider-engine/pull/350)) 98 | - eth-json-rpc-filters@4.2.1 ([#351](https://github.com/MetaMask/web3-provider-engine/pull/351)) 99 | - eth-json-rpc-infura@5.1.0 ([#352](https://github.com/MetaMask/web3-provider-engine/pull/352)) 100 | - eth-rpc-errors@3.0.0 ([#353](https://github.com/MetaMask/web3-provider-engine/pull/353)) 101 | - Specify publish files 102 | 103 | ## [15.0.0] 104 | 105 | ### Changed 106 | 107 | - uses eth-block-tracker@4, but still provides block body on ('block', 'latest', and 'rawBlock'). Other events ('sync') provide block number hex string instead of block body. 108 | - SubscriptionsSubprovider automatically forwards events to provider 109 | - replacing subprovider implementations with those in [`eth-json-rpc-engine`](https://github.com/MetaMask/eth-json-rpc-middleware) 110 | - browserify: moved to `babelify@10` + `@babel/core@7` 111 | 112 | ## [14.0.0] 113 | 114 | ### Changed 115 | 116 | - default dataProvider for zero is Infura mainnet REST api 117 | - websocket support 118 | - subscriptions support 119 | - remove solc subprovider 120 | - removed `dist` from git (but published in npm module) 121 | - es5 builds in `dist/es5` 122 | - zero + ProviderEngine bundles are es5 123 | - web3 subprovider renamed to provider subprovider 124 | - error if provider subprovider is missing a proper provider 125 | - removed need to supply getAccounts hook 126 | - fixed `hooked-wallet-ethtx` message signing 127 | - fixed `hooked-wallet` default txParams 128 | 129 | ## [13.0.0] 130 | 131 | ### Changed 132 | 133 | - txs included in blocks via [`eth-block-tracker`](https://github.com/kumavis/eth-block-tracker)@2.0.0 134 | 135 | ## [12.0.0] 136 | 137 | ### Changed 138 | 139 | - moved block polling to [`eth-block-tracker`](https://github.com/kumavis/eth-block-tracker). 140 | 141 | ## [11.0.0] 142 | 143 | ### Changed 144 | 145 | - zero.js - replaced http subprovider with fetch provider (includes polyfill for node). 146 | 147 | ## [10.0.0] 148 | 149 | ### Changed 150 | 151 | - renamed HookedWalletSubprovider `personalRecoverSigner` to `recoverPersonalSignature` 152 | 153 | ## [9.0.0] 154 | 155 | ### Changed 156 | 157 | - `pollingShouldUnref` option now defaults to false 158 | 159 | [Unreleased]:https://github.com/MetaMask/web3-provider-engine/compare/v16.0.1...HEAD 160 | [16.0.1]:https://github.com/MetaMask/web3-provider-engine/compare/v16.0.0...v16.0.1 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 MetaMask 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web3 ProviderEngine 2 | 3 | Web3 ProviderEngine is a tool for composing your own [web3 providers](https://github.com/ethereum/wiki/wiki/JavaScript-API#web3). 4 | 5 | > [!CAUTION] 6 | > This package has been deprecated. 7 | > 8 | > This package was originally created for MetaMask, but has been replaced by `@metamask/json-rpc-engine`, `@metamask/eth-json-rpc-middleware`, `@metamask/eth-json-rpc-provider`, and various other packages. 9 | > 10 | > Here is an example of how to create a provider using those packages: 11 | > 12 | > ```javascript 13 | > import { providerFromMiddleware } from '@metamask/eth-json-rpc-provider'; 14 | > import { createFetchMiddleware } from '@metamask/eth-json-rpc-middleware'; 15 | > import { valueToBytes, bytesToBase64 } from '@metamask/utils'; 16 | > import fetch from 'cross-fetch'; 17 | > 18 | > const rpcUrl = '[insert RPC URL here]'; 19 | > 20 | > const fetchMiddleware = createFetchMiddleware({ 21 | > btoa: (stringToEncode) => bytesToBase64(valueToBytes(stringToEncode)), 22 | > fetch, 23 | > rpcUrl, 24 | > }); 25 | > const provider = providerFromMiddleware(fetchMiddleware); 26 | > 27 | > provider.sendAsync( 28 | > { id: 1, jsonrpc: '2.0', method: 'eth_chainId' }, 29 | > (error, response) => { 30 | > if (error) { 31 | > console.error(error); 32 | > } else { 33 | > console.log(response.result); 34 | > } 35 | > } 36 | > ); 37 | > ``` 38 | > 39 | > This example was written with v12.1.0 of `@metamask/eth-json-rpc-middleware`, v3.0.1 of `@metamask/eth-json-rpc-provider`, and v8.4.0 of `@metamask/utils`. 40 | > 41 | 42 | 43 | ### Composable 44 | 45 | Built to be modular - works via a stack of 'sub-providers' which are like normal web3 providers but only handle a subset of rpc methods. 46 | 47 | The subproviders can emit new rpc requests in order to handle their own; e.g. `eth_call` may trigger `eth_getAccountBalance`, `eth_getCode`, and others. 48 | The provider engine also handles caching of rpc request results. 49 | 50 | ```js 51 | const ProviderEngine = require('web3-provider-engine') 52 | const CacheSubprovider = require('web3-provider-engine/subproviders/cache.js') 53 | const FixtureSubprovider = require('web3-provider-engine/subproviders/fixture.js') 54 | const FilterSubprovider = require('web3-provider-engine/subproviders/filters.js') 55 | const VmSubprovider = require('web3-provider-engine/subproviders/vm.js') 56 | const HookedWalletSubprovider = require('web3-provider-engine/subproviders/hooked-wallet.js') 57 | const NonceSubprovider = require('web3-provider-engine/subproviders/nonce-tracker.js') 58 | const RpcSubprovider = require('web3-provider-engine/subproviders/rpc.js') 59 | 60 | var engine = new ProviderEngine() 61 | var web3 = new Web3(engine) 62 | 63 | // static results 64 | engine.addProvider(new FixtureSubprovider({ 65 | web3_clientVersion: 'ProviderEngine/v0.0.0/javascript', 66 | net_listening: true, 67 | eth_hashrate: '0x00', 68 | eth_mining: false, 69 | eth_syncing: true, 70 | })) 71 | 72 | // cache layer 73 | engine.addProvider(new CacheSubprovider()) 74 | 75 | // filters 76 | engine.addProvider(new FilterSubprovider()) 77 | 78 | // pending nonce 79 | engine.addProvider(new NonceSubprovider()) 80 | 81 | // vm 82 | engine.addProvider(new VmSubprovider()) 83 | 84 | // id mgmt 85 | engine.addProvider(new HookedWalletSubprovider({ 86 | getAccounts: function(cb){ ... }, 87 | approveTransaction: function(cb){ ... }, 88 | signTransaction: function(cb){ ... }, 89 | })) 90 | 91 | // data source 92 | engine.addProvider(new RpcSubprovider({ 93 | rpcUrl: 'https://testrpc.metamask.io/', 94 | })) 95 | 96 | // log new blocks 97 | engine.on('block', function(block){ 98 | console.log('================================') 99 | console.log('BLOCK CHANGED:', '#'+block.number.toString('hex'), '0x'+block.hash.toString('hex')) 100 | console.log('================================') 101 | }) 102 | 103 | // network connectivity error 104 | engine.on('error', function(err){ 105 | // report connectivity errors 106 | console.error(err.stack) 107 | }) 108 | 109 | // start polling for blocks 110 | engine.start() 111 | ``` 112 | 113 | When importing in webpack: 114 | ```js 115 | import * as Web3ProviderEngine from 'web3-provider-engine'; 116 | import * as RpcSource from 'web3-provider-engine/subproviders/rpc'; 117 | import * as HookedWalletSubprovider from 'web3-provider-engine/subproviders/hooked-wallet'; 118 | ``` 119 | 120 | ### Built For Zero-Clients 121 | 122 | The [Ethereum JSON RPC](https://github.com/ethereum/wiki/wiki/JSON-RPC) was not designed to have one node service many clients. 123 | However a smaller, lighter subset of the JSON RPC can be used to provide the blockchain data that an Ethereum 'zero-client' node would need to function. 124 | We handle as many types of requests locally as possible, and just let data lookups fallback to some data source ( hosted rpc, blockchain api, etc ). 125 | Categorically, we don’t want / can’t have the following types of RPC calls go to the network: 126 | * id mgmt + tx signing (requires private data) 127 | * filters (requires a stateful data api) 128 | * vm (expensive, hard to scale) 129 | 130 | ## Running tests 131 | 132 | ```bash 133 | yarn test 134 | ``` 135 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter 2 | const inherits = require('util').inherits 3 | const ethUtil = require('@ethereumjs/util') 4 | const { PollingBlockTracker } = require('eth-block-tracker') 5 | const map = require('async/map') 6 | const eachSeries = require('async/eachSeries') 7 | const Stoplight = require('./util/stoplight.js') 8 | const cacheUtils = require('./util/rpc-cache-utils.js') 9 | const createPayload = require('./util/create-payload.js') 10 | const noop = function(){} 11 | 12 | module.exports = Web3ProviderEngine 13 | 14 | 15 | inherits(Web3ProviderEngine, EventEmitter) 16 | 17 | function Web3ProviderEngine(opts) { 18 | const self = this 19 | EventEmitter.call(self) 20 | self.setMaxListeners(30) 21 | // parse options 22 | opts = opts || {} 23 | 24 | // block polling 25 | const directProvider = { sendAsync: self._handleAsync.bind(self) } 26 | const blockTrackerProvider = opts.blockTrackerProvider || directProvider 27 | self._blockTracker = opts.blockTracker || new PollingBlockTracker({ 28 | provider: blockTrackerProvider, 29 | pollingInterval: opts.pollingInterval || 4000, 30 | setSkipCacheFlag: true, 31 | }) 32 | 33 | // set initialization blocker 34 | self._ready = new Stoplight() 35 | 36 | // local state 37 | self.currentBlock = null 38 | self._providers = [] 39 | } 40 | 41 | // public 42 | 43 | Web3ProviderEngine.prototype.start = function(cb = noop){ 44 | const self = this 45 | 46 | // trigger start 47 | self._ready.go() 48 | 49 | // on new block, request block body and emit as events 50 | self._blockTracker.on('latest', (blockNumber) => { 51 | // get block body 52 | self._getBlockByNumberWithRetry(blockNumber, (err, block) => { 53 | if (err) { 54 | this.emit('error', err) 55 | return 56 | } 57 | if (!block) { 58 | console.log(block) 59 | this.emit('error', new Error("Could not find block")) 60 | return 61 | } 62 | const bufferBlock = toBufferBlock(block) 63 | // set current + emit "block" event 64 | self._setCurrentBlock(bufferBlock) 65 | // emit other events 66 | self.emit('rawBlock', block) 67 | self.emit('latest', block) 68 | }) 69 | }) 70 | 71 | // forward other events 72 | self._blockTracker.on('sync', self.emit.bind(self, 'sync')) 73 | self._blockTracker.on('error', self.emit.bind(self, 'error')) 74 | 75 | // update state 76 | self._running = true 77 | // signal that we started 78 | self.emit('start') 79 | } 80 | 81 | Web3ProviderEngine.prototype.stop = function(){ 82 | const self = this 83 | // stop block polling by removing event listeners 84 | self._blockTracker.removeAllListeners() 85 | // update state 86 | self._running = false 87 | // signal that we stopped 88 | self.emit('stop') 89 | } 90 | 91 | Web3ProviderEngine.prototype.isRunning = function(){ 92 | const self = this 93 | return self._running 94 | } 95 | 96 | Web3ProviderEngine.prototype.addProvider = function(source, index){ 97 | const self = this 98 | if (typeof index === 'number') { 99 | self._providers.splice(index, 0, source) 100 | } else { 101 | self._providers.push(source) 102 | } 103 | source.setEngine(this) 104 | } 105 | 106 | Web3ProviderEngine.prototype.removeProvider = function(source){ 107 | const self = this 108 | const index = self._providers.indexOf(source) 109 | if (index < 0) throw new Error('Provider not found.') 110 | self._providers.splice(index, 1) 111 | } 112 | 113 | Web3ProviderEngine.prototype.send = function(payload){ 114 | throw new Error('Web3ProviderEngine does not support synchronous requests.') 115 | } 116 | 117 | Web3ProviderEngine.prototype.sendAsync = function(payload, cb){ 118 | const self = this 119 | self._ready.await(function(){ 120 | 121 | if (Array.isArray(payload)) { 122 | // handle batch 123 | map(payload, self._handleAsync.bind(self), cb) 124 | } else { 125 | // handle single 126 | self._handleAsync(payload, cb) 127 | } 128 | 129 | }) 130 | } 131 | 132 | // private 133 | 134 | Web3ProviderEngine.prototype._getBlockByNumberWithRetry = function(blockNumber, cb) { 135 | const self = this 136 | 137 | let retriesRemaining = 5 138 | 139 | attemptRequest() 140 | return 141 | 142 | function attemptRequest () { 143 | self._getBlockByNumber(blockNumber, afterRequest) 144 | } 145 | 146 | function afterRequest (err, block) { 147 | // anomalous error occurred 148 | if (err) return cb(err) 149 | // block not ready yet 150 | if (!block) { 151 | if (retriesRemaining > 0) { 152 | // wait 1s then try again 153 | retriesRemaining-- 154 | setTimeout(function () { 155 | attemptRequest() 156 | }, 1000) 157 | return 158 | } else { 159 | // give up, return a null block 160 | cb(null, null) 161 | return 162 | } 163 | } 164 | // otherwise return result 165 | cb(null, block) 166 | return 167 | } 168 | } 169 | 170 | 171 | Web3ProviderEngine.prototype._getBlockByNumber = function(blockNumber, cb) { 172 | const req = createPayload({ method: 'eth_getBlockByNumber', params: [blockNumber, false], skipCache: true }) 173 | this._handleAsync(req, (err, res) => { 174 | if (err) return cb(err) 175 | return cb(null, res.result) 176 | }) 177 | } 178 | 179 | Web3ProviderEngine.prototype._handleAsync = function(payload, finished) { 180 | var self = this 181 | var currentProvider = -1 182 | var result = null 183 | var error = null 184 | 185 | var stack = [] 186 | 187 | next() 188 | 189 | function next(after) { 190 | currentProvider += 1 191 | stack.unshift(after) 192 | 193 | // Bubbled down as far as we could go, and the request wasn't 194 | // handled. Return an error. 195 | if (currentProvider >= self._providers.length) { 196 | end(new Error('Request for method "' + payload.method + '" not handled by any subprovider. Please check your subprovider configuration to ensure this method is handled.')) 197 | } else { 198 | try { 199 | var provider = self._providers[currentProvider] 200 | provider.handleRequest(payload, next, end) 201 | } catch (e) { 202 | end(e) 203 | } 204 | } 205 | } 206 | 207 | function end(_error, _result) { 208 | error = _error 209 | result = _result 210 | 211 | eachSeries(stack, function(fn, callback) { 212 | 213 | if (fn) { 214 | fn(error, result, callback) 215 | } else { 216 | callback() 217 | } 218 | }, function() { 219 | 220 | var resultObj = { 221 | id: payload.id, 222 | jsonrpc: payload.jsonrpc, 223 | result: result 224 | } 225 | 226 | if (error != null) { 227 | resultObj.error = { 228 | message: error.stack || error.message || error, 229 | code: -32000 230 | } 231 | // respond with both error formats 232 | finished(error, resultObj) 233 | } else { 234 | finished(null, resultObj) 235 | } 236 | }) 237 | } 238 | } 239 | 240 | // 241 | // from remote-data 242 | // 243 | 244 | Web3ProviderEngine.prototype._setCurrentBlock = function(block){ 245 | const self = this 246 | self.currentBlock = block 247 | self.emit('block', block) 248 | } 249 | 250 | // util 251 | 252 | function toBufferBlock (jsonBlock) { 253 | return { 254 | number: ethUtil.toBuffer(jsonBlock.number), 255 | hash: ethUtil.toBuffer(jsonBlock.hash), 256 | parentHash: ethUtil.toBuffer(jsonBlock.parentHash), 257 | nonce: ethUtil.toBuffer(jsonBlock.nonce), 258 | mixHash: ethUtil.toBuffer(jsonBlock.mixHash), 259 | sha3Uncles: ethUtil.toBuffer(jsonBlock.sha3Uncles), 260 | logsBloom: ethUtil.toBuffer(jsonBlock.logsBloom), 261 | transactionsRoot: ethUtil.toBuffer(jsonBlock.transactionsRoot), 262 | stateRoot: ethUtil.toBuffer(jsonBlock.stateRoot), 263 | receiptsRoot: ethUtil.toBuffer(jsonBlock.receiptRoot || jsonBlock.receiptsRoot), 264 | miner: ethUtil.toBuffer(jsonBlock.miner), 265 | difficulty: ethUtil.toBuffer(jsonBlock.difficulty), 266 | totalDifficulty: ethUtil.toBuffer(jsonBlock.totalDifficulty), 267 | size: ethUtil.toBuffer(jsonBlock.size), 268 | extraData: ethUtil.toBuffer(jsonBlock.extraData), 269 | gasLimit: ethUtil.toBuffer(jsonBlock.gasLimit), 270 | gasUsed: ethUtil.toBuffer(jsonBlock.gasUsed), 271 | timestamp: ethUtil.toBuffer(jsonBlock.timestamp), 272 | transactions: jsonBlock.transactions, 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web3-provider-engine", 3 | "version": "17.0.1", 4 | "description": "A JavaScript library for composing Ethereum provider objects using middleware modules", 5 | "repository": "https://github.com/MetaMask/web3-provider-engine", 6 | "main": "index.js", 7 | "engines": { 8 | "node": "^16.20 || ^18.16 || >=20" 9 | }, 10 | "scripts": { 11 | "test": "node test/index.js && yarn lint", 12 | "prepublishOnly": "yarn build && yarn bundle", 13 | "build": "babel zero.js index.js -d dist/es5 && babel subproviders -d dist/es5/subproviders && babel util -d dist/es5/util", 14 | "bundle": "mkdir -p ./dist && yarn bundle-engine && yarn bundle-zero", 15 | "bundle-zero": "browserify -s ZeroClientProvider -e zero.js -t [ babelify --presets [ @babel/preset-env ] --global true --ignore [ 'node_modules/@babel' 'node_modules/core-js-pure' 'node_modules/core-js' ] ] > dist/ZeroClientProvider.js", 16 | "bundle-engine": "browserify -s ProviderEngine -e index.js -t [ babelify --presets [ @babel/preset-env ] --global true --ignore [ 'node_modules/@babel' 'node_modules/core-js-pure' 'node_modules/core-js' ] ] > dist/ProviderEngine.js", 17 | "lint": "eslint --quiet --ignore-path .gitignore ." 18 | }, 19 | "files": [ 20 | "*.js", 21 | "dist", 22 | "subproviders", 23 | "util" 24 | ], 25 | "license": "MIT", 26 | "dependencies": { 27 | "@cypress/request": "^3.0.1", 28 | "@ethereumjs/statemanager": "^1.1.0", 29 | "@ethereumjs/block": "^4.3.0", 30 | "@ethereumjs/tx": "^4.2.0", 31 | "@ethereumjs/vm": "^6.5.0", 32 | "@ethereumjs/util": "^8.1.0", 33 | "@metamask/eth-json-rpc-filters": "^7.0.0", 34 | "@metamask/eth-json-rpc-infura": "^9.1.0", 35 | "@metamask/eth-json-rpc-middleware": "^12.1.0", 36 | "@metamask/eth-sig-util": "^7.0.1", 37 | "@metamask/rpc-errors": "^6.2.1", 38 | "async": "^2.6.4", 39 | "backoff": "^2.5.0", 40 | "clone": "^2.1.2", 41 | "eth-block-tracker": "^8.1.0", 42 | "json-stable-stringify": "^1.1.1", 43 | "promise-to-callback": "^1.0.0", 44 | "readable-stream": "^3.6.2", 45 | "semaphore": "^1.1.0", 46 | "ws": "^7.5.9", 47 | "xhr": "^2.6.0", 48 | "xtend": "^4.0.2" 49 | }, 50 | "devDependencies": { 51 | "@babel/cli": "^7.23.0", 52 | "@babel/core": "^7.23.0", 53 | "@babel/preset-env": "^7.23.2", 54 | "@metamask/ethjs": "^0.6.0", 55 | "babelify": "^10.0.0", 56 | "browserify": "^17.0.0", 57 | "eslint": "^6.2.0", 58 | "ethereum-cryptography": "^2.1.2", 59 | "ganache": "^7.9.2", 60 | "tape": "^5.7.1" 61 | }, 62 | "browser": { 63 | "request": false, 64 | "ws": false 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /subproviders/cache.js: -------------------------------------------------------------------------------- 1 | const ProviderSubprovider = require('./json-rpc-engine-middleware') 2 | const { createBlockCacheMiddleware } = require('@metamask/eth-json-rpc-middleware') 3 | 4 | class BlockCacheSubprovider extends ProviderSubprovider { 5 | constructor(opts) { 6 | super(({ blockTracker }) => createBlockCacheMiddleware(Object.assign({ blockTracker }, opts))) 7 | } 8 | } 9 | 10 | module.exports = BlockCacheSubprovider 11 | -------------------------------------------------------------------------------- /subproviders/default-fixture.js: -------------------------------------------------------------------------------- 1 | const inherits = require('util').inherits 2 | const extend = require('xtend') 3 | const FixtureProvider = require('./fixture.js') 4 | const version = require('../package.json').version 5 | 6 | module.exports = DefaultFixtures 7 | 8 | inherits(DefaultFixtures, FixtureProvider) 9 | 10 | function DefaultFixtures(opts) { 11 | const self = this 12 | opts = opts || {} 13 | var responses = extend({ 14 | web3_clientVersion: 'ProviderEngine/v'+version+'/javascript', 15 | net_listening: true, 16 | eth_hashrate: '0x00', 17 | eth_mining: false, 18 | }, opts) 19 | FixtureProvider.call(self, responses) 20 | } 21 | -------------------------------------------------------------------------------- /subproviders/etherscan.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Etherscan.io API connector 3 | * @author github.com/axic 4 | * 5 | * The etherscan.io API supports: 6 | * 7 | * 1) Natively via proxy methods 8 | * - eth_blockNumber * 9 | * - eth_getBlockByNumber * 10 | * - eth_getBlockTransactionCountByNumber 11 | * - getTransactionByHash 12 | * - getTransactionByBlockNumberAndIndex 13 | * - eth_getTransactionCount * 14 | * - eth_sendRawTransaction * 15 | * - eth_call * 16 | * - eth_getTransactionReceipt * 17 | * - eth_getCode * 18 | * - eth_getStorageAt * 19 | * 20 | * 2) Via non-native methods 21 | * - eth_getBalance 22 | * - eth_listTransactions (non-standard) 23 | */ 24 | 25 | const xhr = process.browser ? require('xhr') : require('@cypress/request') 26 | const inherits = require('util').inherits 27 | const Subprovider = require('./subprovider.js') 28 | 29 | module.exports = EtherscanProvider 30 | 31 | inherits(EtherscanProvider, Subprovider) 32 | 33 | function EtherscanProvider(opts) { 34 | opts = opts || {} 35 | this.network = opts.network || 'api' 36 | this.proto = (opts.https || false) ? 'https' : 'http' 37 | this.requests = []; 38 | this.times = isNaN(opts.times) ? 4 : opts.times; 39 | this.interval = isNaN(opts.interval) ? 1000 : opts.interval; 40 | this.retryFailed = typeof opts.retryFailed === 'boolean' ? opts.retryFailed : true; // not built yet 41 | 42 | setInterval(this.handleRequests, this.interval, this); 43 | } 44 | 45 | EtherscanProvider.prototype.handleRequests = function(self){ 46 | if(self.requests.length == 0) return; 47 | 48 | //console.log('Handling the next ' + self.times + ' of ' + self.requests.length + ' requests'); 49 | 50 | for(var requestIndex = 0; requestIndex < self.times; requestIndex++) { 51 | var requestItem = self.requests.shift() 52 | 53 | if(typeof requestItem !== 'undefined') 54 | handlePayload(requestItem.proto, requestItem.network, requestItem.payload, requestItem.next, requestItem.end) 55 | } 56 | } 57 | 58 | EtherscanProvider.prototype.handleRequest = function(payload, next, end){ 59 | var requestObject = {proto: this.proto, network: this.network, payload: payload, next: next, end: end}, 60 | self = this; 61 | 62 | if(this.retryFailed) 63 | requestObject.end = function(err, result){ 64 | if(err === '403 - Forbidden: Access is denied.') 65 | self.requests.push(requestObject); 66 | else 67 | end(err, result); 68 | }; 69 | 70 | this.requests.push(requestObject); 71 | } 72 | 73 | function handlePayload(proto, network, payload, next, end){ 74 | switch(payload.method) { 75 | case 'eth_blockNumber': 76 | etherscanXHR(true, proto, network, 'proxy', 'eth_blockNumber', {}, end) 77 | return 78 | 79 | case 'eth_getBlockByNumber': 80 | etherscanXHR(true, proto, network, 'proxy', 'eth_getBlockByNumber', { 81 | tag: payload.params[0], 82 | boolean: payload.params[1] }, end) 83 | return 84 | 85 | case 'eth_getBlockTransactionCountByNumber': 86 | etherscanXHR(true, proto, network, 'proxy', 'eth_getBlockTransactionCountByNumber', { 87 | tag: payload.params[0] 88 | }, end) 89 | return 90 | 91 | case 'eth_getTransactionByHash': 92 | etherscanXHR(true, proto, network, 'proxy', 'eth_getTransactionByHash', { 93 | txhash: payload.params[0] 94 | }, end) 95 | return 96 | 97 | case 'eth_getBalance': 98 | etherscanXHR(true, proto, network, 'account', 'balance', { 99 | address: payload.params[0], 100 | tag: payload.params[1] }, end) 101 | return 102 | 103 | case 'eth_listTransactions': 104 | return (function(){ 105 | const props = [ 106 | 'address', 107 | 'startblock', 108 | 'endblock', 109 | 'sort', 110 | 'page', 111 | 'offset' 112 | ] 113 | 114 | const params = {} 115 | for (let i = 0, l = Math.min(payload.params.length, props.length); i < l; i++) { 116 | params[props[i]] = payload.params[i] 117 | } 118 | 119 | etherscanXHR(true, proto, network, 'account', 'txlist', params, end) 120 | })() 121 | 122 | case 'eth_call': 123 | etherscanXHR(true, proto, network, 'proxy', 'eth_call', payload.params[0], end) 124 | return 125 | 126 | case 'eth_sendRawTransaction': 127 | etherscanXHR(false, proto, network, 'proxy', 'eth_sendRawTransaction', { hex: payload.params[0] }, end) 128 | return 129 | 130 | case 'eth_getTransactionReceipt': 131 | etherscanXHR(true, proto, network, 'proxy', 'eth_getTransactionReceipt', { txhash: payload.params[0] }, end) 132 | return 133 | 134 | // note !! this does not support topic filtering yet, it will return all block logs 135 | case 'eth_getLogs': 136 | return (function(){ 137 | var payloadObject = payload.params[0], 138 | txProcessed = 0, 139 | logs = []; 140 | 141 | etherscanXHR(true, proto, network, 'proxy', 'eth_getBlockByNumber', { 142 | tag: payloadObject.toBlock, 143 | boolean: payload.params[1] }, function(err, blockResult) { 144 | if(err) return end(err); 145 | 146 | for(var transaction in blockResult.transactions){ 147 | etherscanXHR(true, proto, network, 'proxy', 'eth_getTransactionReceipt', { txhash: transaction.hash }, function(err, receiptResult) { 148 | if(!err) logs.concat(receiptResult.logs); 149 | txProcessed += 1; 150 | if(txProcessed === blockResult.transactions.length) end(null, logs) 151 | }) 152 | } 153 | }) 154 | })() 155 | 156 | case 'eth_getTransactionCount': 157 | etherscanXHR(true, proto, network, 'proxy', 'eth_getTransactionCount', { 158 | address: payload.params[0], 159 | tag: payload.params[1] 160 | }, end) 161 | return 162 | 163 | case 'eth_getCode': 164 | etherscanXHR(true, proto, network, 'proxy', 'eth_getCode', { 165 | address: payload.params[0], 166 | tag: payload.params[1] 167 | }, end) 168 | return 169 | 170 | case 'eth_getStorageAt': 171 | etherscanXHR(true, proto, network, 'proxy', 'eth_getStorageAt', { 172 | address: payload.params[0], 173 | position: payload.params[1], 174 | tag: payload.params[2] 175 | }, end) 176 | return 177 | 178 | default: 179 | next(); 180 | return 181 | } 182 | } 183 | 184 | function toQueryString(params) { 185 | return Object.keys(params).map(function(k) { 186 | return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]) 187 | }).join('&') 188 | } 189 | 190 | function etherscanXHR(useGetMethod, proto, network, module, action, params, end) { 191 | var uri = proto + '://' + network + '.etherscan.io/api?' + toQueryString({ module: module, action: action }) + '&' + toQueryString(params) 192 | 193 | xhr({ 194 | uri: uri, 195 | method: useGetMethod ? 'GET' : 'POST', 196 | headers: { 197 | 'Accept': 'application/json', 198 | // 'Content-Type': 'application/json', 199 | }, 200 | rejectUnauthorized: false, 201 | }, function(err, res, body) { 202 | // console.log('[etherscan] response: ', err) 203 | 204 | if (err) return end(err) 205 | 206 | /*console.log('[etherscan request]' 207 | + ' method: ' + useGetMethod 208 | + ' proto: ' + proto 209 | + ' network: ' + network 210 | + ' module: ' + module 211 | + ' action: ' + action 212 | + ' params: ' + params 213 | + ' return body: ' + body);*/ 214 | 215 | if(body.indexOf('403 - Forbidden: Access is denied.') > -1) 216 | return end('403 - Forbidden: Access is denied.') 217 | 218 | var data 219 | try { 220 | data = JSON.parse(body) 221 | } catch (err) { 222 | console.error(err.stack) 223 | return end(err) 224 | } 225 | 226 | // console.log('[etherscan] response decoded: ', data) 227 | 228 | // NOTE: or use id === -1? (id=1 is 'success') 229 | if ((module === 'proxy') && data.error) { 230 | // Maybe send back the code too? 231 | return end(data.error.message) 232 | } 233 | 234 | // NOTE: or data.status !== 1? 235 | if ((module === 'account') && (data.message !== 'OK')) { 236 | return end(data.message) 237 | } 238 | 239 | end(null, data.result) 240 | }) 241 | } 242 | -------------------------------------------------------------------------------- /subproviders/fetch.js: -------------------------------------------------------------------------------- 1 | const ProviderSubprovider = require('./json-rpc-engine-middleware') 2 | const { createFetchMiddleware } = require('@metamask/eth-json-rpc-middleware') 3 | 4 | class FetchSubprovider extends ProviderSubprovider { 5 | constructor(opts) { 6 | super(({ blockTracker, provider, engine }) => { 7 | return createFetchMiddleware(opts) 8 | }) 9 | } 10 | } 11 | 12 | module.exports = FetchSubprovider 13 | -------------------------------------------------------------------------------- /subproviders/filters.js: -------------------------------------------------------------------------------- 1 | const ProviderSubprovider = require('./json-rpc-engine-middleware') 2 | const createFilterMiddleware = require('@metamask/eth-json-rpc-filters') 3 | 4 | class FiltersSubprovider extends ProviderSubprovider { 5 | constructor() { 6 | super(({ blockTracker, provider }) => { 7 | return createFilterMiddleware({ blockTracker, provider }) 8 | }) 9 | } 10 | } 11 | 12 | module.exports = FiltersSubprovider 13 | -------------------------------------------------------------------------------- /subproviders/fixture.js: -------------------------------------------------------------------------------- 1 | const inherits = require('util').inherits 2 | const Subprovider = require('./subprovider.js') 3 | 4 | module.exports = FixtureProvider 5 | 6 | inherits(FixtureProvider, Subprovider) 7 | 8 | function FixtureProvider(staticResponses){ 9 | const self = this 10 | staticResponses = staticResponses || {} 11 | self.staticResponses = staticResponses 12 | } 13 | 14 | FixtureProvider.prototype.handleRequest = function(payload, next, end){ 15 | const self = this 16 | var staticResponse = self.staticResponses[payload.method] 17 | // async function 18 | if ('function' === typeof staticResponse) { 19 | staticResponse(payload, next, end) 20 | // static response - null is valid response 21 | } else if (staticResponse !== undefined) { 22 | // return result asynchronously 23 | setTimeout(() => end(null, staticResponse)) 24 | // no prepared response - skip 25 | } else { 26 | next() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /subproviders/gasprice.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Calculate gasPrice based on last blocks. 3 | * @author github.com/axic 4 | * 5 | * FIXME: support minimum suggested gas and perhaps other options from geth: 6 | * https://github.com/ethereum/go-ethereum/blob/master/eth/gasprice.go 7 | * https://github.com/ethereum/go-ethereum/wiki/Gas-Price-Oracle 8 | */ 9 | 10 | const map = require('async/map') 11 | const inherits = require('util').inherits 12 | const Subprovider = require('./subprovider.js') 13 | 14 | module.exports = GaspriceProvider 15 | 16 | inherits(GaspriceProvider, Subprovider) 17 | 18 | function GaspriceProvider(opts) { 19 | opts = opts || {} 20 | this.numberOfBlocks = opts.numberOfBlocks || 10 21 | this.delayInBlocks = opts.delayInBlocks || 5 22 | } 23 | 24 | GaspriceProvider.prototype.handleRequest = function(payload, next, end){ 25 | if (payload.method !== 'eth_gasPrice') 26 | return next() 27 | 28 | const self = this 29 | 30 | self.emitPayload({ method: 'eth_blockNumber' }, function(err, res) { 31 | // FIXME: convert number using a bignum library 32 | var lastBlock = parseInt(res.result, 16) - self.delayInBlocks 33 | var blockNumbers = [ ] 34 | for (var i = 0; i < self.numberOfBlocks; i++) { 35 | blockNumbers.push('0x' + lastBlock.toString(16)) 36 | lastBlock-- 37 | } 38 | 39 | function getBlock(item, end) { 40 | self.emitPayload({ method: 'eth_getBlockByNumber', params: [ item, true ] }, function(err, res) { 41 | if (err) return end(err) 42 | if (!res.result) return end(new Error(`GaspriceProvider - No block for "${item}"`)) 43 | end(null, res.result.transactions) 44 | }) 45 | } 46 | 47 | // FIXME: this could be made much faster 48 | function calcPrice(err, transactions) { 49 | // flatten array 50 | transactions = transactions.reduce(function(a, b) { return a.concat(b) }, []) 51 | 52 | // leave only the gasprice 53 | // FIXME: convert number using a bignum library 54 | transactions = transactions.map(function(a) { return parseInt(a.gasPrice, 16) }, []) 55 | 56 | // order ascending 57 | transactions.sort(function(a, b) { return a - b }) 58 | 59 | // ze median 60 | var half = Math.floor(transactions.length / 2) 61 | 62 | var median 63 | if (transactions.length % 2) 64 | median = transactions[half] 65 | else 66 | median = Math.floor((transactions[half - 1] + transactions[half]) / 2.0) 67 | 68 | end(null, median) 69 | } 70 | 71 | map(blockNumbers, getBlock, calcPrice) 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /subproviders/hooked-wallet-ethtx.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Uses @ethereumjs/tx to sign a transaction. 3 | * 4 | * The two callbacks a user needs to implement are: 5 | * - getAccounts() -- array of addresses supported 6 | * - getPrivateKey(address) -- return private key for a given address 7 | * 8 | * Optionally approveTransaction(), approveMessage() can be supplied too. 9 | */ 10 | 11 | const inherits = require('util').inherits 12 | const HookedWalletProvider = require('./hooked-wallet.js') 13 | const { TransactionFactory } = require('@ethereumjs/tx') 14 | const ethUtil = require('@ethereumjs/util') 15 | const sigUtil = require('@metamask/eth-sig-util') 16 | 17 | module.exports = HookedWalletEthTxSubprovider 18 | 19 | inherits(HookedWalletEthTxSubprovider, HookedWalletProvider) 20 | 21 | function HookedWalletEthTxSubprovider(opts) { 22 | const self = this 23 | 24 | HookedWalletEthTxSubprovider.super_.call(self, opts) 25 | 26 | self.signTransaction = function(txData, cb) { 27 | // defaults 28 | if (txData.gas !== undefined) txData.gasLimit = txData.gas 29 | txData.value = txData.value || '0x00' 30 | txData.data = ethUtil.addHexPrefix(txData.data) 31 | 32 | opts.getPrivateKey(txData.from, function(err, privateKey) { 33 | if (err) return cb(err) 34 | 35 | const rawTx = TransactionFactory.fromTxData(txData) 36 | const signedTx = rawTx.sign(privateKey) 37 | cb(null, '0x' + signedTx.serialize().toString('hex')) 38 | }) 39 | } 40 | 41 | self.signMessage = function(msgParams, cb) { 42 | opts.getPrivateKey(msgParams.from, function(err, privateKey) { 43 | if (err) return cb(err) 44 | const data = typeof msgParams.data === 'string' && !ethUtil.isHexString(msgParams.data) 45 | ? Buffer.from(msgParams.data) 46 | : msgParams.data; 47 | var dataBuff = ethUtil.toBuffer(data) 48 | var msgHash = ethUtil.hashPersonalMessage(dataBuff) 49 | var sig = ethUtil.ecsign(msgHash, privateKey) 50 | var serialized = ethUtil.bufferToHex(concatSig(sig.v, sig.r, sig.s)) 51 | cb(null, serialized) 52 | }) 53 | } 54 | 55 | self.signPersonalMessage = function(msgParams, cb) { 56 | opts.getPrivateKey(msgParams.from, function(err, privateKey) { 57 | if (err) return cb(err) 58 | const serialized = sigUtil.personalSign({ 59 | privateKey, 60 | data: msgParams.data 61 | }) 62 | cb(null, serialized) 63 | }) 64 | } 65 | 66 | self.signTypedMessage = function (msgParams, cb) { 67 | opts.getPrivateKey(msgParams.from, function(err, privateKey) { 68 | if (err) return cb(err) 69 | const serialized = sigUtil.signTypedData({ 70 | privateKey, 71 | version: msgParams.version || 'V1', 72 | data: msgParams.data, 73 | }) 74 | cb(null, serialized) 75 | }) 76 | } 77 | 78 | } 79 | 80 | function concatSig(v, r, s) { 81 | r = ethUtil.fromSigned(r) 82 | s = ethUtil.fromSigned(s) 83 | v = ethUtil.bufferToInt(v) 84 | r = ethUtil.toUnsigned(r).toString('hex').padStart(64, 0) 85 | s = ethUtil.toUnsigned(s).toString('hex').padStart(64, 0) 86 | v = ethUtil.stripHexPrefix(ethUtil.intToHex(v)) 87 | return ethUtil.addHexPrefix(r.concat(s, v).toString("hex")) 88 | } 89 | -------------------------------------------------------------------------------- /subproviders/hooked-wallet.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Emulate 'eth_accounts' / 'eth_sendTransaction' using 'eth_sendRawTransaction' 3 | * 4 | * The two callbacks a user needs to implement are: 5 | * - getAccounts() -- array of addresses supported 6 | * - signTransaction(tx) -- sign a raw transaction object 7 | */ 8 | 9 | const waterfall = require('async/waterfall') 10 | const parallel = require('async/parallel') 11 | const inherits = require('util').inherits 12 | const ethUtil = require('@ethereumjs/util') 13 | const sigUtil = require('@metamask/eth-sig-util') 14 | const extend = require('xtend') 15 | const Semaphore = require('semaphore') 16 | const Subprovider = require('./subprovider.js') 17 | const estimateGas = require('../util/estimate-gas.js') 18 | const hexRegex = /^[0-9A-Fa-f]+$/g 19 | 20 | module.exports = HookedWalletSubprovider 21 | 22 | // handles the following RPC methods: 23 | // eth_coinbase 24 | // eth_accounts 25 | // eth_sendTransaction 26 | // eth_sign 27 | // eth_signTypedData 28 | // eth_signTypedData_v3 29 | // eth_signTypedData_v4 30 | // personal_sign 31 | // eth_decryptMessage 32 | // encryption_public_key 33 | // personal_ecRecover 34 | // parity_postTransaction 35 | // parity_checkRequest 36 | // parity_defaultAccount 37 | 38 | // 39 | // Tx Signature Flow 40 | // 41 | // handleRequest: eth_sendTransaction 42 | // validateTransaction (basic validity check) 43 | // validateSender (checks that sender is in accounts) 44 | // processTransaction (sign tx and submit to network) 45 | // approveTransaction (UI approval hook) 46 | // checkApproval 47 | // finalizeAndSubmitTx (tx signing) 48 | // nonceLock.take (bottle neck to ensure atomic nonce) 49 | // fillInTxExtras (set fallback gasPrice, nonce, etc) 50 | // signTransaction (perform the signature) 51 | // publishTransaction (publish signed tx to network) 52 | // 53 | 54 | 55 | inherits(HookedWalletSubprovider, Subprovider) 56 | 57 | function HookedWalletSubprovider(opts){ 58 | const self = this 59 | // control flow 60 | self.nonceLock = Semaphore(1) 61 | 62 | // data lookup 63 | if (opts.getAccounts) self.getAccounts = opts.getAccounts 64 | // high level override 65 | if (opts.processTransaction) self.processTransaction = opts.processTransaction 66 | if (opts.processMessage) self.processMessage = opts.processMessage 67 | if (opts.processPersonalMessage) self.processPersonalMessage = opts.processPersonalMessage 68 | if (opts.processTypedMessage) self.processTypedMessage = opts.processTypedMessage 69 | // approval hooks 70 | self.approveTransaction = opts.approveTransaction || self.autoApprove 71 | self.approveMessage = opts.approveMessage || self.autoApprove 72 | self.approvePersonalMessage = opts.approvePersonalMessage || self.autoApprove 73 | self.approveDecryptMessage = opts.approveDecryptMessage || self.autoApprove 74 | self.approveEncryptionPublicKey = opts.approveEncryptionPublicKey || self.autoApprove 75 | self.approveTypedMessage = opts.approveTypedMessage || self.autoApprove 76 | // actually perform the signature 77 | if (opts.signTransaction) self.signTransaction = opts.signTransaction || mustProvideInConstructor('signTransaction') 78 | if (opts.signMessage) self.signMessage = opts.signMessage || mustProvideInConstructor('signMessage') 79 | if (opts.signPersonalMessage) self.signPersonalMessage = opts.signPersonalMessage || mustProvideInConstructor('signPersonalMessage') 80 | if (opts.decryptMessage) self.decryptMessage = opts.decryptMessage || mustProvideInConstructor('decryptMessage') 81 | if (opts.encryptionPublicKey) self.encryptionPublicKey = opts.encryptionPublicKey || mustProvideInConstructor('encryptionPublicKey') 82 | if (opts.signTypedMessage) self.signTypedMessage = opts.signTypedMessage || mustProvideInConstructor('signTypedMessage') 83 | if (opts.recoverPersonalSignature) self.recoverPersonalSignature = opts.recoverPersonalSignature 84 | // publish to network 85 | if (opts.publishTransaction) self.publishTransaction = opts.publishTransaction 86 | // gas options 87 | self.estimateGas = opts.estimateGas || self.estimateGas 88 | self.getGasPrice = opts.getGasPrice || self.getGasPrice 89 | } 90 | 91 | HookedWalletSubprovider.prototype.handleRequest = function(payload, next, end){ 92 | const self = this 93 | self._parityRequests = {} 94 | self._parityRequestCount = 0 95 | 96 | // switch statement is not block scoped 97 | // sp we cant repeat var declarations 98 | let txParams, msgParams, extraParams 99 | let message, address 100 | 101 | switch(payload.method) { 102 | 103 | case 'eth_coinbase': 104 | // process normally 105 | self.getAccounts(function(err, accounts){ 106 | if (err) return end(err) 107 | let result = accounts[0] || null 108 | end(null, result) 109 | }) 110 | return 111 | 112 | case 'eth_accounts': 113 | // process normally 114 | self.getAccounts(function(err, accounts){ 115 | if (err) return end(err) 116 | end(null, accounts) 117 | }) 118 | return 119 | 120 | case 'eth_sendTransaction': 121 | txParams = payload.params[0] 122 | waterfall([ 123 | (cb) => self.validateTransaction(txParams, cb), 124 | (cb) => self.processTransaction(txParams, cb), 125 | ], end) 126 | return 127 | 128 | case 'eth_signTransaction': 129 | txParams = payload.params[0] 130 | waterfall([ 131 | (cb) => self.validateTransaction(txParams, cb), 132 | (cb) => self.processSignTransaction(txParams, cb), 133 | ], end) 134 | return 135 | 136 | case 'eth_sign': 137 | // process normally 138 | address = payload.params[0] 139 | message = payload.params[1] 140 | // non-standard "extraParams" to be appended to our "msgParams" obj 141 | // good place for metadata 142 | extraParams = payload.params[2] || {} 143 | msgParams = extend(extraParams, { 144 | from: address, 145 | data: message, 146 | }) 147 | waterfall([ 148 | (cb) => self.validateMessage(msgParams, cb), 149 | (cb) => self.processMessage(msgParams, cb), 150 | ], end) 151 | return 152 | 153 | case 'personal_sign': 154 | return (function(){ 155 | // process normally 156 | const first = payload.params[0] 157 | const second = payload.params[1] 158 | 159 | // We initially incorrectly ordered these parameters. 160 | // To gracefully respect users who adopted this API early, 161 | // we are currently gracefully recovering from the wrong param order 162 | // when it is clearly identifiable. 163 | // 164 | // That means when the first param is definitely an address, 165 | // and the second param is definitely not, but is hex. 166 | if (resemblesData(second) && resemblesAddress(first)) { 167 | let warning = `The eth_personalSign method requires params ordered ` 168 | warning += `[message, address]. This was previously handled incorrectly, ` 169 | warning += `and has been corrected automatically. ` 170 | warning += `Please switch this param order for smooth behavior in the future.` 171 | console.warn(warning) 172 | 173 | address = payload.params[0] 174 | message = payload.params[1] 175 | } else { 176 | message = payload.params[0] 177 | address = payload.params[1] 178 | } 179 | 180 | // non-standard "extraParams" to be appended to our "msgParams" obj 181 | // good place for metadata 182 | extraParams = payload.params[2] || {} 183 | msgParams = extend(extraParams, { 184 | from: address, 185 | data: message, 186 | }) 187 | waterfall([ 188 | (cb) => self.validatePersonalMessage(msgParams, cb), 189 | (cb) => self.processPersonalMessage(msgParams, cb), 190 | ], end) 191 | })() 192 | 193 | case 'eth_decryptMessage': 194 | return (function(){ 195 | // process normally 196 | const first = payload.params[0] 197 | const second = payload.params[1] 198 | 199 | // We initially incorrectly ordered these parameters. 200 | // To gracefully respect users who adopted this API early, 201 | // we are currently gracefully recovering from the wrong param order 202 | // when it is clearly identifiable. 203 | // 204 | // That means when the first param is definitely an address, 205 | // and the second param is definitely not, but is hex. 206 | if (resemblesData(second) && resemblesAddress(first)) { 207 | let warning = `The eth_decryptMessage method requires params ordered ` 208 | warning += `[message, address]. This was previously handled incorrectly, ` 209 | warning += `and has been corrected automatically. ` 210 | warning += `Please switch this param order for smooth behavior in the future.` 211 | console.warn(warning) 212 | 213 | address = payload.params[0] 214 | message = payload.params[1] 215 | } else { 216 | message = payload.params[0] 217 | address = payload.params[1] 218 | } 219 | 220 | // non-standard "extraParams" to be appended to our "msgParams" obj 221 | // good place for metadata 222 | extraParams = payload.params[2] || {} 223 | msgParams = extend(extraParams, { 224 | from: address, 225 | data: message, 226 | }) 227 | waterfall([ 228 | (cb) => self.validateDecryptMessage(msgParams, cb), 229 | (cb) => self.processDecryptMessage(msgParams, cb), 230 | ], end) 231 | })() 232 | 233 | case 'encryption_public_key': 234 | return (function(){ 235 | const address = payload.params[0] 236 | 237 | waterfall([ 238 | (cb) => self.validateEncryptionPublicKey(address, cb), 239 | (cb) => self.processEncryptionPublicKey(address, cb), 240 | ], end) 241 | })() 242 | 243 | case 'personal_ecRecover': 244 | return (function(){ 245 | message = payload.params[0] 246 | let signature = payload.params[1] 247 | // non-standard "extraParams" to be appended to our "msgParams" obj 248 | // good place for metadata 249 | extraParams = payload.params[2] || {} 250 | msgParams = extend(extraParams, { 251 | signature, 252 | data: message, 253 | }) 254 | self.recoverPersonalSignature(msgParams, end) 255 | })() 256 | 257 | case 'eth_signTypedData': 258 | case 'eth_signTypedData_v3': 259 | case 'eth_signTypedData_v4': 260 | return (function(){ 261 | // process normally 262 | 263 | const first = payload.params[0] 264 | const second = payload.params[1] 265 | 266 | if (resemblesAddress(first)) { 267 | address = first 268 | message = second 269 | } else { 270 | message = first 271 | address = second 272 | } 273 | 274 | extraParams = payload.params[2] || {} 275 | msgParams = extend(extraParams, { 276 | from: address, 277 | data: message, 278 | }) 279 | waterfall([ 280 | (cb) => self.validateTypedMessage(msgParams, cb), 281 | (cb) => self.processTypedMessage(msgParams, cb), 282 | ], end) 283 | })() 284 | 285 | case 'parity_postTransaction': 286 | txParams = payload.params[0] 287 | self.parityPostTransaction(txParams, end) 288 | return 289 | 290 | case 'parity_postSign': 291 | address = payload.params[0] 292 | message = payload.params[1] 293 | self.parityPostSign(address, message, end) 294 | return 295 | 296 | case 'parity_checkRequest': 297 | return (function(){ 298 | const requestId = payload.params[0] 299 | self.parityCheckRequest(requestId, end) 300 | })() 301 | 302 | case 'parity_defaultAccount': 303 | self.getAccounts(function(err, accounts){ 304 | if (err) return end(err) 305 | const account = accounts[0] || null 306 | end(null, account) 307 | }) 308 | return 309 | 310 | default: 311 | next() 312 | return 313 | 314 | } 315 | } 316 | 317 | // 318 | // data lookup 319 | // 320 | 321 | HookedWalletSubprovider.prototype.getAccounts = function(cb) { 322 | cb(null, []) 323 | } 324 | 325 | 326 | // 327 | // "process" high level flow 328 | // 329 | 330 | HookedWalletSubprovider.prototype.processTransaction = function(txParams, cb) { 331 | const self = this 332 | waterfall([ 333 | (cb) => self.approveTransaction(txParams, cb), 334 | (didApprove, cb) => self.checkApproval('transaction', didApprove, cb), 335 | (cb) => self.finalizeAndSubmitTx(txParams, cb), 336 | ], cb) 337 | } 338 | 339 | 340 | HookedWalletSubprovider.prototype.processSignTransaction = function(txParams, cb) { 341 | const self = this 342 | waterfall([ 343 | (cb) => self.approveTransaction(txParams, cb), 344 | (didApprove, cb) => self.checkApproval('transaction', didApprove, cb), 345 | (cb) => self.finalizeTx(txParams, cb), 346 | ], cb) 347 | } 348 | 349 | HookedWalletSubprovider.prototype.processMessage = function(msgParams, cb) { 350 | const self = this 351 | waterfall([ 352 | (cb) => self.approveMessage(msgParams, cb), 353 | (didApprove, cb) => self.checkApproval('message', didApprove, cb), 354 | (cb) => self.signMessage(msgParams, cb), 355 | ], cb) 356 | } 357 | 358 | HookedWalletSubprovider.prototype.processPersonalMessage = function(msgParams, cb) { 359 | const self = this 360 | waterfall([ 361 | (cb) => self.approvePersonalMessage(msgParams, cb), 362 | (didApprove, cb) => self.checkApproval('message', didApprove, cb), 363 | (cb) => self.signPersonalMessage(msgParams, cb), 364 | ], cb) 365 | } 366 | 367 | HookedWalletSubprovider.prototype.processDecryptMessage = function(msgParams, cb) { 368 | const self = this 369 | waterfall([ 370 | (cb) => self.approveDecryptMessage(msgParams, cb), 371 | (didApprove, cb) => self.checkApproval('decryptMessage', didApprove, cb), 372 | (cb) => self.decryptMessage(msgParams, cb), 373 | ], cb) 374 | } 375 | 376 | HookedWalletSubprovider.prototype.processEncryptionPublicKey = function(msgParams, cb) { 377 | const self = this 378 | waterfall([ 379 | (cb) => self.approveEncryptionPublicKey(msgParams, cb), 380 | (didApprove, cb) => self.checkApproval('encryptionPublicKey', didApprove, cb), 381 | (cb) => self.encryptionPublicKey(msgParams, cb), 382 | ], cb) 383 | } 384 | 385 | HookedWalletSubprovider.prototype.processTypedMessage = function(msgParams, cb) { 386 | const self = this 387 | waterfall([ 388 | (cb) => self.approveTypedMessage(msgParams, cb), 389 | (didApprove, cb) => self.checkApproval('message', didApprove, cb), 390 | (cb) => self.signTypedMessage(msgParams, cb), 391 | ], cb) 392 | } 393 | 394 | // 395 | // approval 396 | // 397 | 398 | HookedWalletSubprovider.prototype.autoApprove = function(txParams, cb) { 399 | cb(null, true) 400 | } 401 | 402 | HookedWalletSubprovider.prototype.checkApproval = function(type, didApprove, cb) { 403 | cb( didApprove ? null : new Error('User denied '+type+' signature.') ) 404 | } 405 | 406 | // 407 | // parity 408 | // 409 | 410 | HookedWalletSubprovider.prototype.parityPostTransaction = function(txParams, cb) { 411 | const self = this 412 | 413 | // get next id 414 | const count = self._parityRequestCount 415 | const reqId = `0x${count.toString(16)}` 416 | self._parityRequestCount++ 417 | 418 | self.emitPayload({ 419 | method: 'eth_sendTransaction', 420 | params: [txParams], 421 | }, function(error, res){ 422 | if (error) { 423 | self._parityRequests[reqId] = { error } 424 | return 425 | } 426 | const txHash = res.result 427 | self._parityRequests[reqId] = txHash 428 | }) 429 | 430 | cb(null, reqId) 431 | } 432 | 433 | 434 | HookedWalletSubprovider.prototype.parityPostSign = function(address, message, cb) { 435 | const self = this 436 | 437 | // get next id 438 | const count = self._parityRequestCount 439 | const reqId = `0x${count.toString(16)}` 440 | self._parityRequestCount++ 441 | 442 | self.emitPayload({ 443 | method: 'eth_sign', 444 | params: [address, message], 445 | }, function(error, res){ 446 | if (error) { 447 | self._parityRequests[reqId] = { error } 448 | return 449 | } 450 | const result = res.result 451 | self._parityRequests[reqId] = result 452 | }) 453 | 454 | cb(null, reqId) 455 | } 456 | 457 | HookedWalletSubprovider.prototype.parityCheckRequest = function(reqId, cb) { 458 | const self = this 459 | const result = self._parityRequests[reqId] || null 460 | // tx not handled yet 461 | if (!result) return cb(null, null) 462 | // tx was rejected (or other error) 463 | if (result.error) return cb(result.error) 464 | // tx sent 465 | cb(null, result) 466 | } 467 | 468 | // 469 | // signature and recovery 470 | // 471 | 472 | HookedWalletSubprovider.prototype.recoverPersonalSignature = function(msgParams, cb) { 473 | let senderHex 474 | try { 475 | senderHex = sigUtil.recoverPersonalSignature(msgParams) 476 | } catch (err) { 477 | return cb(err) 478 | } 479 | cb(null, senderHex) 480 | } 481 | 482 | // 483 | // validation 484 | // 485 | 486 | HookedWalletSubprovider.prototype.validateTransaction = function(txParams, cb){ 487 | const self = this 488 | // shortcut: undefined sender is invalid 489 | if (txParams.from === undefined) return cb(new Error(`Undefined address - from address required to sign transaction.`)) 490 | self.validateSender(txParams.from, function(err, senderIsValid){ 491 | if (err) return cb(err) 492 | if (!senderIsValid) return cb(new Error(`Unknown address - unable to sign transaction for this address: "${txParams.from}"`)) 493 | cb() 494 | }) 495 | } 496 | 497 | HookedWalletSubprovider.prototype.validateMessage = function(msgParams, cb){ 498 | const self = this 499 | if (msgParams.from === undefined) return cb(new Error(`Undefined address - from address required to sign message.`)) 500 | self.validateSender(msgParams.from, function(err, senderIsValid){ 501 | if (err) return cb(err) 502 | if (!senderIsValid) return cb(new Error(`Unknown address - unable to sign message for this address: "${msgParams.from}"`)) 503 | cb() 504 | }) 505 | } 506 | 507 | HookedWalletSubprovider.prototype.validatePersonalMessage = function(msgParams, cb){ 508 | const self = this 509 | if (msgParams.from === undefined) return cb(new Error(`Undefined address - from address required to sign personal message.`)) 510 | if (msgParams.data === undefined) return cb(new Error(`Undefined message - message required to sign personal message.`)) 511 | if (!isValidHex(msgParams.data)) return cb(new Error(`HookedWalletSubprovider - validateMessage - message was not encoded as hex.`)) 512 | self.validateSender(msgParams.from, function(err, senderIsValid){ 513 | if (err) return cb(err) 514 | if (!senderIsValid) return cb(new Error(`Unknown address - unable to sign message for this address: "${msgParams.from}"`)) 515 | cb() 516 | }) 517 | } 518 | 519 | HookedWalletSubprovider.prototype.validateDecryptMessage = function(msgParams, cb){ 520 | const self = this 521 | if (msgParams.from === undefined) return cb(new Error(`Undefined address - from address required to decrypt message.`)) 522 | if (msgParams.data === undefined) return cb(new Error(`Undefined message - message required to decrypt message.`)) 523 | if (!isValidHex(msgParams.data)) return cb(new Error(`HookedWalletSubprovider - validateDecryptMessage - message was not encoded as hex.`)) 524 | self.validateSender(msgParams.from, function(err, senderIsValid){ 525 | if (err) return cb(err) 526 | if (!senderIsValid) return cb(new Error(`Unknown address - unable to decrypt message for this address: "${msgParams.from}"`)) 527 | cb() 528 | }) 529 | } 530 | 531 | HookedWalletSubprovider.prototype.validateEncryptionPublicKey = function(address, cb){ 532 | const self = this 533 | 534 | self.validateSender(address, function(err, senderIsValid){ 535 | if (err) return cb(err) 536 | if (!senderIsValid) return cb(new Error(`Unknown address - unable to obtain encryption public key for this address: "${address}"`)) 537 | cb() 538 | }) 539 | } 540 | 541 | HookedWalletSubprovider.prototype.validateTypedMessage = function(msgParams, cb){ 542 | if (msgParams.from === undefined) return cb(new Error(`Undefined address - from address required to sign typed data.`)) 543 | if (msgParams.data === undefined) return cb(new Error(`Undefined data - message required to sign typed data.`)) 544 | this.validateSender(msgParams.from, function(err, senderIsValid){ 545 | if (err) return cb(err) 546 | if (!senderIsValid) return cb(new Error(`Unknown address - unable to sign message for this address: "${msgParams.from}"`)) 547 | cb() 548 | }) 549 | } 550 | 551 | HookedWalletSubprovider.prototype.validateSender = function(senderAddress, cb){ 552 | const self = this 553 | // shortcut: undefined sender is invalid 554 | if (!senderAddress) return cb(null, false) 555 | self.getAccounts(function(err, accounts){ 556 | if (err) return cb(err) 557 | const senderIsValid = (accounts.map(toLowerCase).indexOf(senderAddress.toLowerCase()) !== -1) 558 | cb(null, senderIsValid) 559 | }) 560 | } 561 | 562 | // 563 | // tx helpers 564 | // 565 | 566 | HookedWalletSubprovider.prototype.finalizeAndSubmitTx = function(txParams, cb) { 567 | const self = this 568 | // can only allow one tx to pass through this flow at a time 569 | // so we can atomically consume a nonce 570 | self.nonceLock.take(function(){ 571 | waterfall([ 572 | self.fillInTxExtras.bind(self, txParams), 573 | self.signTransaction.bind(self), 574 | self.publishTransaction.bind(self), 575 | ], function(err, txHash){ 576 | self.nonceLock.leave() 577 | if (err) return cb(err) 578 | cb(null, txHash) 579 | }) 580 | }) 581 | } 582 | 583 | HookedWalletSubprovider.prototype.finalizeTx = function(txParams, cb) { 584 | const self = this 585 | // can only allow one tx to pass through this flow at a time 586 | // so we can atomically consume a nonce 587 | self.nonceLock.take(function(){ 588 | waterfall([ 589 | self.fillInTxExtras.bind(self, txParams), 590 | self.signTransaction.bind(self), 591 | ], function(err, signedTx){ 592 | self.nonceLock.leave() 593 | if (err) return cb(err) 594 | cb(null, {raw: signedTx, tx: txParams}) 595 | }) 596 | }) 597 | } 598 | 599 | HookedWalletSubprovider.prototype.publishTransaction = function(rawTx, cb) { 600 | const self = this 601 | self.emitPayload({ 602 | method: 'eth_sendRawTransaction', 603 | params: [rawTx], 604 | }, function(err, res){ 605 | if (err) return cb(err) 606 | cb(null, res.result) 607 | }) 608 | } 609 | 610 | HookedWalletSubprovider.prototype.estimateGas = function(txParams, cb) { 611 | const self = this 612 | estimateGas(self.engine, txParams, cb) 613 | } 614 | 615 | HookedWalletSubprovider.prototype.getGasPrice = function(cb) { 616 | const self = this 617 | self.emitPayload({ method: 'eth_gasPrice', params: [] }, function (err, res) { 618 | if (err) return cb(err) 619 | cb(null, res.result) 620 | }) 621 | } 622 | 623 | HookedWalletSubprovider.prototype.fillInTxExtras = function(txParams, cb){ 624 | const self = this 625 | const address = txParams.from 626 | // console.log('fillInTxExtras - address:', address) 627 | 628 | const tasks = {} 629 | 630 | if (txParams.gasPrice === undefined) { 631 | // console.log("need to get gasprice") 632 | tasks.gasPrice = self.getGasPrice.bind(self) 633 | } 634 | 635 | if (txParams.nonce === undefined) { 636 | // console.log("need to get nonce") 637 | tasks.nonce = self.emitPayload.bind(self, { method: 'eth_getTransactionCount', params: [address, 'pending'] }) 638 | } 639 | 640 | if (txParams.gas === undefined) { 641 | // console.log("need to get gas") 642 | tasks.gas = self.estimateGas.bind(self, cloneTxParams(txParams)) 643 | } 644 | 645 | parallel(tasks, function(err, taskResults) { 646 | if (err) return cb(err) 647 | 648 | const result = {} 649 | if (taskResults.gasPrice) result.gasPrice = taskResults.gasPrice 650 | if (taskResults.nonce) result.nonce = taskResults.nonce.result 651 | if (taskResults.gas) result.gas = taskResults.gas 652 | 653 | cb(null, extend(txParams, result)) 654 | }) 655 | } 656 | 657 | // util 658 | 659 | // we use this to clean any custom params from the txParams 660 | function cloneTxParams(txParams){ 661 | return { 662 | from: txParams.from, 663 | to: txParams.to, 664 | value: txParams.value, 665 | data: txParams.data, 666 | gas: txParams.gas, 667 | gasPrice: txParams.gasPrice, 668 | nonce: txParams.nonce, 669 | } 670 | } 671 | 672 | function toLowerCase(string){ 673 | return string.toLowerCase() 674 | } 675 | 676 | function resemblesAddress (string) { 677 | const fixed = ethUtil.addHexPrefix(string) 678 | const isValid = ethUtil.isValidAddress(fixed) 679 | return isValid 680 | } 681 | 682 | // Returns true if resembles hex data 683 | // but definitely not a valid address. 684 | function resemblesData (string) { 685 | const fixed = ethUtil.addHexPrefix(string) 686 | const isValidAddress = ethUtil.isValidAddress(fixed) 687 | return !isValidAddress && isValidHex(string) 688 | } 689 | 690 | function isValidHex(data) { 691 | const isString = typeof data === 'string' 692 | if (!isString) return false 693 | const isHexPrefixed = data.slice(0,2) === '0x' 694 | if (!isHexPrefixed) return false 695 | const nonPrefixed = data.slice(2) 696 | const isValid = nonPrefixed.match(hexRegex) 697 | return isValid 698 | } 699 | 700 | function mustProvideInConstructor(methodName) { 701 | return function(params, cb) { 702 | cb(new Error('ProviderEngine - HookedWalletSubprovider - Must provide "' + methodName + '" fn in constructor options')) 703 | } 704 | } 705 | -------------------------------------------------------------------------------- /subproviders/inflight-cache.js: -------------------------------------------------------------------------------- 1 | const ProviderSubprovider = require('./json-rpc-engine-middleware') 2 | const { createInflightCacheMiddleware } = require('@metamask/eth-json-rpc-middleware') 3 | 4 | class InflightCacheSubprovider extends ProviderSubprovider { 5 | constructor(opts) { 6 | super(() => createInflightCacheMiddleware(opts)) 7 | } 8 | } 9 | 10 | module.exports = InflightCacheSubprovider 11 | -------------------------------------------------------------------------------- /subproviders/infura.js: -------------------------------------------------------------------------------- 1 | const { createProvider } = require('@metamask/eth-json-rpc-infura') 2 | const ProviderSubprovider = require('./provider.js') 3 | 4 | class InfuraSubprovider extends ProviderSubprovider { 5 | constructor(opts = {}) { 6 | const provider = createProvider(opts) 7 | super(provider) 8 | } 9 | } 10 | 11 | module.exports = InfuraSubprovider 12 | -------------------------------------------------------------------------------- /subproviders/ipc.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const inherits = require('util').inherits 3 | const createPayload = require('../util/create-payload.js') 4 | const Subprovider = require('./subprovider.js') 5 | 6 | module.exports = IpcSource 7 | 8 | inherits(IpcSource, Subprovider) 9 | 10 | function IpcSource(opts) { 11 | const self = this 12 | self.ipcPath = opts.ipcPath || '/root/.ethereum/geth.ipc' 13 | } 14 | 15 | 16 | IpcSource.prototype.handleRequest = function(payload, next, end){ 17 | const self = this 18 | var targetPath = self.ipcPath 19 | var method = payload.method 20 | var params = payload.params 21 | 22 | // new payload with random large id, 23 | // so as not to conflict with other concurrent users 24 | var newPayload = createPayload(payload) 25 | // console.log('------------------ network attempt -----------------') 26 | // console.log(payload) 27 | // console.log('---------------------------------------------') 28 | 29 | if(newPayload == null){ 30 | console.log('no payload'); 31 | end('no payload', null); 32 | } 33 | 34 | var client = net.connect({path: targetPath}, () => { 35 | client.end(JSON.stringify(payload)); 36 | }) 37 | 38 | 39 | client.on('connection', (d) => { 40 | console.log(d) 41 | }); 42 | 43 | client.on('data', (data) => { 44 | var response = ""; 45 | response += data.toString(); 46 | var res = JSON.parse(response); 47 | end(null, res.result); 48 | }); 49 | 50 | // client.on('end', () => { 51 | // console.log('Socket Received payload'); 52 | // }); 53 | 54 | client.on('error', (error) => { 55 | console.error(error); 56 | end(error, null); 57 | }); 58 | 59 | process.setMaxListeners(Infinity); 60 | 61 | process.on('SIGINT', () => { 62 | console.log("Caught interrupt signal"); 63 | 64 | client.end(); 65 | process.exit(); 66 | }); 67 | 68 | } 69 | -------------------------------------------------------------------------------- /subproviders/json-rpc-engine-middleware.js: -------------------------------------------------------------------------------- 1 | const Subprovider = require('./subprovider.js') 2 | 3 | // wraps a json-rpc-engine middleware in a subprovider interface 4 | 5 | class JsonRpcEngineMiddlewareSubprovider extends Subprovider { 6 | 7 | // take a constructorFn to call once we have a reference to the engine 8 | constructor (constructorFn) { 9 | super() 10 | if (!constructorFn) throw new Error('JsonRpcEngineMiddlewareSubprovider - no constructorFn specified') 11 | this._constructorFn = constructorFn 12 | } 13 | 14 | // this is called once the subprovider has been added to the provider engine 15 | setEngine (engine) { 16 | if (this.middleware) throw new Error('JsonRpcEngineMiddlewareSubprovider - subprovider added to engine twice') 17 | const blockTracker = engine._blockTracker 18 | const middleware = this._constructorFn({ engine, provider: engine, blockTracker }) 19 | if (!middleware) throw new Error('JsonRpcEngineMiddlewareSubprovider - _constructorFn did not return middleware') 20 | if (typeof middleware !== 'function') throw new Error('JsonRpcEngineMiddlewareSubprovider - specified middleware is not a function') 21 | this.middleware = middleware 22 | } 23 | 24 | handleRequest (req, provEngNext, provEngEnd) { 25 | const res = { id: req.id } 26 | this.middleware(req, res, middlewareNext, middlewareEnd) 27 | 28 | function middlewareNext (handler) { 29 | provEngNext((err, result, cb) => { 30 | // update response object with result or error 31 | if (err) { 32 | delete res.result 33 | res.error = { message: err.message || err } 34 | } else { 35 | res.result = result 36 | } 37 | // call middleware's next handler (even if error) 38 | if (handler) { 39 | handler(cb) 40 | } else { 41 | cb() 42 | } 43 | }) 44 | } 45 | 46 | function middlewareEnd (err) { 47 | if (err) return provEngEnd(err) 48 | provEngEnd(null, res.result) 49 | } 50 | } 51 | 52 | } 53 | 54 | module.exports = JsonRpcEngineMiddlewareSubprovider 55 | -------------------------------------------------------------------------------- /subproviders/nonce-tracker.js: -------------------------------------------------------------------------------- 1 | const inherits = require('util').inherits 2 | const { TransactionFactory } = require('@ethereumjs/tx') 3 | const ethUtil = require('@ethereumjs/util') 4 | const Subprovider = require('./subprovider.js') 5 | const blockTagForPayload = require('../util/rpc-cache-utils').blockTagForPayload 6 | 7 | module.exports = NonceTrackerSubprovider 8 | 9 | // handles the following RPC methods: 10 | // eth_getTransactionCount (pending only) 11 | // 12 | // observes the following RPC methods: 13 | // eth_sendRawTransaction 14 | // evm_revert (to clear the nonce cache) 15 | 16 | inherits(NonceTrackerSubprovider, Subprovider) 17 | 18 | function NonceTrackerSubprovider(){ 19 | const self = this 20 | 21 | self.nonceCache = {} 22 | } 23 | 24 | NonceTrackerSubprovider.prototype.handleRequest = function(payload, next, end){ 25 | const self = this 26 | 27 | switch(payload.method) { 28 | 29 | case 'eth_getTransactionCount': 30 | var blockTag = blockTagForPayload(payload) 31 | var address = payload.params[0].toLowerCase() 32 | var cachedResult = self.nonceCache[address] 33 | // only handle requests against the 'pending' blockTag 34 | if (blockTag === 'pending') { 35 | // has a result 36 | if (cachedResult) { 37 | end(null, cachedResult) 38 | // fallthrough then populate cache 39 | } else { 40 | next(function(err, result, cb){ 41 | if (err) return cb() 42 | if (self.nonceCache[address] === undefined) { 43 | self.nonceCache[address] = result 44 | } 45 | cb() 46 | }) 47 | } 48 | } else { 49 | next() 50 | } 51 | return 52 | 53 | case 'eth_sendRawTransaction': 54 | // allow the request to continue normally 55 | next(function(err, result, cb){ 56 | // only update local nonce if tx was submitted correctly 57 | if (err) return cb() 58 | // parse raw tx 59 | var rawTx = payload.params[0] 60 | var rawData = Buffer.from(ethUtil.stripHexPrefix(rawTx), 'hex') 61 | const tx = TransactionFactory.fromSerializedData(rawData) 62 | // extract address 63 | var address = tx.getSenderAddress().toString('hex').toLowerCase() 64 | // extract nonce and increment 65 | var nonce = ethUtil.bufferToInt(tx.nonce) 66 | nonce++ 67 | // hexify and normalize 68 | var hexNonce = nonce.toString(16) 69 | if (hexNonce.length%2) hexNonce = '0'+hexNonce 70 | hexNonce = '0x'+hexNonce 71 | // dont update our record on the nonce until the submit was successful 72 | // update cache 73 | self.nonceCache[address] = hexNonce 74 | cb() 75 | }) 76 | return 77 | 78 | // Clear cache on a testrpc revert 79 | case 'evm_revert': 80 | self.nonceCache = {} 81 | next() 82 | return 83 | 84 | default: 85 | next() 86 | return 87 | 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /subproviders/provider.js: -------------------------------------------------------------------------------- 1 | const inherits = require('util').inherits 2 | const Subprovider = require('./subprovider.js') 3 | 4 | // wraps a provider in a subprovider interface 5 | 6 | module.exports = ProviderSubprovider 7 | 8 | inherits(ProviderSubprovider, Subprovider) 9 | 10 | function ProviderSubprovider(provider){ 11 | if (!provider) throw new Error('ProviderSubprovider - no provider specified') 12 | if (!provider.sendAsync) throw new Error('ProviderSubprovider - specified provider does not have a sendAsync method') 13 | this.provider = provider 14 | } 15 | 16 | ProviderSubprovider.prototype.handleRequest = function(payload, next, end){ 17 | this.provider.sendAsync(payload, function(err, response) { 18 | if (err) return end(err) 19 | if (response.error) return end(new Error(response.error.message)) 20 | end(null, response.result) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /subproviders/rpc.js: -------------------------------------------------------------------------------- 1 | const xhr = process.browser ? require('xhr') : require('@cypress/request') 2 | const inherits = require('util').inherits 3 | const createPayload = require('../util/create-payload.js') 4 | const Subprovider = require('./subprovider.js') 5 | const { rpcErrors, serializeError } = require('@metamask/rpc-errors') 6 | 7 | 8 | module.exports = RpcSource 9 | 10 | inherits(RpcSource, Subprovider) 11 | 12 | function RpcSource(opts) { 13 | const self = this 14 | self.rpcUrl = opts.rpcUrl 15 | } 16 | 17 | RpcSource.prototype.handleRequest = function(payload, next, end){ 18 | const self = this 19 | const targetUrl = self.rpcUrl 20 | 21 | // overwrite id to conflict with other concurrent users 22 | const sanitizedPayload = sanitizePayload(payload) 23 | const newPayload = createPayload(sanitizedPayload) 24 | 25 | xhr({ 26 | uri: targetUrl, 27 | method: 'POST', 28 | headers: { 29 | 'Accept': 'application/json', 30 | 'Content-Type': 'application/json', 31 | }, 32 | body: JSON.stringify(newPayload), 33 | rejectUnauthorized: false, 34 | timeout: 20000, 35 | }, function(err, res, body) { 36 | if (err) return end(serializeError(err)) 37 | 38 | // check for error code 39 | switch (res.statusCode) { 40 | case 405: 41 | return end(rpcErrors.methodNotFound()) 42 | case 504: // Gateway timeout 43 | return (function(){ 44 | let msg = `Gateway timeout. The request took too long to process. ` 45 | msg += `This can happen when querying logs over too wide a block range.` 46 | const err = new Error(msg) 47 | return end(serializeError(err)) 48 | })() 49 | case 429: // Too many requests (rate limiting) 50 | return (function(){ 51 | const err = new Error(`Too Many Requests`) 52 | return end(serializeError(err)) 53 | })() 54 | default: 55 | if (res.statusCode != 200) { 56 | const msg = 'Unknown Error: ' + res.body 57 | const err = new Error(msg) 58 | return end(serializeError(err)) 59 | } 60 | } 61 | 62 | // parse response 63 | let data 64 | try { 65 | data = JSON.parse(body) 66 | } catch (err) { 67 | console.error(err.stack) 68 | return end(serializeError(err)) 69 | } 70 | if (data.error) return end(data.error) 71 | 72 | end(null, data.result) 73 | }) 74 | } 75 | 76 | // drops any non-standard params 77 | function sanitizePayload (payload) { 78 | return { 79 | id: payload.id, 80 | jsonrpc: payload.jsonrpc, 81 | method: payload.method, 82 | params: payload.params, 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /subproviders/sanitizer.js: -------------------------------------------------------------------------------- 1 | /* Sanitization Subprovider 2 | * For Parity compatibility 3 | * removes irregular keys 4 | */ 5 | 6 | const inherits = require('util').inherits 7 | const Subprovider = require('./subprovider.js') 8 | const extend = require('xtend') 9 | const ethUtil = require('@ethereumjs/util') 10 | 11 | module.exports = SanitizerSubprovider 12 | 13 | inherits(SanitizerSubprovider, Subprovider) 14 | 15 | function SanitizerSubprovider(opts){ 16 | const self = this 17 | } 18 | 19 | SanitizerSubprovider.prototype.handleRequest = function(payload, next, end){ 20 | var txParams = payload.params[0] 21 | 22 | if (typeof txParams === 'object' && !Array.isArray(txParams)) { 23 | var sanitized = cloneTxParams(txParams) 24 | payload.params[0] = sanitized 25 | } 26 | 27 | next() 28 | } 29 | 30 | // we use this to clean any custom params from the txParams 31 | var permitted = [ 32 | 'from', 33 | 'to', 34 | 'value', 35 | 'data', 36 | 'gas', 37 | 'gasPrice', 38 | 'nonce', 39 | 'fromBlock', 40 | 'toBlock', 41 | 'address', 42 | 'topics', 43 | ] 44 | 45 | function cloneTxParams(txParams){ 46 | var sanitized = permitted.reduce(function(copy, permitted) { 47 | if (permitted in txParams) { 48 | if (Array.isArray(txParams[permitted])) { 49 | copy[permitted] = txParams[permitted] 50 | .map(function(item) { 51 | return sanitize(item) 52 | }) 53 | } else { 54 | copy[permitted] = sanitize(txParams[permitted]) 55 | } 56 | } 57 | return copy 58 | }, {}) 59 | 60 | return sanitized 61 | } 62 | 63 | function sanitize(value) { 64 | switch (value) { 65 | case 'latest': 66 | return value 67 | case 'pending': 68 | return value 69 | case 'earliest': 70 | return value 71 | default: 72 | if (typeof value === 'string') { 73 | return ethUtil.addHexPrefix(value.toLowerCase()) 74 | } else { 75 | return value 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /subproviders/stream.js: -------------------------------------------------------------------------------- 1 | const Duplex = require('readable-stream').Duplex 2 | const inherits = require('util').inherits 3 | const Subprovider = require('./subprovider.js') 4 | 5 | module.exports = StreamSubprovider 6 | 7 | 8 | inherits(StreamSubprovider, Duplex) 9 | 10 | function StreamSubprovider(){ 11 | Duplex.call(this, { 12 | objectMode: true, 13 | }) 14 | 15 | this._payloads = {} 16 | } 17 | 18 | StreamSubprovider.prototype.handleRequest = function(payload, next, end){ 19 | var id = payload.id 20 | // handle batch requests 21 | if (Array.isArray(payload)) { 22 | // short circuit for empty batch requests 23 | if (payload.length === 0){ 24 | return end(null, []) 25 | } 26 | id = generateBatchId(payload) 27 | } 28 | // store request details 29 | this._payloads[id] = [payload, end] 30 | this.push(payload) 31 | } 32 | 33 | StreamSubprovider.prototype.setEngine = noop 34 | 35 | // stream plumbing 36 | 37 | StreamSubprovider.prototype._read = noop 38 | 39 | StreamSubprovider.prototype._write = function(msg, encoding, cb){ 40 | this._onResponse(msg) 41 | cb() 42 | } 43 | 44 | // private 45 | 46 | StreamSubprovider.prototype._onResponse = function(response){ 47 | var id = response.id 48 | // handle batch requests 49 | if (Array.isArray(response)) { 50 | id = generateBatchId(response) 51 | } 52 | var data = this._payloads[id] 53 | if (!data) throw new Error('StreamSubprovider - Unknown response id') 54 | delete this._payloads[id] 55 | var callback = data[1] 56 | 57 | // run callback on empty stack, 58 | // prevent internal stream-handler from catching errors 59 | setTimeout(function(){ 60 | callback(null, response.result) 61 | }) 62 | } 63 | 64 | 65 | // util 66 | 67 | function generateBatchId(batchPayload){ 68 | return 'batch:'+batchPayload.map(function(payload){ return payload.id }).join(',') 69 | } 70 | 71 | function noop(){} 72 | 73 | 74 | module.exports = StreamSubprovider 75 | -------------------------------------------------------------------------------- /subproviders/subprovider.js: -------------------------------------------------------------------------------- 1 | const createPayload = require('../util/create-payload.js') 2 | 3 | module.exports = SubProvider 4 | 5 | // this is the base class for a subprovider -- mostly helpers 6 | 7 | 8 | function SubProvider() { 9 | 10 | } 11 | 12 | SubProvider.prototype.setEngine = function(engine) { 13 | const self = this 14 | if (self.engine) return 15 | self.engine = engine 16 | engine.on('block', function(block) { 17 | self.currentBlock = block 18 | }) 19 | 20 | engine.on('start', function () { 21 | self.start() 22 | }) 23 | 24 | engine.on('stop', function () { 25 | self.stop() 26 | }) 27 | } 28 | 29 | SubProvider.prototype.handleRequest = function(payload, next, end) { 30 | throw new Error('Subproviders should override `handleRequest`.') 31 | } 32 | 33 | SubProvider.prototype.emitPayload = function(payload, cb){ 34 | const self = this 35 | self.engine.sendAsync(createPayload(payload), cb) 36 | } 37 | 38 | // dummies for overriding 39 | 40 | SubProvider.prototype.stop = function () {} 41 | 42 | SubProvider.prototype.start = function () {} 43 | -------------------------------------------------------------------------------- /subproviders/subscriptions.js: -------------------------------------------------------------------------------- 1 | const ProviderSubprovider = require('./json-rpc-engine-middleware') 2 | const createSubscriptionManager = require('@metamask/eth-json-rpc-filters/subscriptionManager') 3 | 4 | class SubscriptionsSubprovider extends ProviderSubprovider { 5 | constructor() { 6 | super(({ blockTracker, provider, engine }) => { 7 | const { events, middleware } = createSubscriptionManager({ blockTracker, provider }) 8 | // forward subscription events on the engine 9 | events.on('notification', (data) => engine.emit('data', null, data)) 10 | // return the subscription install/remove middleware 11 | return middleware 12 | }) 13 | } 14 | } 15 | 16 | module.exports = SubscriptionsSubprovider 17 | -------------------------------------------------------------------------------- /subproviders/vm.js: -------------------------------------------------------------------------------- 1 | const doWhilst = require('async/doWhilst') 2 | const inherits = require('util').inherits 3 | const Stoplight = require('../util/stoplight.js') 4 | const { VM } = require('@ethereumjs/vm') 5 | const Block = require('@ethereumjs/block') 6 | const { EthersStateManager } = require('@ethereumjs/statemanager') 7 | const { TransactionFactory } = require('@ethereumjs/tx') 8 | const ethUtil = require('@ethereumjs/util') 9 | const rpcHexEncoding = require('../util/rpc-hex-encoding.js') 10 | const Subprovider = require('./subprovider.js') 11 | 12 | module.exports = VmSubprovider 13 | 14 | // handles the following RPC methods: 15 | // eth_call 16 | // eth_estimateGas 17 | 18 | 19 | inherits(VmSubprovider, Subprovider) 20 | 21 | function VmSubprovider(opts){ 22 | const self = this 23 | self.opts = opts || {}; 24 | self.methods = ['eth_call', 'eth_estimateGas'] 25 | // set initialization blocker 26 | self._ready = new Stoplight() 27 | self._blockGasLimit = null 28 | } 29 | 30 | // setup a block listener on 'setEngine' 31 | VmSubprovider.prototype.setEngine = function(engine) { 32 | const self = this 33 | Subprovider.prototype.setEngine.call(self, engine) 34 | // unblock initialization after first block 35 | engine.once('block', function(block) { 36 | self._blockGasLimit = ethUtil.bufferToInt(block.gasLimit) 37 | self._ready.go() 38 | }) 39 | } 40 | 41 | VmSubprovider.prototype.handleRequest = function(payload, next, end) { 42 | const self = this 43 | 44 | // skip unrelated methods 45 | if (self.methods.indexOf(payload.method) < 0) { 46 | return next() 47 | } 48 | 49 | // wait until we have seen 1 block 50 | self._ready.await(() => { 51 | 52 | switch (payload.method) { 53 | 54 | case 'eth_call': 55 | self.runVm(payload, function(err, results){ 56 | if (err) return end(err) 57 | var result = '0x' 58 | if (!results.error && results.vm.return) { 59 | result = ethUtil.addHexPrefix(results.vm.return.toString('hex')) 60 | } 61 | end(null, result) 62 | }) 63 | return 64 | 65 | case 'eth_estimateGas': 66 | self.estimateGas(payload, end) 67 | return 68 | } 69 | }) 70 | } 71 | 72 | VmSubprovider.prototype.estimateGas = function(payload, end) { 73 | const self = this 74 | var lo = 0 75 | var hi = self._blockGasLimit 76 | if (!hi) return end(new Error('VmSubprovider - missing blockGasLimit')) 77 | 78 | var minDiffBetweenIterations = 1200 79 | var prevGasLimit = self._blockGasLimit 80 | doWhilst( 81 | function(callback) { 82 | // Take a guess at the gas, and check transaction validity 83 | var mid = (hi + lo) / 2 84 | payload.params[0].gas = mid 85 | self.runVm(payload, function(err, results) { 86 | var gasUsed = err ? self._blockGasLimit : ethUtil.bufferToInt(results.gasUsed) 87 | if (err || gasUsed === 0) { 88 | lo = mid 89 | } else { 90 | hi = mid 91 | // Perf improvement: stop the binary search when the difference in gas between two iterations 92 | // is less then `minDiffBetweenIterations`. Doing this cuts the number of iterations from 23 93 | // to 12, with only a ~1000 gas loss in precision. 94 | if (Math.abs(prevGasLimit - mid) < minDiffBetweenIterations) { 95 | lo = hi 96 | } 97 | } 98 | prevGasLimit = mid 99 | callback() 100 | }) 101 | }, 102 | function() { return lo+1 < hi }, 103 | function(err) { 104 | if (err) { 105 | end(err) 106 | } else { 107 | hi = Math.floor(hi) 108 | var gasEstimateHex = rpcHexEncoding.intToQuantityHex(hi) 109 | end(null, gasEstimateHex) 110 | } 111 | } 112 | ) 113 | } 114 | 115 | VmSubprovider.prototype.runVm = function(payload, cb){ 116 | const self = this 117 | 118 | var blockData = self.currentBlock 119 | var block = blockFromBlockData(blockData) 120 | var blockNumber = ethUtil.addHexPrefix(blockData.number.toString('hex')) 121 | 122 | // create vm with state lookup intercepted 123 | const vm = self.vm = new VM({ 124 | stateManager: new EthersStateManager({ 125 | provider: self.engine, 126 | blockTag: blockNumber, 127 | }), 128 | }) 129 | 130 | if (self.opts.debug) { 131 | vm.on('step', function (data) { 132 | console.log(data.opcode.name) 133 | }) 134 | } 135 | 136 | // create tx 137 | var txParams = payload.params[0] 138 | // console.log('params:', payload.params) 139 | 140 | const normalizedTxParams = { 141 | to: txParams.to ? ethUtil.addHexPrefix(txParams.to) : undefined, 142 | from: txParams.from ? ethUtil.addHexPrefix(txParams.from) : undefined, 143 | value: txParams.value ? ethUtil.addHexPrefix(txParams.value) : undefined, 144 | data: txParams.data ? ethUtil.addHexPrefix(txParams.data) : undefined, 145 | gasLimit: txParams.gas ? ethUtil.addHexPrefix(txParams.gas) : block.header.gasLimit, 146 | gasPrice: txParams.gasPrice ? ethUtil.addHexPrefix(txParams.gasPrice) : undefined, 147 | nonce: txParams.nonce ? ethUtil.addHexPrefix(txParams.nonce) : undefined, 148 | } 149 | const tx = TransactionFactory.fromTxData(normalizedTxParams) 150 | const fakeTx = Object.create(tx) 151 | // override getSenderAddress 152 | fakeTx.getSenderAddress = () => normalizedTxParams.from 153 | 154 | vm.runTx({ 155 | tx: fakeTx, 156 | block: block, 157 | skipNonce: true, 158 | skipBalance: true 159 | }, function(err, results) { 160 | if (err) return cb(err) 161 | if (results.error != null) { 162 | return cb(new Error("VM error: " + results.error)) 163 | } 164 | if (results.vm && results.vm.exception !== 1) { 165 | return cb(new Error("VM Exception while executing " + payload.method + ": " + results.vm.exceptionError)) 166 | } 167 | 168 | cb(null, results) 169 | }) 170 | 171 | } 172 | 173 | function blockFromBlockData(blockData){ 174 | var block = new Block() 175 | // block.header.hash = ethUtil.addHexPrefix(blockData.hash.toString('hex')) 176 | 177 | block.header.parentHash = blockData.parentHash 178 | block.header.uncleHash = blockData.sha3Uncles 179 | block.header.coinbase = blockData.miner 180 | block.header.stateRoot = blockData.stateRoot 181 | block.header.transactionTrie = blockData.transactionsRoot 182 | block.header.receiptTrie = blockData.receiptRoot || blockData.receiptsRoot 183 | block.header.bloom = blockData.logsBloom 184 | block.header.difficulty = blockData.difficulty 185 | block.header.number = blockData.number 186 | block.header.gasLimit = blockData.gasLimit 187 | block.header.gasUsed = blockData.gasUsed 188 | block.header.timestamp = blockData.timestamp 189 | block.header.extraData = blockData.extraData 190 | return block 191 | } 192 | -------------------------------------------------------------------------------- /subproviders/wallet.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const inherits = require('util').inherits 4 | const HookedWalletEthTxSubprovider = require('./hooked-wallet-ethtx.js') 5 | 6 | module.exports = WalletSubprovider 7 | 8 | inherits(WalletSubprovider, HookedWalletEthTxSubprovider) 9 | 10 | function WalletSubprovider (wallet, opts) { 11 | opts.getAccounts = function (cb) { 12 | cb(null, [ wallet.getAddressString() ]) 13 | } 14 | 15 | opts.getPrivateKey = function (address, cb) { 16 | if (address.toLowerCase() !== wallet.getAddressString()) { 17 | return cb('Account not found') 18 | } 19 | 20 | cb(null, wallet.getPrivateKey()) 21 | } 22 | 23 | WalletSubprovider.super_.call(this, opts) 24 | } 25 | -------------------------------------------------------------------------------- /subproviders/websocket.js: -------------------------------------------------------------------------------- 1 | const Backoff = require('backoff') 2 | const EventEmitter = require('events') 3 | const inherits = require('util').inherits 4 | const WebSocket = global.WebSocket || require('ws') 5 | const Subprovider = require('./subprovider') 6 | const createPayload = require('../util/create-payload') 7 | 8 | class WebsocketSubprovider 9 | extends Subprovider { 10 | constructor({ rpcUrl, debug, origin }) { 11 | super() 12 | 13 | // inherit from EventEmitter 14 | EventEmitter.call(this) 15 | 16 | Object.defineProperties(this, { 17 | _backoff: { 18 | value: Backoff.exponential({ 19 | randomisationFactor: 0.2, 20 | maxDelay: 5000 21 | }) 22 | }, 23 | _connectTime: { 24 | value: null, 25 | writable: true 26 | }, 27 | _log: { 28 | value: debug 29 | ? (...args) => console.info.apply(console, ['[WSProvider]', ...args]) 30 | : () => { } 31 | }, 32 | _origin: { 33 | value: origin 34 | }, 35 | _pendingRequests: { 36 | value: new Map() 37 | }, 38 | _socket: { 39 | value: null, 40 | writable: true 41 | }, 42 | _unhandledRequests: { 43 | value: [] 44 | }, 45 | _url: { 46 | value: rpcUrl 47 | } 48 | }) 49 | 50 | this._handleSocketClose = this._handleSocketClose.bind(this) 51 | this._handleSocketMessage = this._handleSocketMessage.bind(this) 52 | this._handleSocketOpen = this._handleSocketOpen.bind(this) 53 | 54 | // Called when a backoff timeout has finished. Time to try reconnecting. 55 | this._backoff.on('ready', () => { 56 | this._openSocket() 57 | }) 58 | 59 | this._openSocket() 60 | } 61 | 62 | handleRequest(payload, next, end) { 63 | if (!this._socket || this._socket.readyState !== WebSocket.OPEN) { 64 | this._unhandledRequests.push(Array.from(arguments)) 65 | this._log('Socket not open. Request queued.') 66 | return 67 | } 68 | 69 | this._pendingRequests.set(payload.id, [payload, end]) 70 | 71 | const newPayload = createPayload(payload) 72 | delete newPayload.origin 73 | 74 | this._socket.send(JSON.stringify(newPayload)) 75 | this._log(`Sent: ${newPayload.method} #${newPayload.id}`) 76 | } 77 | 78 | _handleSocketClose({ reason, code }) { 79 | this._log(`Socket closed, code ${code} (${reason || 'no reason'})`) 80 | // If the socket has been open for longer than 5 seconds, reset the backoff 81 | if (this._connectTime && Date.now() - this._connectTime > 5000) { 82 | this._backoff.reset() 83 | } 84 | 85 | this._socket.removeEventListener('close', this._handleSocketClose) 86 | this._socket.removeEventListener('message', this._handleSocketMessage) 87 | this._socket.removeEventListener('open', this._handleSocketOpen) 88 | 89 | this._socket = null 90 | this._backoff.backoff() 91 | } 92 | 93 | _handleSocketMessage(message) { 94 | let payload 95 | 96 | try { 97 | payload = JSON.parse(message.data) 98 | } catch (e) { 99 | this._log('Received a message that is not valid JSON:', payload) 100 | return 101 | } 102 | 103 | // check if server-sent notification 104 | if (payload.id === undefined) { 105 | return this.engine.emit('data', null, payload) 106 | } 107 | 108 | // ignore if missing 109 | if (!this._pendingRequests.has(payload.id)) { 110 | return 111 | } 112 | 113 | // retrieve payload + arguments 114 | const [originalReq, end] = this._pendingRequests.get(payload.id) 115 | this._pendingRequests.delete(payload.id) 116 | 117 | this._log(`Received: ${originalReq.method} #${payload.id}`) 118 | 119 | // forward response 120 | if (payload.error) { 121 | return end(new Error(payload.error.message)) 122 | } 123 | end(null, payload.result) 124 | } 125 | 126 | _handleSocketOpen() { 127 | this._log('Socket open.') 128 | this._connectTime = Date.now() 129 | 130 | // Any pending requests need to be resent because our session was lost 131 | // and will not get responses for them in our new session. 132 | this._pendingRequests.forEach(([payload, end]) => { 133 | this._unhandledRequests.push([payload, null, end]) 134 | }) 135 | this._pendingRequests.clear() 136 | 137 | const unhandledRequests = this._unhandledRequests.splice(0, this._unhandledRequests.length) 138 | unhandledRequests.forEach(request => { 139 | this.handleRequest.apply(this, request) 140 | }) 141 | } 142 | 143 | _openSocket() { 144 | this._log('Opening socket...') 145 | this._socket = new WebSocket(this._url, [], this._origin ? {headers:{origin: this._origin}} : {}) 146 | this._socket.addEventListener('close', this._handleSocketClose) 147 | this._socket.addEventListener('message', this._handleSocketMessage) 148 | this._socket.addEventListener('open', this._handleSocketOpen) 149 | } 150 | } 151 | 152 | // multiple inheritance 153 | Object.assign(WebsocketSubprovider.prototype, EventEmitter.prototype) 154 | 155 | module.exports = WebsocketSubprovider 156 | -------------------------------------------------------------------------------- /subproviders/whitelist.js: -------------------------------------------------------------------------------- 1 | const inherits = require('util').inherits 2 | const Subprovider = require('./subprovider.js') 3 | 4 | module.exports = WhitelistProvider 5 | 6 | inherits(WhitelistProvider, Subprovider) 7 | 8 | function WhitelistProvider(methods){ 9 | this.methods = methods; 10 | 11 | if (this.methods == null) { 12 | this.methods = [ 13 | 'eth_gasPrice', 14 | 'eth_blockNumber', 15 | 'eth_getBalance', 16 | 'eth_getBlockByHash', 17 | 'eth_getBlockByNumber', 18 | 'eth_getBlockTransactionCountByHash', 19 | 'eth_getBlockTransactionCountByNumber', 20 | 'eth_getCode', 21 | 'eth_getStorageAt', 22 | 'eth_getTransactionByBlockHashAndIndex', 23 | 'eth_getTransactionByBlockNumberAndIndex', 24 | 'eth_getTransactionByHash', 25 | 'eth_getTransactionCount', 26 | 'eth_getTransactionReceipt', 27 | 'eth_getUncleByBlockHashAndIndex', 28 | 'eth_getUncleByBlockNumberAndIndex', 29 | 'eth_getUncleCountByBlockHash', 30 | 'eth_getUncleCountByBlockNumber', 31 | 'eth_sendRawTransaction', 32 | 'eth_getLogs' 33 | ]; 34 | } 35 | } 36 | 37 | WhitelistProvider.prototype.handleRequest = function(payload, next, end){ 38 | if (this.methods.indexOf(payload.method) >= 0) { 39 | next(); 40 | } else { 41 | end(new Error("Method '" + payload.method + "' not allowed in whitelist.")); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const ProviderEngine = require('../index.js') 3 | const PassthroughProvider = require('./util/passthrough.js') 4 | const FixtureProvider = require('../subproviders/fixture.js') 5 | const TestBlockProvider = require('./util/block.js') 6 | const createPayload = require('../util/create-payload.js') 7 | const injectMetrics = require('./util/inject-metrics') 8 | 9 | 10 | test('fallthrough test', function(t){ 11 | t.plan(8) 12 | 13 | // handle nothing 14 | var providerA = injectMetrics(new PassthroughProvider()) 15 | // handle "test_rpc" 16 | var providerB = injectMetrics(new FixtureProvider({ 17 | test_rpc: true, 18 | })) 19 | // handle block requests 20 | var providerC = injectMetrics(new TestBlockProvider()) 21 | 22 | var engine = new ProviderEngine() 23 | engine.addProvider(providerA) 24 | engine.addProvider(providerB) 25 | engine.addProvider(providerC) 26 | 27 | engine.start() 28 | engine.sendAsync(createPayload({ method: 'test_rpc' }), function(err, response){ 29 | t.ifError(err, 'did not error') 30 | t.ok(response, 'has response') 31 | 32 | t.equal(providerA.getWitnessed('test_rpc').length, 1, 'providerA did see "test_rpc"') 33 | t.equal(providerA.getHandled('test_rpc').length, 0, 'providerA did NOT handle "test_rpc"') 34 | 35 | t.equal(providerB.getWitnessed('test_rpc').length, 1, 'providerB did see "test_rpc"') 36 | t.equal(providerB.getHandled('test_rpc').length, 1, 'providerB did handle "test_rpc"') 37 | 38 | t.equal(providerC.getWitnessed('test_rpc').length, 0, 'providerC did NOT see "test_rpc"') 39 | t.equal(providerC.getHandled('test_rpc').length, 0, 'providerC did NOT handle "test_rpc"') 40 | 41 | engine.stop() 42 | t.end() 43 | }) 44 | 45 | }) 46 | 47 | test('add provider at index', function(t){ 48 | var providerA = new PassthroughProvider() 49 | var providerB = new PassthroughProvider() 50 | var providerC = new PassthroughProvider() 51 | var engine = new ProviderEngine() 52 | engine.addProvider(providerA) 53 | engine.addProvider(providerB) 54 | engine.addProvider(providerC, 1) 55 | 56 | t.deepEqual(engine._providers, [providerA, providerC, providerB]) 57 | t.end() 58 | }) 59 | 60 | test('remove provider', function(t){ 61 | var providerA = new PassthroughProvider() 62 | var providerB = new PassthroughProvider() 63 | var providerC = new PassthroughProvider() 64 | var engine = new ProviderEngine() 65 | engine.addProvider(providerA) 66 | engine.addProvider(providerB) 67 | engine.addProvider(providerC) 68 | engine.removeProvider(providerB) 69 | 70 | t.deepEqual(engine._providers, [providerA, providerC]) 71 | t.end() 72 | }) 73 | -------------------------------------------------------------------------------- /test/cache-utils.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const cacheUtils = require('../util/rpc-cache-utils') 3 | 4 | test('cacheIdentifierForPayload for latest block', function (t) { 5 | const payload1 = {id: 1, jsonrpc: '2.0', params: ['latest', false], method: 'eth_getBlockByNumber'} 6 | const payload2 = {id: 2, jsonrpc: '2.0', params: ['0x0', false], method: 'eth_getBlockByNumber'} 7 | const cacheId1 = cacheUtils.cacheIdentifierForPayload(payload1, { includeBlockRef: true }) 8 | const cacheId2 = cacheUtils.cacheIdentifierForPayload(payload2, { includeBlockRef: true }) 9 | 10 | t.notEqual(cacheId1, cacheId2, 'cacheIds are unique') 11 | t.end() 12 | }) 13 | 14 | test('blockTagForPayload for different methods', function (t) { 15 | const payloads = [ 16 | {jsonrpc: '2.0', method: 'eth_getBalance', params: ['0x407d73d8a49eeb85d32cf465507dd71d507100c1', '0x1234'], id: 1}, 17 | {jsonrpc: '2.0', method: 'eth_getCode', params: ['0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b', '0x1234'], id: 1}, 18 | {jsonrpc: '2.0', method: 'eth_getTransactionCount', params: ['0x407d73d8a49eeb85d32cf465507dd71d507100c1','0x1234'], id: 1}, 19 | {jsonrpc: '2.0', method: 'eth_getStorageAt', params: ['0x295a70b2de5e3953354a6a8344e616ed314d7251', '0x0', '0x1234'], id: 1}, 20 | {jsonrpc: '2.0', method: 'eth_call', params: [{to: '0x295a70b2de5e3953354a6a8344e616ed314d7251'}, '0x1234'], id: 1}, 21 | {jsonrpc: '2.0', method: 'eth_estimateGas', params: [{to: '0x295a70b2de5e3953354a6a8344e616ed314d7251'}, '0x1234'], id: 1}, 22 | {jsonrpc: '2.0', method: 'eth_getBlockByNumber', params: ['0x1234', true], id: 1}, 23 | ] 24 | 25 | 26 | payloads.forEach(function (payload) { 27 | const blockTag = cacheUtils.blockTagForPayload(payload) 28 | t.isEqual(blockTag, '0x1234', 'block tag for ' + payload.method + ' is correct') 29 | }) 30 | 31 | t.end() 32 | }) 33 | -------------------------------------------------------------------------------- /test/cache.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const series = require('async/series') 3 | const createGanacheProvider = require('ganache').provider 4 | const ProviderEngine = require('../index.js') 5 | const FixtureProvider = require('../subproviders/fixture.js') 6 | const CacheProvider = require('../subproviders/cache.js') 7 | const ProviderSubprovider = require('../subproviders/provider.js') 8 | const createPayload = require('../util/create-payload.js') 9 | const injectMetrics = require('./util/inject-metrics') 10 | 11 | // skip cache 12 | 13 | cacheTest('skipCache - true', { 14 | method: 'eth_getBalance', 15 | skipCache: true, 16 | }, false) 17 | 18 | cacheTest('skipCache - false', { 19 | method: 'eth_getBalance', 20 | skipCache: false, 21 | }, true) 22 | 23 | // block tags 24 | 25 | cacheTest('getBalance + undefined blockTag', { 26 | method: 'eth_getBalance', 27 | params: ['0x1234'], 28 | }, true) 29 | 30 | cacheTest('getBalance + latest blockTag', { 31 | method: 'eth_getBalance', 32 | params: ['0x1234', 'latest'], 33 | }, true) 34 | 35 | cacheTest('getBalance + pending blockTag', { 36 | method: 'eth_getBalance', 37 | params: ['0x1234', 'pending'], 38 | }, false) 39 | 40 | // tx by hash 41 | 42 | cacheTest('getTransactionByHash for transaction that doesn\'t exist', { 43 | method: 'eth_getTransactionByHash', 44 | params: ['0x00000000000000000000000000000000000000000000000000deadbeefcafe00'], 45 | }, false) 46 | 47 | cacheTest('getTransactionByHash for transaction that\'s pending', { 48 | method: 'eth_getTransactionByHash', 49 | params: ['0x00000000000000000000000000000000000000000000000000deadbeefcafe01'], 50 | }, false) 51 | 52 | cacheTest('getTransactionByHash for mined transaction', { 53 | method: 'eth_getTransactionByHash', 54 | params: ['0x00000000000000000000000000000000000000000000000000deadbeefcafe02'], 55 | }, true) 56 | 57 | // code 58 | 59 | cacheTest('getCode for latest block, then for earliest block, should not return cached response on second request', [{ 60 | method: 'eth_getCode', 61 | params: ['0x1234', 'latest'], 62 | }, { 63 | method: 'eth_getCode', 64 | params: ['0x1234', 'earliest'], 65 | }], false) 66 | 67 | cacheTest('getCode for a specific block, then for the one before it, should not return cached response on second request', [{ 68 | method: 'eth_getCode', 69 | params: ['0x1234', '0x3'], 70 | }, { 71 | method: 'eth_getCode', 72 | params: ['0x1234', '0x2'], 73 | }], false) 74 | 75 | // perma-cache implementation was reduced to block-cache when we moved to eth-json-rpc-middleware 76 | // cacheTest('getCode for a specific block, then the one after it, should return cached response on second request', [{ 77 | // method: 'eth_getCode', 78 | // params: ['0x1234', '0x2'], 79 | // }, { 80 | // method: 'eth_getCode', 81 | // params: ['0x1234', '0x3'], 82 | // }], true) 83 | 84 | cacheTest('getCode for an unspecified block, then for the latest, should return cached response on second request', [{ 85 | method: 'eth_getCode', 86 | params: ['0x1234'], 87 | }, { 88 | method: 'eth_getCode', 89 | params: ['0x1234', 'latest'], 90 | }], true) 91 | 92 | // blocks 93 | 94 | cacheTest('getBlockForNumber for latest (1) then block 0', [{ 95 | method: 'eth_getBlockByNumber', 96 | params: ['latest', false], 97 | }, { 98 | method: 'eth_getBlockByNumber', 99 | params: ['0x0', false], 100 | }], false) 101 | 102 | cacheTest('getBlockForNumber for latest (1) then block 1', [{ 103 | method: 'eth_getBlockByNumber', 104 | params: ['latest', false], 105 | }, { 106 | method: 'eth_getBlockByNumber', 107 | params: ['0x1', false], 108 | }], true) 109 | 110 | cacheTest('getBlockForNumber for 0 then block 1', [{ 111 | method: 'eth_getBlockByNumber', 112 | params: ['0x0', false], 113 | }, { 114 | method: 'eth_getBlockByNumber', 115 | params: ['0x1', false], 116 | }], false) 117 | 118 | cacheTest('getBlockForNumber for block 0', [{ 119 | method: 'eth_getBlockByNumber', 120 | params: ['0x0', false], 121 | }, { 122 | method: 'eth_getBlockByNumber', 123 | params: ['0x0', false], 124 | }], true) 125 | 126 | // storage 127 | 128 | cacheTest('getStorageAt for same block should cache', [{ 129 | method: 'eth_getStorageAt', 130 | params: ['0x295a70b2de5e3953354a6a8344e616ed314d7251', '0x0', '0x1234'], 131 | }, { 132 | method: 'eth_getStorageAt', 133 | params: ['0x295a70b2de5e3953354a6a8344e616ed314d7251', '0x0', '0x1234'], 134 | }], true) 135 | 136 | cacheTest('getStorageAt for different block should not cache', [{ 137 | method: 'eth_getStorageAt', 138 | params: ['0x295a70b2de5e3953354a6a8344e616ed314d7251', '0x0', '0x1234'], 139 | }, { 140 | method: 'eth_getStorageAt', 141 | params: ['0x295a70b2de5e3953354a6a8344e616ed314d7251', '0x0', '0x4321'], 142 | }], false) 143 | 144 | 145 | // test helper for caching 146 | // 1. Sets up caching and data provider 147 | // 2. Performs first request 148 | // 3. Performs second request 149 | // 4. checks if cache hit or missed 150 | 151 | function cacheTest(label, payloads, shouldHitCacheOnSecondRequest){ 152 | if (!Array.isArray(payloads)) { 153 | payloads = [payloads, payloads] 154 | } 155 | 156 | test('cache - '+label, function(t){ 157 | t.plan(13) 158 | 159 | // cache layer 160 | var cacheProvider = injectMetrics(new CacheProvider()) 161 | // handle balance 162 | var dataProvider = injectMetrics(new FixtureProvider({ 163 | eth_getBalance: '0xdeadbeef', 164 | eth_getCode: '6060604052600560005560408060156000396000f3606060405260e060020a60003504633fa4f245811460245780635524107714602c575b005b603660005481565b6004356000556022565b6060908152602090f3', 165 | eth_getTransactionByHash: function(payload, next, end) { 166 | // represents a pending tx 167 | if (payload.params[0] === '0x00000000000000000000000000000000000000000000000000deadbeefcafe00') { 168 | end(null, null) 169 | } else if (payload.params[0] === '0x00000000000000000000000000000000000000000000000000deadbeefcafe01') { 170 | end(null, { 171 | hash: '0x00000000000000000000000000000000000000000000000000deadbeefcafe01', 172 | nonce: '0xd', 173 | blockHash: null, 174 | blockNumber: null, 175 | transactionIndex: null, 176 | from: '0xb1cc05ab12928297911695b55ee78c1188f8ef91', 177 | to: '0xfbb1b73c4f0bda4f67dca266ce6ef42f520fbb98', 178 | value: '0xddb66b2addf4800', 179 | gas: '0x5622', 180 | gasPrice: '0xba43b7400', 181 | input: '0x', 182 | }) 183 | } else { 184 | end(null, { 185 | hash: payload.params[0], 186 | nonce: '0xd', 187 | blockHash: '0x1', 188 | blockNumber: '0x1', 189 | transactionIndex: '0x0', 190 | from: '0xb1cc05ab12928297911695b55ee78c1188f8ef91', 191 | to: '0xfbb1b73c4f0bda4f67dca266ce6ef42f520fbb98', 192 | value: '0xddb66b2addf4800', 193 | gas: '0x5622', 194 | gasPrice: '0xba43b7400', 195 | input: '0x', 196 | }) 197 | } 198 | }, 199 | eth_getStorageAt: '0x00000000000000000000000000000000000000000000000000000000deadbeef', 200 | })) 201 | 202 | // handle dummy block 203 | const ganacheProvider = createGanacheProvider() 204 | var blockProvider = injectMetrics(new ProviderSubprovider(ganacheProvider)) 205 | 206 | var engine = new ProviderEngine() 207 | engine.addProvider(cacheProvider) 208 | engine.addProvider(dataProvider) 209 | engine.addProvider(blockProvider) 210 | 211 | engine.on('error', (err) => { 212 | t.ifErr(err) 213 | }) 214 | 215 | series([ 216 | // increment one block from #0 to #1 217 | (next) => ganacheProvider.sendAsync({ id: 1, method: 'evm_mine', params: [] }, next), 218 | // run polling until first block 219 | (next) => { 220 | engine.start() 221 | engine.once('block', () => next()) 222 | }, 223 | // perform cache test 224 | (next) => { 225 | // stop polling 226 | engine.stop() 227 | // clear subprovider metrics 228 | cacheProvider.clearMetrics() 229 | dataProvider.clearMetrics() 230 | blockProvider.clearMetrics() 231 | 232 | // determine which provider will handle the request 233 | const isBlockTest = (payloads[0].method === 'eth_getBlockByNumber') || (payloads[0].method === 'eth_blockNumber') 234 | const handlingProvider = isBlockTest ? blockProvider : dataProvider 235 | 236 | // begin cache test 237 | cacheCheck(t, engine, cacheProvider, handlingProvider, payloads, next) 238 | } 239 | ], (err) => { 240 | t.ifError(err) 241 | t.end() 242 | }) 243 | 244 | function cacheCheck(t, engine, cacheProvider, handlingProvider, payloads, cb) { 245 | var method = payloads[0].method 246 | requestTwice(payloads, function(err, response){ 247 | // first request 248 | t.ifError(err || response.error && response.error.message, 'did not error') 249 | t.ok(response, 'has response') 250 | 251 | t.equal(cacheProvider.getWitnessed(method).length, 1, 'cacheProvider did see "'+method+'"') 252 | t.equal(cacheProvider.getHandled(method).length, 0, 'cacheProvider did NOT handle "'+method+'"') 253 | 254 | t.equal(handlingProvider.getWitnessed(method).length, 1, 'handlingProvider did see "'+method+'"') 255 | t.equal(handlingProvider.getHandled(method).length, 1, 'handlingProvider did handle "'+method+'"') 256 | 257 | }, function(err, response){ 258 | // second request 259 | t.ifError(err || response.error && response.error.message, 'did not error') 260 | t.ok(response, 'has response') 261 | 262 | if (shouldHitCacheOnSecondRequest) { 263 | t.equal(cacheProvider.getWitnessed(method).length, 2, 'cacheProvider did see "'+method+'"') 264 | t.equal(cacheProvider.getHandled(method).length, 1, 'cacheProvider did handle "'+method+'"') 265 | 266 | t.equal(handlingProvider.getWitnessed(method).length, 1, 'handlingProvider did NOT see "'+method+'"') 267 | t.equal(handlingProvider.getHandled(method).length, 1, 'handlingProvider did NOT handle "'+method+'"') 268 | } else { 269 | t.equal(cacheProvider.getWitnessed(method).length, 2, 'cacheProvider did see "'+method+'"') 270 | t.equal(cacheProvider.getHandled(method).length, 0, 'cacheProvider did NOT handle "'+method+'"') 271 | 272 | t.equal(handlingProvider.getWitnessed(method).length, 2, 'handlingProvider did see "'+method+'"') 273 | t.equal(handlingProvider.getHandled(method).length, 2, 'handlingProvider did handle "'+method+'"') 274 | } 275 | 276 | cb() 277 | }) 278 | } 279 | 280 | function requestTwice(payloads, afterFirst, afterSecond){ 281 | engine.sendAsync(createPayload(payloads[0]), function(err, result){ 282 | afterFirst(err, result) 283 | engine.sendAsync(createPayload(payloads[1]), afterSecond) 284 | }) 285 | } 286 | 287 | }) 288 | 289 | } 290 | -------------------------------------------------------------------------------- /test/filters.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const asyncWaterfall = require('async').waterfall 3 | const ProviderEngine = require('../index.js') 4 | const FilterProvider = require('../subproviders/filters.js') 5 | const TestBlockProvider = require('./util/block.js') 6 | const createPayload = require('../util/create-payload.js') 7 | const injectMetrics = require('./util/inject-metrics') 8 | 9 | 10 | filterTest('block filter - basic', { method: 'eth_newBlockFilter' }, 11 | function afterInstall(t, testMeta, response, cb){ 12 | var block = testMeta.block = testMeta.blockProvider.nextBlock() 13 | cb() 14 | }, 15 | function filterChangesOne(t, testMeta, response, cb){ 16 | var results = response.result 17 | var returnedBlockHash = response.result[0] 18 | t.equal(results.length, 1, 'correct number of results') 19 | t.equal(returnedBlockHash, testMeta.block.hash, 'correct result') 20 | cb() 21 | }, 22 | function filterChangesTwo(t, testMeta, response, cb){ 23 | var results = response.result 24 | t.equal(results.length, 0, 'correct number of results') 25 | cb() 26 | } 27 | ) 28 | 29 | filterTest('log filter - basic', { 30 | method: 'eth_newFilter', 31 | params: [{ 32 | topics: ['0x00000000000000000000000000000000000000000000000000deadbeefcafe01'] 33 | }], 34 | }, 35 | function afterInstall(t, testMeta, response, cb){ 36 | testMeta.tx = testMeta.blockProvider.addTx({ 37 | hash: '0x0000000000000000000000000000000000000000000000000000000000000001', 38 | _logTopics: ['0x00000000000000000000000000000000000000000000000000deadbeefcafe01'] 39 | }) 40 | testMeta.badTx = testMeta.blockProvider.addTx({ 41 | _logTopics: ['0x00000000000000000000000000000000000000000000000000deadbeefcafe02'] 42 | }) 43 | var block = testMeta.block = testMeta.blockProvider.nextBlock() 44 | cb() 45 | }, 46 | function filterChangesOne(t, testMeta, response, cb){ 47 | var results = response.result 48 | var matchedLog = response.result[0] 49 | t.equal(results.length, 1, 'correct number of results') 50 | t.equal(matchedLog.transactionHash, testMeta.tx.hash, 'result log matches tx hash') 51 | cb() 52 | }, 53 | function filterChangesTwo(t, testMeta, response, cb){ 54 | var results = response.result 55 | t.equal(results.length, 0, 'correct number of results') 56 | cb() 57 | } 58 | ) 59 | 60 | filterTest('log filter - mixed case', { 61 | method: 'eth_newFilter', 62 | params: [{ 63 | address: '0x00000000000000000000000000000000aAbBcCdD', 64 | topics: ['0x00000000000000000000000000000000000000000000000000DeadBeefCafe01'] 65 | }], 66 | }, 67 | function afterInstall(t, testMeta, response, cb){ 68 | testMeta.tx = testMeta.blockProvider.addTx({ 69 | hash: '0x0000000000000000000000000000000000000000000000000000000000000001', 70 | _logAddress: '0x00000000000000000000000000000000AABBCCDD', 71 | _logTopics: ['0x00000000000000000000000000000000000000000000000000DEADBEEFCAFE01'] 72 | }) 73 | testMeta.badTx = testMeta.blockProvider.addTx({ 74 | _logAddress: '0x00000000000000000000000000000000aAbBcCdD', 75 | _logTopics: ['0x00000000000000000000000000000000000000000000000000DeadBeefCafe02'] 76 | }) 77 | var block = testMeta.block = testMeta.blockProvider.nextBlock() 78 | cb() 79 | }, 80 | function filterChangesOne(t, testMeta, response, cb){ 81 | var results = response.result 82 | var matchedLog = response.result[0] 83 | t.equal(results.length, 1, 'correct number of results') 84 | t.equal(matchedLog && matchedLog.transactionHash, testMeta.tx.hash, 'result log matches tx hash') 85 | cb() 86 | }, 87 | function filterChangesTwo(t, testMeta, response, cb){ 88 | var results = response.result 89 | t.equal(results.length, 0, 'correct number of results') 90 | cb() 91 | } 92 | ) 93 | 94 | filterTest('log filter - address array', { 95 | method: 'eth_newFilter', 96 | params: [{ 97 | address: [ 98 | '0x00000000000000000000000000000000aAbBcCdD', 99 | '0x00000000000000000000000000000000a1b2c3d4'], 100 | topics: ['0x00000000000000000000000000000000000000000000000000DeadBeefCafe01'] 101 | }], 102 | }, 103 | function afterInstall(t, testMeta, response, cb){ 104 | testMeta.tx = testMeta.blockProvider.addTx({ 105 | hash: '0x0000000000000000000000000000000000000000000000000000000000000001', 106 | _logAddress: '0x00000000000000000000000000000000AABBCCDD', 107 | _logTopics: ['0x00000000000000000000000000000000000000000000000000DEADBEEFCAFE01'] 108 | }) 109 | testMeta.badTx = testMeta.blockProvider.addTx({ 110 | _logAddress: '0x00000000000000000000000000000000aAbBcCdD', 111 | _logTopics: ['0x00000000000000000000000000000000000000000000000000DeadBeefCafe02'] 112 | }) 113 | var block = testMeta.block = testMeta.blockProvider.nextBlock() 114 | cb() 115 | }, 116 | function filterChangesOne(t, testMeta, response, cb){ 117 | var results = response.result 118 | var matchedLog = response.result[0] 119 | t.equal(results.length, 1, 'correct number of results') 120 | t.equal(matchedLog && matchedLog.transactionHash, testMeta.tx.hash, 'result log matches tx hash') 121 | cb() 122 | }, 123 | function filterChangesTwo(t, testMeta, response, cb){ 124 | var results = response.result 125 | t.equal(results.length, 0, 'correct number of results') 126 | cb() 127 | } 128 | ) 129 | 130 | filterTest('log filter - and logic', { 131 | method: 'eth_newFilter', 132 | params: [{ 133 | topics: [ 134 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe01', 135 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 136 | ], 137 | }], 138 | }, 139 | function afterInstall(t, testMeta, response, cb){ 140 | testMeta.tx = testMeta.blockProvider.addTx({ 141 | hash: '0x0000000000000000000000000000000000000000000000000000000000000001', 142 | _logTopics: [ 143 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe01', 144 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 145 | ], 146 | }) 147 | testMeta.badTx = testMeta.blockProvider.addTx({ 148 | _logTopics: [ 149 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 150 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe01', 151 | ], 152 | }) 153 | var block = testMeta.block = testMeta.blockProvider.nextBlock() 154 | cb() 155 | }, 156 | function filterChangesOne(t, testMeta, response, cb){ 157 | var results = response.result 158 | var matchedLog = response.result[0] 159 | t.equal(results.length, 1, 'correct number of results') 160 | t.equal(matchedLog && matchedLog.transactionHash, testMeta.tx.hash, 'result log matches tx hash') 161 | cb() 162 | }, 163 | function filterChangesTwo(t, testMeta, response, cb){ 164 | var results = response.result 165 | t.equal(results.length, 0, 'correct number of results') 166 | cb() 167 | } 168 | ) 169 | 170 | filterTest('log filter - or logic', { 171 | method: 'eth_newFilter', 172 | params: [{ 173 | topics: [ 174 | [ 175 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe01', 176 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 177 | ], 178 | ], 179 | }], 180 | }, 181 | function afterInstall(t, testMeta, response, cb){ 182 | testMeta.tx1 = testMeta.blockProvider.addTx({ 183 | hash: '0x0000000000000000000000000000000000000000000000000000000000000001', 184 | _logTopics: [ 185 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe01', 186 | ], 187 | }) 188 | testMeta.tx2 = testMeta.blockProvider.addTx({ 189 | hash: '0x0000000000000000000000000000000000000000000000000000000000000002', 190 | _logTopics: [ 191 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 192 | ], 193 | }) 194 | testMeta.badTx = testMeta.blockProvider.addTx({ 195 | _logTopics: [ 196 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe03', 197 | ], 198 | }) 199 | var block = testMeta.block = testMeta.blockProvider.nextBlock() 200 | cb() 201 | }, 202 | function filterChangesOne(t, testMeta, response, cb){ 203 | var results = response.result 204 | var matchedLog1 = response.result[0] 205 | var matchedLog2 = response.result[1] 206 | t.equal(results.length, 2, 'correct number of results') 207 | t.equal(matchedLog1.transactionHash, testMeta.tx1.hash, 'result log matches tx hash') 208 | t.equal(matchedLog2.transactionHash, testMeta.tx2.hash, 'result log matches tx hash') 209 | cb() 210 | }, 211 | function filterChangesTwo(t, testMeta, response, cb){ 212 | var results = response.result 213 | t.equal(results.length, 0, 'correct number of results') 214 | cb() 215 | } 216 | ) 217 | 218 | filterTest('log filter - wildcard logic', { 219 | method: 'eth_newFilter', 220 | params: [{ 221 | topics: [ 222 | null, 223 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 224 | ], 225 | }], 226 | }, 227 | function afterInstall(t, testMeta, response, cb){ 228 | testMeta.tx1 = testMeta.blockProvider.addTx({ 229 | hash: '0x0000000000000000000000000000000000000000000000000000000000000001', 230 | _logTopics: [ 231 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe01', 232 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 233 | ], 234 | }) 235 | testMeta.tx2 = testMeta.blockProvider.addTx({ 236 | hash: '0x0000000000000000000000000000000000000000000000000000000000000002', 237 | _logTopics: [ 238 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 239 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 240 | ], 241 | }) 242 | testMeta.badTx = testMeta.blockProvider.addTx({ 243 | _logTopics: [ 244 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe01', 245 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe01', 246 | ], 247 | }) 248 | var block = testMeta.block = testMeta.blockProvider.nextBlock() 249 | cb() 250 | }, 251 | function filterChangesOne(t, testMeta, response, cb){ 252 | var results = response.result 253 | var matchedLog1 = response.result[0] 254 | var matchedLog2 = response.result[1] 255 | t.equal(results.length, 2, 'correct number of results') 256 | t.equal(matchedLog1.transactionHash, testMeta.tx1.hash, 'result log matches tx hash') 257 | t.equal(matchedLog2.transactionHash, testMeta.tx2.hash, 'result log matches tx hash') 258 | cb() 259 | }, 260 | function filterChangesTwo(t, testMeta, response, cb){ 261 | var results = response.result 262 | t.equal(results.length, 0, 'correct number of results') 263 | cb() 264 | } 265 | ) 266 | 267 | filterTest('eth_getFilterLogs called with non log filter id should return []', { method: 'eth_newBlockFilter' }, 268 | function afterInstall(t, testMeta, response, cb){ 269 | var block = testMeta.block = testMeta.blockProvider.nextBlock() 270 | testMeta.engine.once('block', function(){ 271 | testMeta.engine.sendAsync(createPayload({ method: 'eth_getFilterLogs', params: [testMeta.filterId] }), function(err, response){ 272 | t.ifError(err, 'did not error') 273 | t.ok(response, 'has response') 274 | t.ok(response.result, 'has response.result') 275 | 276 | t.equal(testMeta.filterProvider.getWitnessed('eth_getFilterLogs').length, 1, 'filterProvider did see "eth_getFilterLogs') 277 | t.equal(testMeta.filterProvider.getHandled('eth_getFilterLogs').length, 1, 'filterProvider did handle "eth_getFilterLogs') 278 | 279 | t.equal(response.result.length, 0, 'eth_getFilterLogs returned an empty result for a non log filter') 280 | cb() 281 | }) 282 | }) 283 | }) 284 | 285 | // util 286 | 287 | function filterTest(label, filterPayload, afterInstall, filterChangesOne, filterChangesTwo){ 288 | test('filters - '+label, function(t){ 289 | // t.plan(8) 290 | 291 | // install filter 292 | // new block 293 | // check filter 294 | 295 | var testMeta = {} 296 | 297 | // handle "test_rpc" 298 | var filterProvider = testMeta.filterProvider = injectMetrics(new FilterProvider()) 299 | // handle block requests 300 | var blockProvider = testMeta.blockProvider = injectMetrics(new TestBlockProvider()) 301 | 302 | var engine = testMeta.engine = new ProviderEngine({ 303 | pollingInterval: 20, 304 | pollingShouldUnref: false, 305 | }) 306 | engine.addProvider(filterProvider) 307 | engine.addProvider(blockProvider) 308 | 309 | asyncWaterfall([ 310 | // wait for first block 311 | (cb) => { 312 | engine.once('block', () => cb()) 313 | engine.start() 314 | }, 315 | // install block filter 316 | (cb) => { 317 | engine.sendAsync(createPayload(filterPayload), cb) 318 | }, 319 | // validate install 320 | (response, cb) => { 321 | t.ok(response, 'has response') 322 | 323 | var method = filterPayload.method 324 | 325 | t.equal(filterProvider.getWitnessed(method).length, 1, 'filterProvider did see "'+method+'"') 326 | t.equal(filterProvider.getHandled(method).length, 1, 'filterProvider did handle "'+method+'"') 327 | 328 | var filterId = testMeta.filterId = response.result 329 | 330 | afterInstall(t, testMeta, response, cb) 331 | }, 332 | (cb) => { 333 | if (filterChangesOne) { 334 | checkFilterChangesOne(cb) 335 | } else { 336 | cb() 337 | } 338 | }, 339 | (cb) => { 340 | if (filterChangesTwo) { 341 | checkFilterChangesTwo(cb) 342 | } else { 343 | cb() 344 | } 345 | }, 346 | ], (err) => { 347 | t.ifError(err, 'did not error') 348 | engine.stop() 349 | t.end() 350 | }) 351 | 352 | function checkFilterChangesOne (done) { 353 | asyncWaterfall([ 354 | // wait next block 355 | (cb) => { 356 | engine.once('block', () => cb()) 357 | }, 358 | // check filter one 359 | (cb) => { 360 | var filterId = testMeta.filterId 361 | engine.sendAsync(createPayload({ method: 'eth_getFilterChanges', params: [filterId] }), cb) 362 | }, 363 | (response, cb) => { 364 | t.ok(response, 'has response') 365 | 366 | t.equal(filterProvider.getWitnessed('eth_getFilterChanges').length, 1, 'filterProvider did see "eth_getFilterChanges"') 367 | t.equal(filterProvider.getHandled('eth_getFilterChanges').length, 1, 'filterProvider did handle "eth_getFilterChanges"') 368 | 369 | filterChangesOne(t, testMeta, response, cb) 370 | } 371 | ], done) 372 | } 373 | 374 | function checkFilterChangesTwo (done) { 375 | asyncWaterfall([ 376 | // check filter two 377 | (cb) => { 378 | var filterId = testMeta.filterId 379 | engine.sendAsync(createPayload({ method: 'eth_getFilterChanges', params: [filterId] }), cb) 380 | }, 381 | (response, cb) => { 382 | t.ok(response, 'has response') 383 | 384 | t.equal(filterProvider.getWitnessed('eth_getFilterChanges').length, 2, 'filterProvider did see "eth_getFilterChanges"') 385 | t.equal(filterProvider.getHandled('eth_getFilterChanges').length, 2, 'filterProvider did handle "eth_getFilterChanges"') 386 | 387 | filterChangesTwo(t, testMeta, response, cb) 388 | }, 389 | ], done) 390 | } 391 | 392 | }) 393 | } 394 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('./basic') 2 | require('./cache-utils') 3 | require('./cache') 4 | require('./inflight-cache') 5 | require('./filters') 6 | require('./subscriptions') 7 | require('./solc') 8 | require('./wallet') 9 | require('./subproviders/sanitizer') 10 | require('./subproviders/vm') 11 | // require('./subproviders/ipc') 12 | // require('./subproviders/etherscan') 13 | 14 | -------------------------------------------------------------------------------- /test/inflight-cache.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const asyncParallel = require('async/parallel') 3 | const asyncSeries = require('async/series') 4 | const createGanacheProvider = require('ganache').provider 5 | const ProviderEngine = require('../index.js') 6 | const FixtureProvider = require('../subproviders/fixture.js') 7 | const InflightCacheProvider = require('../subproviders/inflight-cache.js') 8 | const ProviderSubprovider = require('../subproviders/provider.js') 9 | const createPayload = require('../util/create-payload.js') 10 | const injectMetrics = require('./util/inject-metrics') 11 | 12 | inflightTest('getBalance for latest', { 13 | method: 'eth_getBalance', 14 | params: ['0xabcd', 'latest'], 15 | }, true) 16 | 17 | inflightTest('getBlock by number (0)', { 18 | method: 'eth_getBlockByNumber', 19 | params: ['0x0', false], 20 | }, true) 21 | 22 | // latest is always forwarded for eth_getBlockByNumber 23 | inflightTest('getBlock for latest', { 24 | method: 'eth_getBlockByNumber', 25 | params: ['latest', false], 26 | }, false) 27 | 28 | inflightTest('getBlock for latest (1) then 0', [{ 29 | method: 'eth_getBlockByNumber', 30 | params: ['latest', false], 31 | }, { 32 | method: 'eth_getBlockByNumber', 33 | params: ['0x0', false], 34 | }], false) 35 | 36 | // inflight-cache does not resolve tags like "latest", so we dont know that latest === 0x1 in this case 37 | inflightTest('getBlock for latest (1) then 1', [{ 38 | method: 'eth_getBlockByNumber', 39 | params: ['latest', false], 40 | }, { 41 | method: 'eth_getBlockByNumber', 42 | params: ['0x1', false], 43 | }], false) 44 | 45 | function inflightTest(label, payloads, shouldHitCacheOnSecondRequest){ 46 | if (!Array.isArray(payloads)) { 47 | payloads = [payloads, payloads] 48 | } 49 | 50 | test('inflight cache - '+label, function(t){ 51 | t.plan(6) 52 | 53 | // cache layer 54 | var cacheProvider = injectMetrics(new InflightCacheProvider()) 55 | // handle balance 56 | var dataProvider = injectMetrics(new FixtureProvider({ 57 | eth_getBalance: '0xdeadbeef', 58 | })) 59 | // handle dummy block 60 | const ganacheProvider = createGanacheProvider({ 61 | vmErrorsOnRpcResponse: true, 62 | }) 63 | var blockProvider = injectMetrics(new ProviderSubprovider(ganacheProvider)) 64 | 65 | var engine = new ProviderEngine() 66 | engine.addProvider(cacheProvider) 67 | engine.addProvider(dataProvider) 68 | engine.addProvider(blockProvider) 69 | 70 | asyncSeries([ 71 | // increment one block from #0 to #1 72 | (next) => ganacheProvider.sendAsync({ id: 1, method: 'evm_mine', params: [] }, next), 73 | // run polling until first block 74 | (next) => { 75 | engine.start() 76 | engine.once('block', () => next()) 77 | }, 78 | // perform test 79 | (next) => { 80 | // stop polling 81 | engine.stop() 82 | // clear subprovider metrics 83 | cacheProvider.clearMetrics() 84 | dataProvider.clearMetrics() 85 | blockProvider.clearMetrics() 86 | 87 | // determine which provider will handle the request 88 | const isBlockTest = (payloads[0].method === 'eth_getBlockByNumber') 89 | const handlingProvider = isBlockTest ? blockProvider : dataProvider 90 | 91 | // begin cache test 92 | cacheCheck(t, engine, cacheProvider, handlingProvider, payloads, next) 93 | }, 94 | ], (err) => { 95 | t.ifErr(err) 96 | t.end() 97 | }) 98 | 99 | function cacheCheck(t, engine, cacheProvider, handlingProvider, payloads, cb) { 100 | var method = payloads[0].method 101 | requestSimultaneous(payloads, noop, noop, function(err, responses){ 102 | // first request 103 | t.ifError(err, 'did not error') 104 | t.ok(responses && responses.filter(Boolean).length, 'has responses') 105 | 106 | if (shouldHitCacheOnSecondRequest) { 107 | 108 | t.equal(cacheProvider.getWitnessed(method).length, 2, 'cacheProvider did see "'+method+'"') 109 | t.equal(cacheProvider.getHandled(method).length, 1, 'cacheProvider did NOT handle "'+method+'"') 110 | 111 | t.equal(handlingProvider.getWitnessed(method).length, 1, 'handlingProvider did see "'+method+'"') 112 | t.equal(handlingProvider.getHandled(method).length, 1, 'handlingProvider did handle "'+method+'"') 113 | 114 | } else { 115 | 116 | t.equal(cacheProvider.getWitnessed(method).length, 2, 'cacheProvider did see "'+method+'"') 117 | t.equal(cacheProvider.getHandled(method).length, 0, 'cacheProvider did NOT handle "'+method+'"') 118 | 119 | t.equal(handlingProvider.getWitnessed(method).length, 2, 'handlingProvider did see "'+method+'"') 120 | t.equal(handlingProvider.getHandled(method).length, 2, 'handlingProvider did handle "'+method+'"') 121 | 122 | } 123 | 124 | }) 125 | } 126 | 127 | function requestSimultaneous(payloads, afterFirst, afterSecond, cb){ 128 | asyncParallel([ 129 | (cb) => { 130 | engine.sendAsync(createPayload(payloads[0]), (err, result) => { 131 | afterFirst(err, result) 132 | cb(err, result) 133 | }) 134 | }, 135 | (cb) => { 136 | engine.sendAsync(createPayload(payloads[1]), (err, result) => { 137 | afterSecond(err, result) 138 | cb(err, result) 139 | }) 140 | }, 141 | ], cb) 142 | } 143 | }) 144 | 145 | } 146 | 147 | function noop(){} 148 | -------------------------------------------------------------------------------- /test/nonce.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const { TransactionFactory } = require('@ethereumjs/tx') 3 | const ethUtil = require('@ethereumjs/util') 4 | const ProviderEngine = require('../index.js') 5 | const FixtureProvider = require('../subproviders/fixture.js') 6 | const NonceTracker = require('../subproviders/nonce-tracker.js') 7 | const HookedWalletProvider = require('../subproviders/hooked-wallet.js') 8 | const TestBlockProvider = require('./util/block.js') 9 | const createPayload = require('../util/create-payload.js') 10 | const injectMetrics = require('./util/inject-metrics') 11 | 12 | test('basic nonce tracking', function(t){ 13 | t.plan(11) 14 | 15 | var privateKey = Buffer.from('cccd8f4d88de61f92f3747e4a9604a0395e6ad5138add4bec4a2ddf231ee24f9', 'hex') 16 | var address = Buffer.from('1234362ef32bcd26d3dd18ca749378213625ba0b', 'hex') 17 | var addressHex = '0x'+address.toString('hex') 18 | 19 | // sign all tx's 20 | var providerA = injectMetrics(new HookedWalletProvider({ 21 | signTransaction: function(txParams, cb){ 22 | var tx = TransactionFactory.fromTxData(txParams) 23 | tx.sign(privateKey) 24 | var rawTx = '0x'+tx.serialize().toString('hex') 25 | cb(null, rawTx) 26 | }, 27 | })) 28 | 29 | // handle nonce requests 30 | var providerB = injectMetrics(new NonceTracker()) 31 | // handle all bottom requests 32 | var providerC = injectMetrics(new FixtureProvider({ 33 | eth_gasPrice: '0x1234', 34 | eth_getTransactionCount: '0x00', 35 | eth_sendRawTransaction: function(payload, next, done){ 36 | var rawTx = ethUtil.toBuffer(payload.params[0]) 37 | var tx = new TransactionFactory.fromTxData(rawTx) 38 | var hash = '0x'+tx.hash().toString('hex') 39 | done(null, hash) 40 | }, 41 | })) 42 | // handle block requests 43 | var providerD = injectMetrics(new TestBlockProvider()) 44 | 45 | var engine = new ProviderEngine() 46 | engine.addProvider(providerA) 47 | engine.addProvider(providerB) 48 | engine.addProvider(providerC) 49 | engine.addProvider(providerD) 50 | 51 | var txPayload = { 52 | method: 'eth_sendTransaction', 53 | params: [{ 54 | from: addressHex, 55 | to: addressHex, 56 | value: '0x01', 57 | gas: '0x1234567890', 58 | }] 59 | } 60 | 61 | engine.start() 62 | engine.sendAsync(createPayload(txPayload), function(err, response){ 63 | t.ifError(err, 'did not error') 64 | t.ok(response, 'has response') 65 | 66 | // tx nonce 67 | t.equal(providerB.getWitnessed('eth_getTransactionCount').length, 1, 'providerB did see "eth_getTransactionCount"') 68 | t.equal(providerB.getHandled('eth_getTransactionCount').length, 0, 'providerB did NOT handle "eth_getTransactionCount"') 69 | t.equal(providerC.getWitnessed('eth_getTransactionCount').length, 1, 'providerC did see "eth_getTransactionCount"') 70 | t.equal(providerC.getHandled('eth_getTransactionCount').length, 1, 'providerC did handle "eth_getTransactionCount"') 71 | // send raw tx 72 | t.equal(providerC.getWitnessed('eth_sendRawTransaction').length, 1, 'providerC did see "eth_sendRawTransaction"') 73 | t.equal(providerC.getHandled('eth_sendRawTransaction').length, 1, 'providerC did handle "eth_sendRawTransaction"') 74 | 75 | engine.sendAsync(createPayload({ 76 | method: 'eth_getTransactionCount', 77 | params: [addressHex, 'pending'], 78 | }), function(err, response){ 79 | t.ifError(err, 'did not error') 80 | t.ok(response, 'has response') 81 | 82 | // tx nonce did increment 83 | t.equal(response.result, '0x01', 'the provider gives the correct pending nonce') 84 | 85 | engine.stop() 86 | t.end() 87 | 88 | }) 89 | 90 | }) 91 | 92 | }) 93 | 94 | 95 | test('nonce tracking - on error', function(t){ 96 | t.plan(11) 97 | 98 | var privateKey = Buffer.from('cccd8f4d88de61f92f3747e4a9604a0395e6ad5138add4bec4a2ddf231ee24f9', 'hex') 99 | var address = Buffer.from('1234362ef32bcd26d3dd18ca749378213625ba0b', 'hex') 100 | var addressHex = '0x'+address.toString('hex') 101 | 102 | // sign all tx's 103 | var providerA = injectMetrics(new HookedWalletProvider({ 104 | signTransaction: function(txParams, cb){ 105 | var tx = TransactionFactory.fromTxData(txParams) 106 | tx.sign(privateKey) 107 | var rawTx = '0x'+tx.serialize().toString('hex') 108 | cb(null, rawTx) 109 | }, 110 | })) 111 | 112 | // handle nonce requests 113 | var providerB = injectMetrics(new NonceTracker()) 114 | // handle all bottom requests 115 | var providerC = injectMetrics(new FixtureProvider({ 116 | eth_gasPrice: '0x1234', 117 | eth_getTransactionCount: '0x00', 118 | eth_sendRawTransaction: function(payload, next, done){ 119 | done(new Error('Always fail.')) 120 | }, 121 | })) 122 | // handle block requests 123 | var providerD = injectMetrics(new TestBlockProvider()) 124 | 125 | var engine = new ProviderEngine() 126 | engine.addProvider(providerA) 127 | engine.addProvider(providerB) 128 | engine.addProvider(providerC) 129 | engine.addProvider(providerD) 130 | 131 | var txPayload = { 132 | method: 'eth_sendTransaction', 133 | params: [{ 134 | from: addressHex, 135 | to: addressHex, 136 | value: '0x01', 137 | gas: '0x1234567890', 138 | }] 139 | } 140 | 141 | engine.start() 142 | engine.sendAsync(createPayload(txPayload), function(err, response){ 143 | t.ok(err, 'did not error') 144 | t.ok(response.error, 'has response') 145 | 146 | // tx nonce 147 | t.equal(providerB.getWitnessed('eth_getTransactionCount').length, 1, 'providerB did see "eth_getTransactionCount"') 148 | t.equal(providerB.getHandled('eth_getTransactionCount').length, 0, 'providerB did NOT handle "eth_getTransactionCount"') 149 | t.equal(providerC.getWitnessed('eth_getTransactionCount').length, 1, 'providerC did see "eth_getTransactionCount"') 150 | t.equal(providerC.getHandled('eth_getTransactionCount').length, 1, 'providerC did handle "eth_getTransactionCount"') 151 | 152 | // send raw tx 153 | t.equal(providerC.getWitnessed('eth_sendRawTransaction').length, 1, 'providerC did see "eth_sendRawTransaction"') 154 | t.equal(providerC.getHandled('eth_sendRawTransaction').length, 1, 'providerC did handle "eth_sendRawTransaction"') 155 | 156 | engine.sendAsync(createPayload({ 157 | method: 'eth_getTransactionCount', 158 | params: [addressHex, 'pending'], 159 | }), function(err, response){ 160 | t.ifError(err, 'did not error') 161 | t.ok(response, 'has response') 162 | 163 | // tx nonce did NOT increment 164 | t.equal(response.result, '0x00', 'the provider gives the correct pending nonce') 165 | 166 | engine.stop() 167 | t.end() 168 | 169 | }) 170 | 171 | }) 172 | 173 | }) 174 | -------------------------------------------------------------------------------- /test/solc.js: -------------------------------------------------------------------------------- 1 | // const test = require('tape') 2 | // const ProviderEngine = require('../index.js') 3 | // const PassthroughProvider = require('./util/passthrough.js') 4 | // const FixtureProvider = require('../subproviders/fixture.js') 5 | // const SolcProvider = require('../subproviders/solc.js') 6 | // const TestBlockProvider = require('./util/block.js') 7 | // const createPayload = require('../util/create-payload.js') 8 | // const injectMetrics = require('./util/inject-metrics') 9 | // const solc = require('solc') 10 | // 11 | // test('solc test', function(t){ 12 | // t.plan(15) 13 | // 14 | // // handle solc 15 | // var providerA = injectMetrics(new SolcProvider()) 16 | // // handle block requests 17 | // var providerB = injectMetrics(new TestBlockProvider()) 18 | // 19 | // var engine = new ProviderEngine() 20 | // engine.addProvider(providerA) 21 | // engine.addProvider(providerB) 22 | // 23 | // var contractSource = 'pragma solidity ^0.4.2; contract test { function multiply(uint a) returns(uint d) { return a * 7; } }' 24 | // 25 | // engine.start() 26 | // engine.sendAsync(createPayload({ method: 'eth_compileSolidity', params: [ contractSource ] }), function(err, response){ 27 | // t.ifError(err, 'did not error') 28 | // t.ok(response, 'has response') 29 | // 30 | // t.ok(response.result.code, 'has bytecode') 31 | // t.equal(response.result.info.source, contractSource) 32 | // t.equal(response.result.info.compilerVersion, solc.version()) 33 | // t.ok(response.result.info.abiDefinition, 'has abiDefinition') 34 | // 35 | // t.equal(providerA.getWitnessed('eth_compileSolidity').length, 1, 'providerA did see "eth_compileSolidity"') 36 | // t.equal(providerA.getHandled('eth_compileSolidity').length, 1, 'providerA did handle "eth_compileSolidity"') 37 | // 38 | // t.equal(providerB.getWitnessed('eth_compileSolidity').length, 0, 'providerB did NOT see "eth_compileSolidity"') 39 | // t.equal(providerB.getHandled('eth_compileSolidity').length, 0, 'providerB did NOT handle "eth_compileSolidity"') 40 | // 41 | // engine.sendAsync(createPayload({ method: 'eth_getCompilers', params: [] }), function(err, response){ 42 | // t.ifError(err, 'did not error') 43 | // t.ok(response, 'has response') 44 | // 45 | // t.ok(response.result instanceof Array, 'has array') 46 | // t.equal(response.result.length, 1, 'has length of 1') 47 | // t.equal(response.result[0], 'solidity', 'has "solidity"') 48 | // 49 | // engine.stop() 50 | // t.end() 51 | // }) 52 | // }) 53 | // }) 54 | // 55 | // 56 | // test('solc error test', function(t){ 57 | // // handle solc 58 | // var providerA = injectMetrics(new SolcProvider()) 59 | // // handle block requests 60 | // var providerB = injectMetrics(new TestBlockProvider()) 61 | // 62 | // var engine = new ProviderEngine() 63 | // engine.addProvider(providerA) 64 | // engine.addProvider(providerB) 65 | // 66 | // var contractSource = 'pragma solidity ^0.4.2; contract error { error() }' 67 | // 68 | // engine.start() 69 | // engine.sendAsync(createPayload({ method: 'eth_compileSolidity', params: [ contractSource ] }), function(err, response){ 70 | // t.equal(typeof err, 'string', 'error type is string') 71 | // engine.stop() 72 | // t.end() 73 | // }) 74 | // }) 75 | -------------------------------------------------------------------------------- /test/subproviders/etherscan.js: -------------------------------------------------------------------------------- 1 | const { keccak_256 } = require('ethereum-cryptography'); 2 | const test = require('tape') 3 | const ProviderEngine = require('../../index.js') 4 | const createPayload = require('../../util/create-payload.js') 5 | const EtherscanSubprovider = require('../../subproviders/etherscan') 6 | 7 | test('etherscan eth_getBlockTransactionCountByNumber', function(t) { 8 | t.plan(3) 9 | 10 | var engine = new ProviderEngine() 11 | var etherscan = new EtherscanSubprovider() 12 | engine.addProvider(etherscan) 13 | engine.start() 14 | engine.sendAsync(createPayload({ 15 | method: 'eth_getBlockTransactionCountByNumber', 16 | params: [ 17 | '0x132086' 18 | ], 19 | }), function(err, response){ 20 | t.ifError(err, 'throw no error') 21 | t.ok(response, 'has response') 22 | t.equal(response.result, '0x8') 23 | t.end() 24 | }) 25 | }) 26 | 27 | test('etherscan eth_getTransactionByHash', function(t) { 28 | t.plan(3) 29 | 30 | var engine = new ProviderEngine() 31 | var etherscan = new EtherscanSubprovider() 32 | engine.addProvider(etherscan) 33 | engine.start() 34 | engine.sendAsync(createPayload({ 35 | method: 'eth_getTransactionByHash', 36 | params: [ 37 | '0xe420d77c4f8b5bf95021fa049b634d5e3f051752a14fb7c6a8f1333c37cdf817' 38 | ], 39 | }), function(err, response){ 40 | t.ifError(err, 'throw no error') 41 | t.ok(response, 'has response') 42 | t.equal(response.result.nonce, '0xd', 'nonce matches known nonce') 43 | t.end() 44 | }) 45 | }) 46 | 47 | test('etherscan eth_blockNumber', function(t) { 48 | t.plan(3) 49 | 50 | var engine = new ProviderEngine() 51 | var etherscan = new EtherscanSubprovider() 52 | engine.addProvider(etherscan) 53 | engine.start() 54 | engine.sendAsync(createPayload({ 55 | method: 'eth_blockNumber', 56 | params: [], 57 | }), function(err, response){ 58 | t.ifError(err, 'throw no error') 59 | t.ok(response, 'has response') 60 | t.notEqual(response.result, '0x', 'block number does not equal 0x') 61 | t.end() 62 | }) 63 | }) 64 | 65 | test('etherscan eth_getBlockByNumber', function(t) { 66 | t.plan(3) 67 | 68 | var engine = new ProviderEngine() 69 | var etherscan = new EtherscanSubprovider() 70 | engine.addProvider(etherscan) 71 | engine.start() 72 | engine.sendAsync(createPayload({ 73 | method: 'eth_getBlockByNumber', 74 | params: [ 75 | '0x149a2a', 76 | true 77 | ], 78 | }), function(err, response){ 79 | t.ifError(err, 'throw no error') 80 | t.ok(response, 'has response') 81 | t.equal(response.result.nonce, '0x80fdd9b71954f9fc', 'nonce matches known nonce') 82 | t.end() 83 | }) 84 | }) 85 | 86 | test('etherscan eth_getBalance', function(t) { 87 | t.plan(3) 88 | 89 | var engine = new ProviderEngine() 90 | var etherscan = new EtherscanSubprovider() 91 | engine.addProvider(etherscan) 92 | engine.start() 93 | engine.sendAsync(createPayload({ 94 | method: 'eth_getBalance', 95 | params: [ 96 | '0xa601ea86ae7297e78a54f4b6937fbc222b9d87f4', 97 | 'latest' 98 | ], 99 | }), function(err, response){ 100 | t.ifError(err, 'throw no error') 101 | t.ok(response, 'has response') 102 | t.notEqual(response.result, '0', 'balance does not equal zero') 103 | t.end() 104 | }) 105 | }) 106 | 107 | test('etherscan eth_call', function(t) { 108 | t.plan(3) 109 | 110 | var signature = Buffer.concat([keccak_256(Buffer.from("getLatestBlock()"), 256)], 4).toString('hex'); 111 | var engine = new ProviderEngine() 112 | var etherscan = new EtherscanSubprovider() 113 | engine.addProvider(etherscan) 114 | engine.start() 115 | engine.sendAsync(createPayload({ 116 | method: 'eth_call', 117 | params: [{ 118 | to: '0x4EECf99D543B278106ac0c0e8ffe616F2137f10a', 119 | data : signature 120 | }, 121 | 'latest' 122 | ], 123 | }), function(err, response){ 124 | t.ifError(err, 'throw no error') 125 | t.ok(response, 'has response') 126 | t.notEqual(response.result, '0x', 'eth_call to getLatestBlock() does not equal 0x') 127 | t.end() 128 | }) 129 | }) 130 | -------------------------------------------------------------------------------- /test/subproviders/ipc.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const ProviderEngine = require('../../index.js') 3 | const createPayload = require('../../util/create-payload.js') 4 | const IpcSubprovider = require('../../subproviders/ipc') 5 | const socketPath = process.argv[2]; // e.g. '/root/.ethereum/geth.ipc' 6 | 7 | test('ipc personal_listAccounts', function(t) { 8 | t.plan(3) 9 | var engine = new ProviderEngine() 10 | var ipc = new IpcSubprovider({ipcPath : socketPath}); 11 | engine.addProvider(ipc) 12 | engine.start() 13 | engine.sendAsync(createPayload({ 14 | method: 'personal_listAccounts', 15 | params: [], 16 | }), function(err, response){ 17 | t.ifError(err, 'throw no error') 18 | t.ok(response, 'has response') 19 | t.equal(typeof response.result[0], 'string') 20 | t.end() 21 | }) 22 | }) 23 | 24 | test('ipc personal_newAccount', function(t) { 25 | t.plan(3) 26 | var engine = new ProviderEngine() 27 | var ipc = new IpcSubprovider({ipcPath : socketPath}); 28 | engine.addProvider(ipc) 29 | engine.start() 30 | engine.sendAsync(createPayload({ 31 | method: 'personal_newAccount', 32 | params: ['test'], 33 | }), function(err, response){ 34 | t.ifError(err, 'throw no error') 35 | t.ok(response, 'has response') 36 | t.equal(response.result.length, 42); 37 | t.end() 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /test/subproviders/sanitizer.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const ProviderEngine = require('../../index.js') 3 | const createPayload = require('../../util/create-payload.js') 4 | const FixtureProvider = require('../../subproviders/fixture.js') 5 | const SanitizerSubprovider = require('../../subproviders/sanitizer') 6 | const MockSubprovider = require('../util/mock-subprovider') 7 | const TestBlockProvider = require('../util/block.js') 8 | const extend = require('xtend') 9 | 10 | test('Sanitizer removes unknown keys', function(t) { 11 | t.plan(8) 12 | 13 | var engine = new ProviderEngine() 14 | 15 | var sanitizer = new SanitizerSubprovider() 16 | engine.addProvider(sanitizer) 17 | 18 | // test sanitization 19 | var checkSanitizer = new FixtureProvider({ 20 | test_unsanitized: (req, next, end) => { 21 | if (req.method !== 'test_unsanitized') return next() 22 | const firstParam = payload.params[0] 23 | t.notOk(firstParam && firstParam.foo) 24 | t.equal(firstParam.gas, '0x01') 25 | t.equal(firstParam.data, '0x01') 26 | t.equal(firstParam.fromBlock, 'latest') 27 | t.equal(firstParam.topics.length, 3) 28 | t.equal(firstParam.topics[1], '0x0a') 29 | end(null, { baz: 'bam' }) 30 | }, 31 | }) 32 | engine.addProvider(checkSanitizer) 33 | 34 | // handle block requests 35 | var blockProvider = new TestBlockProvider() 36 | engine.addProvider(blockProvider) 37 | 38 | engine.start() 39 | 40 | var payload = { 41 | method: 'test_unsanitized', 42 | params: [{ 43 | foo: 'bar', 44 | gas: '0x01', 45 | data: '01', 46 | fromBlock: 'latest', 47 | topics: [ 48 | null, 49 | '0X0A', 50 | '0x03', 51 | ], 52 | }], 53 | } 54 | engine.sendAsync(payload, function (err, response) { 55 | engine.stop() 56 | t.notOk(err, 'no error') 57 | t.equal(response.result.baz, 'bam', 'result was received correctly') 58 | t.end() 59 | }) 60 | }) -------------------------------------------------------------------------------- /test/subproviders/vm.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const async = require('async') 3 | const { toBuffer } = require('@ethereumjs/util') 4 | const ProviderEngine = require('../../index.js') 5 | const VmSubprovider = require('../../subproviders/vm') 6 | const TestBlockProvider = require('../util/block.js') 7 | const RpcSubprovider = require('../../subproviders/rpc') 8 | const createPayload = require('../../util/create-payload.js') 9 | const rpcHexEncoding = require('../../util/rpc-hex-encoding.js') 10 | 11 | test('binary search eth_estimateGas implementation', function(t) { 12 | var gasNeededScenarios = [ 13 | { 14 | gasNeeded: 5, 15 | gasEstimate: 1150, 16 | numIterations: 12, 17 | }, 18 | { 19 | gasNeeded: 50000, 20 | gasEstimate: 50046, 21 | numIterations: 13, 22 | }, 23 | { 24 | gasNeeded: 4712387, 25 | gasEstimate: 4712387, 26 | numIterations: 23, // worst-case scenario 27 | }, 28 | ] 29 | 30 | async.eachSeries(gasNeededScenarios, function(scenario, next) { 31 | var engine = new ProviderEngine() 32 | var vmSubprovider = new VmSubprovider() 33 | var numIterations = 0 34 | 35 | // Stub runVm so that it behaves as if it needs gasNeeded to run and increments numIterations 36 | vmSubprovider.runVm = function(payload, cb) { 37 | numIterations++ 38 | if (payload.params[0].gas < scenario.gasNeeded) { 39 | cb(new Error('fake out of gas')) 40 | } else { 41 | cb(null, { 42 | gasUsed: toBuffer(scenario.gasNeeded), 43 | }); 44 | } 45 | } 46 | engine.addProvider(vmSubprovider) 47 | engine.addProvider(new TestBlockProvider()); 48 | engine.start() 49 | 50 | engine.sendAsync(createPayload({ 51 | method: 'eth_estimateGas', 52 | params: [{}, 'latest'], 53 | }), function(err, response) { 54 | t.ifError(err, 'did not error') 55 | t.ok(response, 'has response') 56 | 57 | var gasEstimationInt = rpcHexEncoding.quantityHexToInt(response.result) 58 | t.equal(gasEstimationInt, scenario.gasEstimate, 'properly calculates gas needed') 59 | t.equal(numIterations, scenario.numIterations, 'ran expected number of iterations') 60 | 61 | engine.stop() 62 | next() 63 | }) 64 | }, function(err) { 65 | t.ifError(err, 'did not error') 66 | t.end() 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /test/subscriptions.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const asyncSeries = require('async/series') 3 | const ProviderEngine = require('../index.js') 4 | const SubscriptionSubprovider = require('../subproviders/subscriptions.js') 5 | const TestBlockProvider = require('./util/block.js') 6 | const createPayload = require('../util/create-payload.js') 7 | const injectMetrics = require('./util/inject-metrics') 8 | 9 | subscriptionTest('basic block subscription', {}, { 10 | method: 'eth_subscribe', 11 | params: ['newHeads'] 12 | }, 13 | function afterInstall(t, testMeta, response, cb){ 14 | // nothing to do here, we just need a new block, which subscriptionTest does for us 15 | cb() 16 | }, 17 | function subscriptionChanges(t, testMeta, response, cb){ 18 | let returnedBlockHash = response.params.result.hash 19 | t.equal(returnedBlockHash, testMeta.block.hash, 'correct result') 20 | cb() 21 | } 22 | ) 23 | 24 | subscriptionTest('log subscription - basic', {}, { 25 | method: 'eth_subscribe', 26 | params: ['logs', { 27 | topics: ['0x00000000000000000000000000000000000000000000000000deadbeefcafe01'] 28 | }], 29 | }, 30 | function afterInstall(t, testMeta, response, cb){ 31 | testMeta.tx = testMeta.blockProvider.addTx({ 32 | hash: '0x0000000000000000000000000000000000000000000000000000000000000001', 33 | _logTopics: ['0x00000000000000000000000000000000000000000000000000deadbeefcafe01'] 34 | }) 35 | testMeta.badTx = testMeta.blockProvider.addTx({ 36 | _logTopics: ['0x00000000000000000000000000000000000000000000000000deadbeefcafe02'] 37 | }) 38 | cb() 39 | }, 40 | function subscriptionChanges(t, testMeta, response, cb){ 41 | var matchedLog = response.params.result 42 | t.ok(matchedLog.transactionHash, 'result has tx hash') 43 | t.deepEqual(matchedLog.transactionHash, testMeta.tx.hash, 'result tx hash matches') 44 | cb() 45 | } 46 | ) 47 | 48 | subscriptionTest('log subscription - and logic', {}, { 49 | method: 'eth_subscribe', 50 | params: ['logs', { 51 | topics: [ 52 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe01', 53 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 54 | ], 55 | }], 56 | }, 57 | function afterInstall(t, testMeta, response, cb){ 58 | testMeta.tx = testMeta.blockProvider.addTx({ 59 | hash: '0x0000000000000000000000000000000000000000000000000000000000000001', 60 | _logTopics: [ 61 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe01', 62 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 63 | ], 64 | }) 65 | testMeta.badTx = testMeta.blockProvider.addTx({ 66 | _logTopics: [ 67 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 68 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe01', 69 | ], 70 | }) 71 | cb() 72 | }, 73 | function subscriptionChangesOne(t, testMeta, response, cb){ 74 | var matchedLog = response.params.result 75 | t.ok(matchedLog.transactionHash, 'result has tx hash') 76 | t.deepEqual(matchedLog.transactionHash, testMeta.tx.hash, 'result tx hash matches') 77 | cb() 78 | } 79 | ) 80 | 81 | subscriptionTest('log subscription - or logic', {}, { 82 | method: 'eth_subscribe', 83 | params: ['logs', { 84 | topics: [ 85 | [ 86 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe01', 87 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 88 | ], 89 | ], 90 | }], 91 | }, 92 | function afterInstall(t, testMeta, response, cb){ 93 | testMeta.tx1 = testMeta.blockProvider.addTx({ 94 | hash: '0x0000000000000000000000000000000000000000000000000000000000000001', 95 | _logTopics: [ 96 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe01', 97 | ], 98 | }) 99 | cb() 100 | }, 101 | function subscriptionChangesOne(t, testMeta, response, cb){ 102 | var matchedLog = response.params.result 103 | t.ok(matchedLog.transactionHash, 'result has tx hash') 104 | t.deepEqual(matchedLog.transactionHash, testMeta.tx1.hash, 'result log matches tx hash') 105 | 106 | testMeta.tx2 = testMeta.blockProvider.addTx({ 107 | hash: '0x0000000000000000000000000000000000000000000000000000000000000002', 108 | _logTopics: [ 109 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 110 | ], 111 | }) 112 | cb() 113 | }, 114 | function subscriptionChangesTwo(t, testMeta, response, cb){ 115 | var matchedLog = response.params.result 116 | t.ok(matchedLog.transactionHash, 'result has tx hash') 117 | t.deepEqual(matchedLog.transactionHash, testMeta.tx2.hash, 'result log matches tx hash') 118 | cb() 119 | } 120 | ) 121 | 122 | subscriptionTest('log subscription - wildcard logic', {}, { 123 | method: 'eth_subscribe', 124 | params: ['logs', { 125 | topics: [ 126 | null, 127 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 128 | ], 129 | }], 130 | }, 131 | function afterInstall(t, testMeta, response, cb){ 132 | testMeta.tx1 = testMeta.blockProvider.addTx({ 133 | _logTopics: [ 134 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe01', 135 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 136 | ], 137 | }) 138 | cb() 139 | }, 140 | function subscriptionChangesOne(t, testMeta, response, cb){ 141 | var matchedLog = response.params.result 142 | t.equal(matchedLog.transactionHash, testMeta.tx1.hash, 'result log matches tx hash') 143 | testMeta.tx2 = testMeta.blockProvider.addTx({ 144 | _logTopics: [ 145 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 146 | '0x00000000000000000000000000000000000000000000000000deadbeefcafe02', 147 | ], 148 | }) 149 | cb() 150 | }, 151 | function subscriptionChangesTwo(t, testMeta, response, cb){ 152 | var matchedLog = response.params.result 153 | t.equal(matchedLog.transactionHash, testMeta.tx2.hash, 'result log matches tx hash') 154 | cb() 155 | } 156 | ) 157 | 158 | subscriptionTest('block subscription - parsing large difficulty', { triggerNextBlock: false }, { 159 | method: 'eth_subscribe', 160 | params: ['newHeads'] 161 | }, 162 | function afterInstall(t, testMeta, response, cb) { 163 | const newBlock = testMeta.blockProvider.nextBlock({ 164 | gasLimit: '0x01', 165 | difficulty: '0xfffffffffffffffffffffffffffffffe' 166 | }) 167 | cb() 168 | }, 169 | function subscriptionChangesOne(t, testMeta, response, cb) { 170 | var returnedDifficulty = response.params.result.difficulty 171 | var returnedGasLimit = response.params.result.gasLimit 172 | t.equal(returnedDifficulty, '0xfffffffffffffffffffffffffffffffe', 'correct result') 173 | t.equal(returnedGasLimit, '0x01', 'correct result') 174 | cb() 175 | } 176 | ) 177 | 178 | function subscriptionTest(label, opts, subscriptionPayload, afterInstall, subscriptionChangesOne, subscriptionChangesTwo) { 179 | const shouldTriggerNextBlock = opts.triggerNextBlock === undefined ? true : opts.triggerNextBlock 180 | let testMeta = {} 181 | let t = test('subscriptions - '+label, function(t) { 182 | // subscribe 183 | // new block 184 | // check for notification 185 | 186 | 187 | // handle "test_rpc" 188 | let subscriptionSubprovider = testMeta.subscriptionSubprovider = injectMetrics(new SubscriptionSubprovider()) 189 | // handle block requests 190 | let blockProvider = testMeta.blockProvider = injectMetrics(new TestBlockProvider()) 191 | 192 | let engine = testMeta.engine = new ProviderEngine({ 193 | pollingInterval: 200, 194 | pollingShouldUnref: false, 195 | }) 196 | engine.addProvider(subscriptionSubprovider) 197 | engine.addProvider(blockProvider) 198 | 199 | let response, notification 200 | 201 | asyncSeries([ 202 | // wait for first block 203 | (next) => { 204 | engine.start() 205 | engine.once('rawBlock', (block) => { 206 | testMeta.block = block 207 | next() 208 | }) 209 | }, 210 | // install subscription 211 | (next) => { 212 | engine.sendAsync(createPayload(subscriptionPayload), function(err, _response){ 213 | if (err) return next(err) 214 | 215 | response = _response 216 | t.ok(response, 'has response') 217 | 218 | let method = subscriptionPayload.method 219 | t.equal(subscriptionSubprovider.getWitnessed(method).length, 1, 'subscriptionSubprovider did see "'+method+'"') 220 | t.equal(subscriptionSubprovider.getHandled(method).length, 1, 'subscriptionSubprovider did handle "'+method+'"') 221 | 222 | testMeta.subscriptionId = response.result 223 | next() 224 | }) 225 | }, 226 | // manipulates next block to trigger a notification 227 | (next) => afterInstall(t, testMeta, response, next), 228 | (next) => { 229 | checkSubscriptionChanges(subscriptionChangesOne, next) 230 | }, 231 | (next) => { 232 | if (!subscriptionChangesTwo) return next() 233 | checkSubscriptionChanges(subscriptionChangesTwo, next) 234 | }, 235 | // cleanup 236 | (next) => { 237 | engine.sendAsync(createPayload({ method: 'eth_unsubscribe', params: [testMeta.subscriptionId] }), next) 238 | }, 239 | ], (err) => { 240 | t.ifErr(err) 241 | testMeta.engine.stop() 242 | t.end() 243 | }) 244 | 245 | function checkSubscriptionChanges(onChange, cb) { 246 | let notification 247 | asyncSeries([ 248 | // wait for subscription trigger 249 | (next) => { 250 | engine.once('data', (err, _notification) => { 251 | if (err) return next(err) 252 | notification = _notification 253 | // validate notification 254 | let subscriptionId = testMeta.subscriptionId 255 | t.ok(notification, 'has notification') 256 | t.equal(notification.params.subscription, subscriptionId, 'notification has correct subscription id') 257 | next() 258 | }) 259 | // create next block so that notification is sent 260 | if (shouldTriggerNextBlock) { 261 | testMeta.block = testMeta.blockProvider.nextBlock() 262 | } 263 | }, 264 | // call test-specific onChange handler 265 | (next) => { 266 | onChange(t, testMeta, notification, next) 267 | }, 268 | ], cb) 269 | } 270 | 271 | }) 272 | } 273 | -------------------------------------------------------------------------------- /test/util/block.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto') 2 | const extend = require('xtend') 3 | const { addHexPrefix, bufferToHex, intToHex, stripHexPrefix } = require('@ethereumjs/util') 4 | const FixtureProvider = require('../../subproviders/fixture.js') 5 | 6 | // 7 | // handles only `eth_getBlockByNumber` requests 8 | // returns a dummy block 9 | // 10 | 11 | class TestBlockProvider extends FixtureProvider { 12 | 13 | constructor (methods) { 14 | super({ 15 | eth_blockNumber: (payload, next, end) => { 16 | const blockNumber = this._currentBlock.number 17 | // return result asynchronously 18 | setTimeout(() => end(null, blockNumber)) 19 | }, 20 | eth_getBlockByNumber: (payload, next, end) => { 21 | const blockRef = payload.params[0] 22 | const result = this.getBlockByRef(blockRef) 23 | // return result asynchronously 24 | setTimeout(() => end(null, result)) 25 | }, 26 | eth_getLogs: (payload, next, end) => { 27 | const transactions = this._currentBlock.transactions 28 | const logs = transactions.map((tx) => { 29 | return { 30 | address: tx._logAddress, 31 | blockNumber: tx.blockNumber, 32 | blockHash: tx.blockHash, 33 | data: tx._logData, 34 | logIndex: tx.transactionIndex, 35 | topics: tx._logTopics, 36 | transactionIndex: tx.transactionIndex, 37 | transactionHash: tx.hash, 38 | } 39 | }) 40 | // return result asynchronously 41 | setTimeout(() => end(null, logs)) 42 | }, 43 | }) 44 | this._blockChain = {} 45 | this._pendingTxs = [] 46 | this.nextBlock() 47 | } 48 | 49 | getBlockByRef (blockRef) { 50 | const self = this 51 | if (blockRef === 'latest') { 52 | return self._currentBlock 53 | } else { 54 | const blockNumber = parseInt(blockRef, 16) 55 | // if present, return block at reference 56 | let block = self._blockChain[blockNumber] 57 | if (block) return block 58 | // check if we should create the new block 59 | if (blockNumber > Number(self._currentBlock.number)) return 60 | // create, store, and return the new block 61 | block = createBlock({ number: blockRef }) 62 | self._blockChain[blockNumber] = block 63 | return block 64 | } 65 | } 66 | 67 | nextBlock (blockParams) { 68 | const self = this 69 | const newBlock = createBlock(blockParams, self._currentBlock, self._pendingTxs) 70 | const blockNumber = parseInt(newBlock.number, 16) 71 | self._pendingTxs = [] 72 | self._currentBlock = newBlock 73 | self._blockChain[blockNumber] = newBlock 74 | return newBlock 75 | } 76 | 77 | addTx (txParams) { 78 | const self = this 79 | var newTx = extend({ 80 | hash: randomHash(), 81 | data: randomHash(), 82 | transactionHash: randomHash(), 83 | // set later 84 | blockNumber: null, 85 | blockHash: null, 86 | transactionIndex: null, 87 | // hack for setting log data 88 | _logAddress: randomAddress(), 89 | _logData: randomHash(), 90 | _logTopics: [ 91 | randomHash(), 92 | randomHash(), 93 | randomHash() 94 | ], 95 | // provided 96 | }, txParams) 97 | self._pendingTxs.push(newTx) 98 | return newTx 99 | } 100 | 101 | } 102 | 103 | // class _currentBlocks 104 | TestBlockProvider.createBlock = createBlock 105 | TestBlockProvider.incrementHex = incrementHex 106 | 107 | function createBlock(blockParams, prevBlock, txs) { 108 | blockParams = blockParams || {} 109 | txs = txs || [] 110 | var defaultNumber = prevBlock ? incrementHex(prevBlock.number) : '0x1' 111 | var defaultGasLimit = intToHex(4712388) 112 | const result = extend({ 113 | // defaults 114 | number: defaultNumber, 115 | hash: randomHash(), 116 | parentHash: prevBlock ? prevBlock.hash : randomHash(), 117 | nonce: randomHash(), 118 | mixHash: randomHash(), 119 | sha3Uncles: randomHash(), 120 | logsBloom: randomHash(), 121 | transactionsRoot: randomHash(), 122 | stateRoot: randomHash(), 123 | receiptsRoot: randomHash(), 124 | miner: randomHash(), 125 | difficulty: randomHash(), 126 | totalDifficulty: randomHash(), 127 | size: randomHash(), 128 | extraData: randomHash(), 129 | gasLimit: defaultGasLimit, 130 | gasUsed: randomHash(), 131 | timestamp: randomHash(), 132 | transactions: txs, 133 | // provided 134 | }, blockParams) 135 | txs.forEach((tx, index) => { 136 | tx.blockHash = result.hash 137 | tx.blockNumber = result.number 138 | tx.transactionIndex = intToHex(index) 139 | }) 140 | return result 141 | } 142 | 143 | function incrementHex(hexString){ 144 | return stripLeadingZeroes(intToHex(Number(hexString)+1)) 145 | } 146 | 147 | function randomHash(){ 148 | return bufferToHex(crypto.randomBytes(32)) 149 | } 150 | 151 | function randomAddress(){ 152 | return bufferToHex(crypto.randomBytes(20)) 153 | } 154 | 155 | function stripLeadingZeroes (hexString) { 156 | let strippedHex = stripHexPrefix(hexString) 157 | while (strippedHex[0] === '0') { 158 | strippedHex = strippedHex.substr(1) 159 | } 160 | return addHexPrefix(strippedHex) 161 | } 162 | 163 | module.exports = TestBlockProvider 164 | -------------------------------------------------------------------------------- /test/util/ganache.js: -------------------------------------------------------------------------------- 1 | const { provider } = require('ganache') 2 | const ProviderSubprovider = require('../../subproviders/provider') 3 | 4 | 5 | class GanacheProvider extends ProviderSubprovider { 6 | 7 | constructor () { 8 | super(provider()) 9 | } 10 | 11 | } 12 | 13 | module.exports = GanacheProvider 14 | -------------------------------------------------------------------------------- /test/util/inject-metrics.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = injectSubproviderMetrics 3 | 4 | function injectSubproviderMetrics(subprovider){ 5 | subprovider.getWitnessed = getWitnessed.bind(subprovider) 6 | subprovider.getHandled = getHandled.bind(subprovider) 7 | subprovider.clearMetrics = () => { 8 | subprovider.payloadsWitnessed = {} 9 | subprovider.payloadsHandled = {} 10 | } 11 | 12 | subprovider.clearMetrics() 13 | 14 | var _super = subprovider.handleRequest.bind(subprovider) 15 | subprovider.handleRequest = handleRequest.bind(subprovider, _super) 16 | 17 | return subprovider 18 | } 19 | 20 | function getWitnessed(method){ 21 | const self = this 22 | var witnessed = self.payloadsWitnessed[method] = self.payloadsWitnessed[method] || [] 23 | return witnessed 24 | } 25 | 26 | function getHandled(method){ 27 | const self = this 28 | var witnessed = self.payloadsHandled[method] = self.payloadsHandled[method] || [] 29 | return witnessed 30 | } 31 | 32 | function handleRequest(_super, payload, next, end){ 33 | const self = this 34 | // mark payload witnessed 35 | var witnessed = self.getWitnessed(payload.method) 36 | witnessed.push(payload) 37 | // continue 38 | _super(payload, next, function(err, result){ 39 | // mark payload handled 40 | var handled = self.getHandled(payload.method) 41 | handled.push(payload) 42 | // continue 43 | end(err, result) 44 | }) 45 | } -------------------------------------------------------------------------------- /test/util/mock-subprovider.js: -------------------------------------------------------------------------------- 1 | const inherits = require('util').inherits 2 | const Subprovider = require('../../subproviders/subprovider.js') 3 | const extend = require('xtend') 4 | 5 | module.exports = MockSubprovider 6 | 7 | inherits(MockSubprovider, Subprovider) 8 | 9 | function MockSubprovider(handleRequest){ 10 | const self = this 11 | 12 | // Optionally provide a handleRequest method 13 | if (handleRequest) { 14 | this.handleRequest = handleRequest 15 | } 16 | } 17 | 18 | var mockResponse = { 19 | data: 'mock-success!' 20 | } 21 | MockSubprovider.prototype.handleRequest = function(payload, next, end){ 22 | end(mockResponse) 23 | } 24 | 25 | -------------------------------------------------------------------------------- /test/util/mock_block.json: -------------------------------------------------------------------------------- 1 | {"jsonrpc":"2.0","result":{"author":"0x3045126f1a6bcf27b0712c2c4133a2ce69395546","difficulty":"0x05a848d3","extraData":"0xd78301040f844765746887676f312e372e31856c696e7578","gasLimit":"0x47e7c4","gasUsed":"0x044386","hash":"0x39082b44d673569570d6ac46b8b527a0fe4fcf7b40bd09f70d701bbe42144587","logsBloom":"0x00002000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000020000000000000000002000000000000000000000000001000000000000000000000020020000000008000000800400000008000000000000000000000000000008000000000800000000000000000000000200000000000000000000080000100000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000020000000000000040000000000000000000008000000000000000000000100000000","miner":"0x3045126f1a6bcf27b0712c2c4133a2ce69395546","number":"0x1a64a4","parentHash":"0x535d38d2128ade5a19e72934e8018d9649c2940766b88c1fdbd43ef2cd338dad","receiptsRoot":"0x918e809c1a205b2cce68663f3ac6d72b295bb9269de39bdc52a278bd7f51e841","sealFields":["0x48834e4f263cb23860214003d3c752228cdf70d284cbb286b95919c85a6c440f","0x1f1c95b1a27be157"],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":1825,"stateRoot":"0xee08496f6c8b7d4227a99ccf2372a96f15500e07bb5b94532ee93d175fe6d1ee","timestamp":"0x57f30d81","totalDifficulty":"0x01623f4272fb2e","transactions":["0x74f3c8d4559e02adfc79809f5e7f91595ad12c80d20259f2cd0c270afe40b4f2","0xa7918107717faf6b2c058628939aaf9aed7bca105ae2018bee8f32cb21638768","0x13749d33769ae67f33482aec8a58d11b276be980d509d9d30fa6760a39a249fc","0x73a886e141d8cd03757997201e9382ca6ab248944ea9b7d29d2259fc029088ef","0xeaeb68cba3f26349796a50ec1d469f8cec6d67fcddc4071b08cc47fb308d73d9","0x8abfc676700f75717f4204d2f16aac8b34bee86849293b0e7e133ab52f12d0f3","0xc5e2cd4389a9efee352a5e70a5db5924510531bca73cd39f1ca7f410de129f37","0xd981eaae3a02220e0158773867e02cce3e021a62dc9babd7d3ffff0c8800ab48","0x4ba75e509ff67b2177b3bb3c7f708fdef966f79655887e49cdda92c9b79bdb56","0xb9bc0f47e59beb252d701ede931b343977a6c20018f806a1892e0748cb771abc","0x3dc76b1bc8964904284dbff63a63be3a321d6a37aab2e61df99e9a911ffa150b"],"transactionsRoot":"0xc111e128231888a1237b9d8105193fde1c3aa3e750105507dcd112179ca03019","uncles":[]},"id":1475546514569366} -------------------------------------------------------------------------------- /test/util/passthrough.js: -------------------------------------------------------------------------------- 1 | const inherits = require('util').inherits 2 | const FixtureProvider = require('../../subproviders/fixture.js') 3 | 4 | module.exports = PassthroughProvider 5 | 6 | // 7 | // handles no methods, skips all requests 8 | // mostly useless 9 | // 10 | 11 | inherits(PassthroughProvider, FixtureProvider) 12 | function PassthroughProvider(methods){ 13 | const self = this 14 | FixtureProvider.call(self, {}) 15 | } 16 | -------------------------------------------------------------------------------- /test/wallet.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const { TransactionFactory } = require('@ethereumjs/tx') 3 | const ethUtil = require('@ethereumjs/util') 4 | const ProviderEngine = require('../index.js') 5 | const FixtureProvider = require('../subproviders/fixture.js') 6 | const NonceTracker = require('../subproviders/nonce-tracker.js') 7 | const HookedWalletProvider = require('../subproviders/hooked-wallet.js') 8 | const HookedWalletTxProvider = require('../subproviders/hooked-wallet-ethtx.js') 9 | const TestBlockProvider = require('./util/block.js') 10 | const createPayload = require('../util/create-payload.js') 11 | const injectMetrics = require('./util/inject-metrics') 12 | 13 | 14 | test('tx sig ethtx provider', function(t){ 15 | t.plan(12) 16 | 17 | var privateKey = Buffer.from('cccd8f4d88de61f92f3747e4a9604a0395e6ad5138add4bec4a2ddf231ee24f9', 'hex') 18 | var address = Buffer.from('1234362ef32bcd26d3dd18ca749378213625ba0b', 'hex') 19 | var addressHex = '0x'+address.toString('hex') 20 | 21 | // sign all tx's 22 | var providerA = injectMetrics(new HookedWalletTxProvider({ 23 | getAccounts: function(cb){ 24 | cb(null, [addressHex]) 25 | }, 26 | getPrivateKey: function(address, cb){ 27 | cb(null, privateKey) 28 | }, 29 | signTransaction: function(txParams, cb){ 30 | const tx = TransactionFactory.fromTxData(txParams) 31 | const signedTransaction = tx.sign(privateKey) 32 | var rawTx = '0x'+signedTransaction.serialize().toString('hex') 33 | cb(null, rawTx) 34 | }, 35 | })) 36 | 37 | // handle nonce requests 38 | var providerB = injectMetrics(new NonceTracker()) 39 | // handle all bottom requests 40 | var providerC = injectMetrics(new FixtureProvider({ 41 | eth_gasPrice: '0x1234', 42 | eth_getTransactionCount: '0x00', 43 | eth_sendRawTransaction: function(payload, next, done){ 44 | var rawTx = ethUtil.toBuffer(payload.params[0]) 45 | var tx = TransactionFactory.fromSerializedData(rawTx) 46 | var hash = '0x'+tx.hash().toString('hex') 47 | done(null, hash) 48 | }, 49 | })) 50 | // handle block requests 51 | var providerD = injectMetrics(new TestBlockProvider()) 52 | 53 | var engine = new ProviderEngine() 54 | engine.addProvider(providerA) 55 | engine.addProvider(providerB) 56 | engine.addProvider(providerC) 57 | engine.addProvider(providerD) 58 | 59 | var txPayload = { 60 | method: 'eth_sendTransaction', 61 | params: [{ 62 | from: addressHex, 63 | to: addressHex, 64 | value: '0x01', 65 | gas: '0x1234567890', 66 | }] 67 | } 68 | 69 | engine.start() 70 | engine.sendAsync(createPayload(txPayload), function(err, response){ 71 | t.ifError(err, 'did not error') 72 | t.ok(response, 'has response') 73 | 74 | // intial tx request 75 | t.equal(providerA.getWitnessed('eth_sendTransaction').length, 1, 'providerA did see "signTransaction"') 76 | t.equal(providerA.getHandled('eth_sendTransaction').length, 1, 'providerA did handle "signTransaction"') 77 | 78 | // tx nonce 79 | t.equal(providerB.getWitnessed('eth_getTransactionCount').length, 1, 'providerB did see "eth_getTransactionCount"') 80 | t.equal(providerB.getHandled('eth_getTransactionCount').length, 0, 'providerB did NOT handle "eth_getTransactionCount"') 81 | t.equal(providerC.getWitnessed('eth_getTransactionCount').length, 1, 'providerC did see "eth_getTransactionCount"') 82 | t.equal(providerC.getHandled('eth_getTransactionCount').length, 1, 'providerC did handle "eth_getTransactionCount"') 83 | 84 | // gas price 85 | t.equal(providerC.getWitnessed('eth_gasPrice').length, 1, 'providerB did see "eth_gasPrice"') 86 | t.equal(providerC.getHandled('eth_gasPrice').length, 1, 'providerB did handle "eth_gasPrice"') 87 | 88 | // send raw tx 89 | t.equal(providerC.getWitnessed('eth_sendRawTransaction').length, 1, 'providerC did see "eth_sendRawTransaction"') 90 | t.equal(providerC.getHandled('eth_sendRawTransaction').length, 1, 'providerC did handle "eth_sendRawTransaction"') 91 | 92 | engine.stop() 93 | t.end() 94 | }) 95 | }) 96 | 97 | test('tx sig wallet provider', function(t){ 98 | t.plan(12) 99 | 100 | var privateKey = Buffer.from('cccd8f4d88de61f92f3747e4a9604a0395e6ad5138add4bec4a2ddf231ee24f9', 'hex') 101 | var address = Buffer.from('1234362ef32bcd26d3dd18ca749378213625ba0b', 'hex') 102 | var addressHex = '0x'+address.toString('hex') 103 | 104 | // sign all tx's 105 | var providerA = injectMetrics(new HookedWalletProvider({ 106 | getAccounts: function(cb){ 107 | cb(null, [addressHex]) 108 | }, 109 | signTransaction: function(txParams, cb){ 110 | const tx = TransactionFactory.fromTxData(txParams) 111 | const signedTransaction = tx.sign(privateKey) 112 | var rawTx = '0x'+signedTransaction.serialize().toString('hex') 113 | cb(null, rawTx) 114 | }, 115 | })) 116 | 117 | // handle nonce requests 118 | var providerB = injectMetrics(new NonceTracker()) 119 | // handle all bottom requests 120 | var providerC = injectMetrics(new FixtureProvider({ 121 | eth_gasPrice: '0x1234', 122 | eth_getTransactionCount: '0x00', 123 | eth_sendRawTransaction: function(payload, next, done){ 124 | var rawTx = ethUtil.toBuffer(payload.params[0]) 125 | var tx = TransactionFactory.fromSerializedData(rawTx) 126 | var hash = '0x'+tx.hash().toString('hex') 127 | done(null, hash) 128 | }, 129 | })) 130 | // handle block requests 131 | var providerD = injectMetrics(new TestBlockProvider()) 132 | 133 | var engine = new ProviderEngine() 134 | engine.addProvider(providerA) 135 | engine.addProvider(providerB) 136 | engine.addProvider(providerC) 137 | engine.addProvider(providerD) 138 | 139 | var txPayload = { 140 | method: 'eth_sendTransaction', 141 | params: [{ 142 | from: addressHex, 143 | to: addressHex, 144 | value: '0x01', 145 | gas: '0x1234567890', 146 | }] 147 | } 148 | 149 | engine.start() 150 | engine.sendAsync(createPayload(txPayload), function(err, response){ 151 | t.ifError(err, 'did not error') 152 | t.ok(response, 'has response') 153 | 154 | // intial tx request 155 | t.equal(providerA.getWitnessed('eth_sendTransaction').length, 1, 'providerA did see "signTransaction"') 156 | t.equal(providerA.getHandled('eth_sendTransaction').length, 1, 'providerA did handle "signTransaction"') 157 | 158 | // tx nonce 159 | t.equal(providerB.getWitnessed('eth_getTransactionCount').length, 1, 'providerB did see "eth_getTransactionCount"') 160 | t.equal(providerB.getHandled('eth_getTransactionCount').length, 0, 'providerB did NOT handle "eth_getTransactionCount"') 161 | t.equal(providerC.getWitnessed('eth_getTransactionCount').length, 1, 'providerC did see "eth_getTransactionCount"') 162 | t.equal(providerC.getHandled('eth_getTransactionCount').length, 1, 'providerC did handle "eth_getTransactionCount"') 163 | 164 | // gas price 165 | t.equal(providerC.getWitnessed('eth_gasPrice').length, 1, 'providerB did see "eth_gasPrice"') 166 | t.equal(providerC.getHandled('eth_gasPrice').length, 1, 'providerB did handle "eth_gasPrice"') 167 | 168 | // send raw tx 169 | t.equal(providerC.getWitnessed('eth_sendRawTransaction').length, 1, 'providerC did see "eth_sendRawTransaction"') 170 | t.equal(providerC.getHandled('eth_sendRawTransaction').length, 1, 'providerC did handle "eth_sendRawTransaction"') 171 | 172 | engine.stop() 173 | t.end() 174 | }) 175 | 176 | }) 177 | 178 | test('no such account', function(t){ 179 | t.plan(1) 180 | 181 | var addressHex = '0x1234362ef32bcd26d3dd18ca749378213625ba0b' 182 | var otherAddressHex = '0x4321362ef32bcd26d3dd18ca749378213625ba0c' 183 | 184 | // sign all tx's 185 | var providerA = injectMetrics(new HookedWalletProvider({ 186 | getAccounts: function(cb){ 187 | cb(null, [addressHex]) 188 | }, 189 | })) 190 | 191 | // handle nonce requests 192 | var providerB = injectMetrics(new NonceTracker()) 193 | // handle all bottom requests 194 | var providerC = injectMetrics(new FixtureProvider({ 195 | eth_gasPrice: '0x1234', 196 | eth_getTransactionCount: '0x00', 197 | eth_sendRawTransaction: function(payload, next, done){ 198 | var rawTx = ethUtil.toBuffer(payload.params[0]) 199 | var tx = TransactionFactory.fromTxData(rawTx) 200 | var hash = '0x'+tx.hash().toString('hex') 201 | done(null, hash) 202 | }, 203 | })) 204 | // handle block requests 205 | var providerD = injectMetrics(new TestBlockProvider()) 206 | 207 | var engine = new ProviderEngine() 208 | engine.addProvider(providerA) 209 | engine.addProvider(providerB) 210 | engine.addProvider(providerC) 211 | engine.addProvider(providerD) 212 | 213 | var txPayload = { 214 | method: 'eth_sendTransaction', 215 | params: [{ 216 | from: otherAddressHex, 217 | to: addressHex, 218 | value: '0x01', 219 | gas: '0x1234567890', 220 | }] 221 | } 222 | 223 | engine.start() 224 | engine.sendAsync(createPayload(txPayload), function(err, response){ 225 | t.ok(err, 'did error') 226 | 227 | engine.stop() 228 | t.end() 229 | }) 230 | 231 | }) 232 | 233 | 234 | test('sign message', function(t){ 235 | t.plan(3) 236 | 237 | var privateKey = Buffer.from('cccd8f4d88de61f92f3747e4a9604a0395e6ad5138add4bec4a2ddf231ee24f9', 'hex') 238 | var addressHex = '0x1234362ef32bcd26d3dd18ca749378213625ba0b' 239 | 240 | var message = 'haay wuurl' 241 | var signature = '0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c' 242 | 243 | // sign all messages 244 | var providerA = injectMetrics(new HookedWalletTxProvider({ 245 | getAccounts: function(cb){ 246 | cb(null, [addressHex]) 247 | }, 248 | getPrivateKey: function(address, cb){ 249 | cb(null, privateKey) 250 | }, 251 | })) 252 | 253 | // handle block requests 254 | var providerB = injectMetrics(new TestBlockProvider()) 255 | 256 | var engine = new ProviderEngine() 257 | engine.addProvider(providerA) 258 | engine.addProvider(providerB) 259 | 260 | var payload = { 261 | method: 'eth_sign', 262 | params: [ 263 | addressHex, 264 | message, 265 | ], 266 | } 267 | 268 | engine.start() 269 | engine.sendAsync(createPayload(payload), function(err, response){ 270 | t.ifError(err, 'did not error') 271 | t.ok(response, 'has response') 272 | 273 | t.equal(response.result, signature, 'signed response is correct') 274 | 275 | engine.stop() 276 | t.end() 277 | }) 278 | 279 | }) 280 | 281 | // personal_sign was declared without an explicit set of test data 282 | // so I made a script out of geth's internals to create this test data 283 | // https://gist.github.com/kumavis/461d2c0e9a04ea0818e423bb77e3d260 284 | 285 | signatureTest({ 286 | testLabel: 'kumavis fml manual test I', 287 | method: 'personal_sign', 288 | // "hello world" 289 | message: '0x68656c6c6f20776f726c64', 290 | signature: '0xce909e8ea6851bc36c007a0072d0524b07a3ff8d4e623aca4c71ca8e57250c4d0a3fc38fa8fbaaa81ead4b9f6bd03356b6f8bf18bccad167d78891636e1d69561b', 291 | addressHex: '0xbe93f9bacbcffc8ee6663f2647917ed7a20a57bb', 292 | privateKey: Buffer.from('6969696969696969696969696969696969696969696969696969696969696969', 'hex'), 293 | }) 294 | 295 | signatureTest({ 296 | testLabel: 'kumavis fml manual test II', 297 | method: 'personal_sign', 298 | // some random binary message from parity's test 299 | message: '0x0cc175b9c0f1b6a831c399e26977266192eb5ffee6ae2fec3ad71c777531578f', 300 | signature: '0x9ff8350cc7354b80740a3580d0e0fd4f1f02062040bc06b893d70906f8728bb5163837fd376bf77ce03b55e9bd092b32af60e86abce48f7b8d3539988ee5a9be1c', 301 | addressHex: '0xbe93f9bacbcffc8ee6663f2647917ed7a20a57bb', 302 | privateKey: Buffer.from('6969696969696969696969696969696969696969696969696969696969696969', 'hex'), 303 | }) 304 | 305 | signatureTest({ 306 | testLabel: 'kumavis fml manual test III', 307 | method: 'personal_sign', 308 | // random binary message data and pk from parity's test 309 | // https://github.com/ethcore/parity/blob/5369a129ae276d38f3490abb18c5093b338246e0/rpc/src/v1/tests/mocked/eth.rs#L301-L317 310 | // note: their signature result is incorrect (last byte moved to front) due to a parity bug 311 | message: '0x0cc175b9c0f1b6a831c399e26977266192eb5ffee6ae2fec3ad71c777531578f', 312 | signature: '0xa2870db1d0c26ef93c7b72d2a0830fa6b841e0593f7186bc6c7cc317af8cf3a42fda03bd589a49949aa05db83300cdb553116274518dbe9d90c65d0213f4af491b', 313 | addressHex: '0xe0da1edcea030875cd0f199d96eb70f6ab78faf2', 314 | privateKey: Buffer.from('4545454545454545454545454545454545454545454545454545454545454545', 'hex'), 315 | }) 316 | 317 | recoverTest({ 318 | testLabel: 'geth kumavis manual I recover', 319 | method: 'personal_ecRecover', 320 | // "hello world" 321 | message: '0x68656c6c6f20776f726c64', 322 | signature: '0xce909e8ea6851bc36c007a0072d0524b07a3ff8d4e623aca4c71ca8e57250c4d0a3fc38fa8fbaaa81ead4b9f6bd03356b6f8bf18bccad167d78891636e1d69561b', 323 | addressHex: '0xbe93f9bacbcffc8ee6663f2647917ed7a20a57bb', 324 | }) 325 | 326 | recoverTest({ 327 | testLabel: 'geth kumavis manual II recover', 328 | method: 'personal_ecRecover', 329 | // message from parity's test - note result is different than what they are testing against 330 | // https://github.com/ethcore/parity/blob/5369a129ae276d38f3490abb18c5093b338246e0/rpc/src/v1/tests/mocked/eth.rs#L301-L317 331 | message: '0x0cc175b9c0f1b6a831c399e26977266192eb5ffee6ae2fec3ad71c777531578f', 332 | signature: '0x9ff8350cc7354b80740a3580d0e0fd4f1f02062040bc06b893d70906f8728bb5163837fd376bf77ce03b55e9bd092b32af60e86abce48f7b8d3539988ee5a9be1c', 333 | addressHex: '0xbe93f9bacbcffc8ee6663f2647917ed7a20a57bb', 334 | }) 335 | 336 | signatureTest({ 337 | testLabel: 'sign typed message', 338 | method: 'eth_signTypedData', 339 | message: [ 340 | { 341 | type: 'string', 342 | name: 'message', 343 | value: 'Hi, Alice!' 344 | } 345 | ], 346 | signature: '0xb2c9c7bdaee2cc73f318647c3f6e24792fca86a9f2736d9e7537e64c503545392313ebbbcb623c828fd8f99fd1fb48f8f4da8cb1d1a924e28b21de018c826e181c', 347 | addressHex: '0xbe93f9bacbcffc8ee6663f2647917ed7a20a57bb', 348 | privateKey: Buffer.from('6969696969696969696969696969696969696969696969696969696969696969', 'hex'), 349 | }) 350 | 351 | test('sender validation, with mixed-case', function(t){ 352 | t.plan(1) 353 | 354 | var senderAddress = '0xE4660fdAb2D6Bd8b50C029ec79E244d132c3bc2B' 355 | 356 | var providerA = injectMetrics(new HookedWalletTxProvider({ 357 | getAccounts: function(cb){ 358 | cb(null, [senderAddress]) 359 | }, 360 | getPrivateKey: function(address, cb){ 361 | t.pass('correctly validated sender') 362 | engine.stop() 363 | t.end() 364 | }, 365 | })) 366 | var providerB = injectMetrics(new TestBlockProvider()) 367 | // handle all bottom requests 368 | var providerC = injectMetrics(new FixtureProvider({ 369 | eth_gasPrice: '0x1234', 370 | eth_estimateGas: '0x1234', 371 | eth_getTransactionCount: '0x00', 372 | })) 373 | 374 | var engine = new ProviderEngine() 375 | engine.addProvider(providerA) 376 | engine.addProvider(providerB) 377 | engine.addProvider(providerC) 378 | 379 | engine.start() 380 | engine.sendAsync({ 381 | method: 'eth_sendTransaction', 382 | params: [{ 383 | from: senderAddress.toLowerCase(), 384 | }] 385 | }, function(err){ 386 | t.notOk(err, 'error was present') 387 | engine.stop() 388 | t.end() 389 | }) 390 | 391 | }) 392 | 393 | 394 | function signatureTest({ testLabel, method, privateKey, addressHex, message, signature }) { 395 | // sign all messages 396 | var providerA = injectMetrics(new HookedWalletTxProvider({ 397 | getAccounts: function(cb){ 398 | cb(null, [addressHex]) 399 | }, 400 | getPrivateKey: function(address, cb){ 401 | cb(null, privateKey) 402 | }, 403 | })) 404 | 405 | // handle block requests 406 | var providerB = injectMetrics(new TestBlockProvider()) 407 | 408 | var engine = new ProviderEngine() 409 | engine.addProvider(providerA) 410 | engine.addProvider(providerB) 411 | 412 | var payload = { 413 | method: method, 414 | params: [message, addressHex], 415 | } 416 | 417 | singleRpcTest({ 418 | testLabel: `sign message ${method} - ${testLabel}`, 419 | payload, 420 | engine, 421 | expectedResult: signature, 422 | }) 423 | 424 | // Personal sign is supposed to have params 425 | // ordered in this direction, not the other. 426 | if (payload.method === 'personal_sign') { 427 | payload = { 428 | method: method, 429 | params: [message, addressHex], 430 | } 431 | 432 | singleRpcTest({ 433 | testLabel: `sign message ${method} - ${testLabel}`, 434 | payload, 435 | engine, 436 | expectedResult: signature, 437 | }) 438 | } 439 | } 440 | 441 | function recoverTest({ testLabel, method, addressHex, message, signature }) { 442 | 443 | // sign all messages 444 | var providerA = injectMetrics(new HookedWalletTxProvider({ 445 | getAccounts: function(cb){ 446 | cb(null, [addressHex]) 447 | }, 448 | getPrivateKey: function(address, cb){ 449 | cb(new Error('this should not be called')) 450 | }, 451 | })) 452 | 453 | // handle block requests 454 | var blockProvider = injectMetrics(new TestBlockProvider()) 455 | 456 | var engine = new ProviderEngine() 457 | engine.addProvider(providerA) 458 | engine.addProvider(blockProvider) 459 | 460 | var payload = { 461 | method: method, 462 | params: [message, signature], 463 | } 464 | 465 | singleRpcTest({ 466 | testLabel: `recover message ${method} - ${testLabel}`, 467 | payload, 468 | engine, 469 | expectedResult: addressHex, 470 | }) 471 | 472 | } 473 | 474 | function singleRpcTest({ testLabel, payload, expectedResult, engine }) { 475 | test(testLabel, function(t){ 476 | t.plan(3) 477 | 478 | engine.start() 479 | engine.sendAsync(createPayload(payload), function(err, response){ 480 | if (err) { 481 | console.log('bad payload:', payload) 482 | console.error(err) 483 | } 484 | t.ifError(err) 485 | t.ok(response, 'has response') 486 | 487 | t.equal(response.result, expectedResult, 'rpc result is as expected') 488 | 489 | engine.stop() 490 | t.end() 491 | }) 492 | 493 | }) 494 | } 495 | -------------------------------------------------------------------------------- /util/assert.js: -------------------------------------------------------------------------------- 1 | module.exports = assert 2 | 3 | function assert(condition, message) { 4 | if (!condition) { 5 | throw message || "Assertion failed"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /util/async.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Works the same as async.parallel 3 | parallel: function(fns, done) { 4 | done = done || function() {}; 5 | this.map(fns, function(fn, callback) { 6 | fn(callback); 7 | }, done); 8 | }, 9 | 10 | // Works the same as async.map 11 | map: function(items, iterator, done) { 12 | done = done || function() {}; 13 | var results = []; 14 | var failure = false; 15 | var expected = items.length; 16 | var actual = 0; 17 | var createIntermediary = function(index) { 18 | return function(err, result) { 19 | // Return if we found a failure anywhere. 20 | // We can't stop execution of functions since they've already 21 | // been fired off; but we can prevent excessive handling of callbacks. 22 | if (failure != false) { 23 | return; 24 | } 25 | 26 | if (err != null) { 27 | failure = true; 28 | done(err, result); 29 | return; 30 | } 31 | 32 | actual += 1; 33 | 34 | if (actual == expected) { 35 | done(null, results); 36 | } 37 | }; 38 | }; 39 | 40 | for (var i = 0; i < items.length; i++) { 41 | var item = items[i]; 42 | iterator(item, createIntermediary(i)); 43 | } 44 | 45 | if (items.length == 0) { 46 | done(null, []); 47 | } 48 | }, 49 | 50 | // Works like async.eachSeries 51 | eachSeries: function(items, iterator, done) { 52 | done = done || function() {}; 53 | var results = []; 54 | var failure = false; 55 | var expected = items.length; 56 | var current = -1; 57 | 58 | function callback(err, result) { 59 | if (err) return done(err); 60 | 61 | results.push(result); 62 | 63 | if (current == expected) { 64 | return done(null, results); 65 | } else { 66 | next(); 67 | } 68 | } 69 | 70 | function next() { 71 | current += 1; 72 | 73 | var item = items[current]; 74 | iterator(item, callback); 75 | } 76 | 77 | next() 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /util/create-payload.js: -------------------------------------------------------------------------------- 1 | const getRandomId = require('./random-id.js') 2 | const extend = require('xtend') 3 | 4 | module.exports = createPayload 5 | 6 | 7 | function createPayload(data){ 8 | return extend({ 9 | // defaults 10 | id: getRandomId(), 11 | jsonrpc: '2.0', 12 | params: [], 13 | // user-specified 14 | }, data) 15 | } 16 | -------------------------------------------------------------------------------- /util/estimate-gas.js: -------------------------------------------------------------------------------- 1 | const createPayload = require('./create-payload.js') 2 | 3 | module.exports = estimateGas 4 | 5 | /* 6 | 7 | This is a work around for https://github.com/ethereum/go-ethereum/issues/2577 8 | 9 | */ 10 | 11 | 12 | function estimateGas(provider, txParams, cb) { 13 | provider.sendAsync(createPayload({ 14 | method: 'eth_estimateGas', 15 | params: [txParams] 16 | }), function(err, res){ 17 | if (err) { 18 | // handle simple value transfer case 19 | if (err.message === 'no contract code at given address') { 20 | return cb(null, '0xcf08') 21 | } else { 22 | return cb(err) 23 | } 24 | } 25 | cb(null, res.result) 26 | }) 27 | } -------------------------------------------------------------------------------- /util/random-id.js: -------------------------------------------------------------------------------- 1 | module.exports = createRandomId 2 | 3 | 4 | function createRandomId () { 5 | // random id 6 | return Math.floor(Number.MAX_SAFE_INTEGER * Math.random()) 7 | } -------------------------------------------------------------------------------- /util/rpc-cache-utils.js: -------------------------------------------------------------------------------- 1 | const stringify = require('json-stable-stringify') 2 | 3 | module.exports = { 4 | cacheIdentifierForPayload: cacheIdentifierForPayload, 5 | canCache: canCache, 6 | blockTagForPayload: blockTagForPayload, 7 | paramsWithoutBlockTag: paramsWithoutBlockTag, 8 | blockTagParamIndex: blockTagParamIndex, 9 | cacheTypeForPayload: cacheTypeForPayload, 10 | } 11 | 12 | function cacheIdentifierForPayload(payload, opts = {}){ 13 | if (!canCache(payload)) return null 14 | const { includeBlockRef } = opts 15 | const params = includeBlockRef ? payload.params : paramsWithoutBlockTag(payload) 16 | return payload.method + ':' + stringify(params) 17 | } 18 | 19 | function canCache(payload){ 20 | return cacheTypeForPayload(payload) !== 'never' 21 | } 22 | 23 | function blockTagForPayload(payload){ 24 | var index = blockTagParamIndex(payload); 25 | 26 | // Block tag param not passed. 27 | if (index >= payload.params.length) { 28 | return null; 29 | } 30 | 31 | return payload.params[index]; 32 | } 33 | 34 | function paramsWithoutBlockTag(payload){ 35 | var index = blockTagParamIndex(payload); 36 | 37 | // Block tag param not passed. 38 | if (index >= payload.params.length) { 39 | return payload.params; 40 | } 41 | 42 | // eth_getBlockByNumber has the block tag first, then the optional includeTx? param 43 | if (payload.method === 'eth_getBlockByNumber') { 44 | return payload.params.slice(1); 45 | } 46 | 47 | return payload.params.slice(0,index); 48 | } 49 | 50 | function blockTagParamIndex(payload){ 51 | switch(payload.method) { 52 | // blockTag is third param 53 | case 'eth_getStorageAt': 54 | return 2 55 | // blockTag is second param 56 | case 'eth_getBalance': 57 | case 'eth_getCode': 58 | case 'eth_getTransactionCount': 59 | case 'eth_call': 60 | case 'eth_estimateGas': 61 | return 1 62 | // blockTag is first param 63 | case 'eth_getBlockByNumber': 64 | return 0 65 | // there is no blockTag 66 | default: 67 | return undefined 68 | } 69 | } 70 | 71 | function cacheTypeForPayload(payload) { 72 | switch (payload.method) { 73 | // cache permanently 74 | case 'web3_clientVersion': 75 | case 'web3_sha3': 76 | case 'eth_protocolVersion': 77 | case 'eth_getBlockTransactionCountByHash': 78 | case 'eth_getUncleCountByBlockHash': 79 | case 'eth_getCode': 80 | case 'eth_getBlockByHash': 81 | case 'eth_getTransactionByHash': 82 | case 'eth_getTransactionByBlockHashAndIndex': 83 | case 'eth_getTransactionReceipt': 84 | case 'eth_getUncleByBlockHashAndIndex': 85 | case 'eth_getCompilers': 86 | case 'eth_compileLLL': 87 | case 'eth_compileSolidity': 88 | case 'eth_compileSerpent': 89 | case 'shh_version': 90 | return 'perma' 91 | 92 | // cache until fork 93 | case 'eth_getBlockByNumber': 94 | case 'eth_getBlockTransactionCountByNumber': 95 | case 'eth_getUncleCountByBlockNumber': 96 | case 'eth_getTransactionByBlockNumberAndIndex': 97 | case 'eth_getUncleByBlockNumberAndIndex': 98 | return 'fork' 99 | 100 | // cache for block 101 | case 'eth_gasPrice': 102 | case 'eth_getBalance': 103 | case 'eth_getStorageAt': 104 | case 'eth_getTransactionCount': 105 | case 'eth_call': 106 | case 'eth_estimateGas': 107 | case 'eth_getFilterLogs': 108 | case 'eth_getLogs': 109 | case 'eth_blockNumber': 110 | return 'block' 111 | 112 | // never cache 113 | case 'net_version': 114 | case 'net_peerCount': 115 | case 'net_listening': 116 | case 'eth_syncing': 117 | case 'eth_sign': 118 | case 'eth_coinbase': 119 | case 'eth_mining': 120 | case 'eth_hashrate': 121 | case 'eth_accounts': 122 | case 'eth_sendTransaction': 123 | case 'eth_sendRawTransaction': 124 | case 'eth_newFilter': 125 | case 'eth_newBlockFilter': 126 | case 'eth_newPendingTransactionFilter': 127 | case 'eth_uninstallFilter': 128 | case 'eth_getFilterChanges': 129 | case 'eth_getWork': 130 | case 'eth_submitWork': 131 | case 'eth_submitHashrate': 132 | case 'db_putString': 133 | case 'db_getString': 134 | case 'db_putHex': 135 | case 'db_getHex': 136 | case 'shh_post': 137 | case 'shh_newIdentity': 138 | case 'shh_hasIdentity': 139 | case 'shh_newGroup': 140 | case 'shh_addToGroup': 141 | case 'shh_newFilter': 142 | case 'shh_uninstallFilter': 143 | case 'shh_getFilterChanges': 144 | case 'shh_getMessages': 145 | return 'never' 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /util/rpc-hex-encoding.js: -------------------------------------------------------------------------------- 1 | const ethUtil = require('@ethereumjs/util') 2 | const assert = require('./assert.js') 3 | 4 | module.exports = { 5 | bufferToQuantityHex: bufferToQuantityHex, 6 | intToQuantityHex: intToQuantityHex, 7 | quantityHexToInt: quantityHexToInt, 8 | } 9 | 10 | /* 11 | * As per https://github.com/ethereum/wiki/wiki/JSON-RPC#hex-value-encoding 12 | * Quantities should be represented by the most compact hex representation possible 13 | * This means that no leading zeroes are allowed. There helpers make it easy 14 | * to convert to and from integers and their compact hex representation 15 | */ 16 | 17 | function bufferToQuantityHex(buffer) { 18 | buffer = ethUtil.toBuffer(buffer); 19 | var hex = buffer.toString('hex'); 20 | var trimmed = ethUtil.unpad(hex); 21 | return ethUtil.addHexPrefix(trimmed); 22 | } 23 | 24 | function intToQuantityHex(n) { 25 | assert(typeof n === 'number' && n === Math.floor(n), 'intToQuantityHex arg must be an integer') 26 | var nHex = ethUtil.toBuffer(n).toString('hex') 27 | if (nHex[0] === '0') { 28 | nHex = nHex.substring(1) 29 | } 30 | return ethUtil.addHexPrefix(nHex) 31 | } 32 | 33 | function quantityHexToInt(prefixedQuantityHex) { 34 | assert(typeof prefixedQuantityHex === 'string', 'arg to quantityHexToInt must be a string') 35 | var quantityHex = ethUtil.stripHexPrefix(prefixedQuantityHex) 36 | var isEven = quantityHex.length % 2 === 0 37 | if (!isEven) { 38 | quantityHex = '0' + quantityHex 39 | } 40 | var buf = Buffer.from(quantityHex, 'hex') 41 | return ethUtil.bufferToInt(buf) 42 | } 43 | -------------------------------------------------------------------------------- /util/stoplight.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events').EventEmitter 2 | const inherits = require('util').inherits 3 | 4 | module.exports = Stoplight 5 | 6 | 7 | inherits(Stoplight, EventEmitter) 8 | 9 | function Stoplight(){ 10 | const self = this 11 | EventEmitter.call(self) 12 | self.isLocked = true 13 | } 14 | 15 | Stoplight.prototype.go = function(){ 16 | const self = this 17 | self.isLocked = false 18 | self.emit('unlock') 19 | } 20 | 21 | Stoplight.prototype.stop = function(){ 22 | const self = this 23 | self.isLocked = true 24 | self.emit('lock') 25 | } 26 | 27 | Stoplight.prototype.await = function(fn){ 28 | const self = this 29 | if (self.isLocked) { 30 | self.once('unlock', fn) 31 | } else { 32 | setTimeout(fn) 33 | } 34 | } -------------------------------------------------------------------------------- /zero.js: -------------------------------------------------------------------------------- 1 | const ProviderEngine = require('./index.js') 2 | const DefaultFixture = require('./subproviders/default-fixture.js') 3 | const NonceTrackerSubprovider = require('./subproviders/nonce-tracker.js') 4 | const CacheSubprovider = require('./subproviders/cache.js') 5 | const FilterSubprovider = require('./subproviders/filters') 6 | const SubscriptionSubprovider = require('./subproviders/subscriptions') 7 | const InflightCacheSubprovider = require('./subproviders/inflight-cache') 8 | const HookedWalletSubprovider = require('./subproviders/hooked-wallet.js') 9 | const SanitizingSubprovider = require('./subproviders/sanitizer.js') 10 | const InfuraSubprovider = require('./subproviders/infura.js') 11 | const FetchSubprovider = require('./subproviders/fetch.js') 12 | const WebSocketSubprovider = require('./subproviders/websocket.js') 13 | 14 | 15 | module.exports = ZeroClientProvider 16 | 17 | 18 | function ZeroClientProvider(opts = {}){ 19 | const connectionType = getConnectionType(opts) 20 | 21 | const engine = new ProviderEngine(opts.engineParams) 22 | 23 | // static 24 | const staticSubprovider = new DefaultFixture(opts.static) 25 | engine.addProvider(staticSubprovider) 26 | 27 | // nonce tracker 28 | engine.addProvider(new NonceTrackerSubprovider()) 29 | 30 | // sanitization 31 | const sanitizer = new SanitizingSubprovider() 32 | engine.addProvider(sanitizer) 33 | 34 | // cache layer 35 | const cacheSubprovider = new CacheSubprovider() 36 | engine.addProvider(cacheSubprovider) 37 | 38 | // filters + subscriptions 39 | // only polyfill if not websockets 40 | if (connectionType !== 'ws') { 41 | engine.addProvider(new SubscriptionSubprovider()) 42 | engine.addProvider(new FilterSubprovider()) 43 | } 44 | 45 | // inflight cache 46 | const inflightCache = new InflightCacheSubprovider() 47 | engine.addProvider(inflightCache) 48 | 49 | // id mgmt 50 | const idmgmtSubprovider = new HookedWalletSubprovider({ 51 | // accounts 52 | getAccounts: opts.getAccounts, 53 | // transactions 54 | processTransaction: opts.processTransaction, 55 | approveTransaction: opts.approveTransaction, 56 | signTransaction: opts.signTransaction, 57 | publishTransaction: opts.publishTransaction, 58 | // messages 59 | // old eth_sign 60 | processMessage: opts.processMessage, 61 | approveMessage: opts.approveMessage, 62 | signMessage: opts.signMessage, 63 | // new personal_sign 64 | processPersonalMessage: opts.processPersonalMessage, 65 | processTypedMessage: opts.processTypedMessage, 66 | approvePersonalMessage: opts.approvePersonalMessage, 67 | approveTypedMessage: opts.approveTypedMessage, 68 | signPersonalMessage: opts.signPersonalMessage, 69 | signTypedMessage: opts.signTypedMessage, 70 | personalRecoverSigner: opts.personalRecoverSigner, 71 | }) 72 | engine.addProvider(idmgmtSubprovider) 73 | 74 | // data source 75 | const dataSubprovider = opts.dataSubprovider || createDataSubprovider(connectionType, opts) 76 | engine.addProvider(dataSubprovider) 77 | 78 | // start polling 79 | if (!opts.stopped) { 80 | engine.start() 81 | } 82 | 83 | return engine 84 | 85 | } 86 | 87 | function createDataSubprovider(connectionType, opts) { 88 | const { rpcUrl, debug } = opts 89 | 90 | // default to infura 91 | if (!connectionType) { 92 | return new InfuraSubprovider() 93 | } 94 | if (connectionType === 'http') { 95 | return new FetchSubprovider({ rpcUrl, debug }) 96 | } 97 | if (connectionType === 'ws') { 98 | return new WebSocketSubprovider({ rpcUrl, debug }) 99 | } 100 | 101 | throw new Error(`ProviderEngine - unrecognized connectionType "${connectionType}"`) 102 | } 103 | 104 | function getConnectionType({ rpcUrl }) { 105 | if (!rpcUrl) return undefined 106 | 107 | const protocol = rpcUrl.split(':')[0].toLowerCase() 108 | switch (protocol) { 109 | case 'http': 110 | case 'https': 111 | return 'http' 112 | case 'ws': 113 | case 'wss': 114 | return 'ws' 115 | default: 116 | throw new Error(`ProviderEngine - unrecognized protocol in "${rpcUrl}"`) 117 | } 118 | } 119 | --------------------------------------------------------------------------------