├── .gitignore ├── README.md ├── http.js ├── index.js ├── ipc.js ├── metamask.js ├── package.json └── ws.js /.gitignore: -------------------------------------------------------------------------------- 1 | sandbox.js 2 | sandbox/ 3 | node_modules 4 | bundle.js 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nanoeth 2 | 3 | Tiny RPC module to request methods on an ETH node 4 | 5 | ``` sh 6 | npm install nanoeth 7 | ``` 8 | 9 | ## Usage 10 | 11 | ``` js 12 | const NanoETH = require('nanoeth/ipc') 13 | 14 | // pass in the ipc socket path 15 | const eth = new NanoETH('/tmp/parity.sock') 16 | 17 | // call methods 18 | await eth.blockNumber() 19 | 20 | // to close the underlying socket/rpc when all 21 | // current requests are done do 22 | await eth.end() 23 | ``` 24 | 25 | For a list of supported methods see https://wiki.parity.io/JSONRPC-eth-module.html 26 | 27 | If you are using Parity you can also use the pubsub module, to subscribe to 28 | changes: 29 | 30 | ```js 31 | const unsubscribe = await eth.subscribe(eth.getBlockByNumber('latest', false), function (err, block) { 32 | if (err) return 33 | if (parseInt(block.timestamp) > Date.now() - 1000 * 60) return unsubscribe() 34 | 35 | console.log(block) 36 | }) 37 | ``` 38 | 39 | ## RPC providers 40 | 41 | The following RPC providers are included 42 | 43 | * `nanoeth/metamask` 44 | * `nanoeth/ipc` 45 | * `nanoeth/ws` 46 | * `nanoeth/http` 47 | 48 | ## API 49 | 50 | ### `const unlisten = await subscribe(req, listener)` 51 | 52 | Create a new pubsub subscription to a "Request". Requests are other method calls 53 | that have not yet been awaited. `subscribe` resolves once the subscription has 54 | been confirmed by the node. `unlisten` is a method that can be called to 55 | unsubscribe. `listener` is called with `(err, data)`. 56 | 57 | ### `await eth.end()` 58 | 59 | End the client gracefully 60 | 61 | ### `await eth.destroy()` 62 | 63 | End the client forcefully 64 | 65 | ### `const bool = eth.destroyed` 66 | 67 | Flag whether the client has been destroyed 68 | 69 | ### `const accounts = await eth.accounts()` 70 | 71 | ### `const height = await eth.blockNumber()` 72 | 73 | ### `const data = await eth.call(obj, [from])` 74 | 75 | ### `const id = await eth.chainId()` 76 | 77 | ### `const addr = await eth.coinbase()` 78 | 79 | ### `const gas = await eth.estimateGas(obj, [from])` 80 | 81 | ### `const price = await eth.gasPrice()` 82 | 83 | ### `const balance = await eth.getBalance(addr, [from])` 84 | 85 | ### `const block = await eth.getBlockByHash(hash, [tx = false])` 86 | 87 | ### `const block = await eth.getBlockByNumber(n, [tx = false])` 88 | 89 | ### `const count = await eth.getBlockTransactionCountByHash(hash)` 90 | 91 | ### `const count = await eth.getBlockTransactionCountByNumber(n)` 92 | 93 | ### `const code = await eth.getCode(addr, [from])` 94 | 95 | ### `const changes = await eth.getFilterChanges(id)` 96 | 97 | ### `const logs = await eth.getFilterLogs(id)` 98 | 99 | ### `const logs = await eth.getLogs(obj)` 100 | 101 | ### `const bytes = await eth.getStorageAt(addr, pos, [from])` 102 | 103 | ### `const tx = await eth.getTransactionByBlockHashAndIndex(hash, pos)` 104 | 105 | ### `const tx = await eth.getTransactionByBlockNumberAndIndex(hash, pos)` 106 | 107 | ### `const tx = await eth.getTransactionByHash(hash)` 108 | 109 | ### `const count = await eth.getTransactionCount(addr, [from])` 110 | 111 | ### `const receipt = await eth.getTransactionReceipt(hash)` 112 | 113 | ### `const block = await eth.getUncleByBlockHashAndIndex(hash, pos)` 114 | 115 | ### `const block = await eth.getUncleByBlockNumberAndIndex(n, pos)` 116 | 117 | ### `const count = await eth.getUncleCountByBlockHash(hash)` 118 | 119 | ### `const count = await eth.getUncleCountByBlockNumber(hash)` 120 | 121 | ### `const work = await eth.getWork()` 122 | 123 | ### `const rate = await eth.hashrate()` 124 | 125 | ### `const bool = await eth.mining()` 126 | 127 | ### `const id = await eth.newBlockFilter()` 128 | 129 | ### `const id = await eth.newFilter(obj)` 130 | 131 | ### `const id = await eth.newPendingTransactionFilter()` 132 | 133 | ### `const version = await eth.protocolVersion()` 134 | 135 | ### `const hash = await eth.sendRawTransaction(data)` 136 | 137 | ### `const hash = await eth.sendTransaction(data)` 138 | 139 | ### `const sig = await eth.sign(addr, data)` 140 | 141 | ### `const raw = await eth.signTransaction(obj)` 142 | 143 | ### `const success = await eth.submitHashrate(rate, id)` 144 | 145 | ### `const success = await eth.submitWork(nonce, pow, mix)` 146 | 147 | ### `const info = await eth.syncing()` 148 | 149 | ### `const success = await eth.uninstallFilter(id)` 150 | 151 | ## License 152 | 153 | MIT 154 | -------------------------------------------------------------------------------- /http.js: -------------------------------------------------------------------------------- 1 | const ETH = require('./') 2 | const got = require('got') 3 | module.exports = class HTTP extends ETH { 4 | constructor (endpoint) { 5 | super(new RPC(endpoint)) 6 | } 7 | } 8 | 9 | class RPC { 10 | constructor (endpoint) { 11 | this.endpoint = endpoint 12 | this.destroyed = false 13 | } 14 | 15 | async request (method, params) { 16 | const res = await got.post({ 17 | url: this.endpoint, 18 | timeout: 5000, 19 | json: { 20 | jsonrpc: '2.0', 21 | method, 22 | params, 23 | id: 1 24 | }, 25 | responseType: 'json' 26 | }) 27 | 28 | if (res.body.error) { 29 | const error = new Error(res.body.error.message) 30 | error.code = res.body.error.code 31 | throw error 32 | } 33 | 34 | return res.body.result 35 | } 36 | 37 | subscribe () { 38 | throw new Error('HTTP does not support pubsub') 39 | } 40 | 41 | destroy () {} 42 | } 43 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | class Request { 2 | constructor (rpc, method, args = []) { 3 | this.rpc = rpc 4 | this.method = method 5 | this.args = args 6 | 7 | this._promise = null 8 | } 9 | 10 | then (resolve, reject) { 11 | if (this.promise == null) this.promise = this.rpc.request(this.method, this.args) 12 | return this.promise.then(resolve, reject) 13 | } 14 | 15 | catch (reject) { 16 | if (this.promise == null) this.promise = this.rpc.request(this.method, this.args) 17 | return this.promise.catch(reject) 18 | } 19 | 20 | finally (cb) { 21 | if (this.promise == null) this.promise = this.rpc.request(this.method, this.args) 22 | return this.promise.finally(cb) 23 | } 24 | } 25 | 26 | module.exports = class ETH { 27 | constructor (rpc) { 28 | this.rpc = rpc 29 | } 30 | 31 | subscribe (req, cb) { 32 | return req.rpc.subscribe(req.method, req.args, cb) 33 | } 34 | 35 | accounts () { 36 | return new Request(this.rpc, 'eth_accounts', []) 37 | } 38 | 39 | blockNumber () { 40 | return new Request(this.rpc, 'eth_blockNumber', []) 41 | } 42 | 43 | call (obj, from) { 44 | return new Request(this.rpc, 'eth_call', from ? [obj, from] : [obj]) 45 | } 46 | 47 | chainId () { 48 | return new Request(this.rpc, 'eth_chainId', []) 49 | } 50 | 51 | coinbase () { 52 | return new Request(this.rpc, 'eth_coinbase', []) 53 | } 54 | 55 | estimateGas (obj, from) { 56 | return new Request(this.rpc, 'eth_estimateGas', from ? [obj, from] : [obj]) 57 | } 58 | 59 | gasPrice () { 60 | return new Request(this.rpc, 'eth_gasPrice', []) 61 | } 62 | 63 | getBalance (obj, from) { 64 | return new Request(this.rpc, 'eth_getBalance', from ? [obj, from] : [obj]) 65 | } 66 | 67 | getBlockByHash (hash, tx) { 68 | return new Request(this.rpc, 'eth_getBlockByHash', [hash, tx || false]) 69 | } 70 | 71 | getBlockByNumber (n, tx) { 72 | return new Request(this.rpc, 'eth_getBlockByNumber', [n, tx || false]) 73 | } 74 | 75 | getBlockTransactionCountByHash (hash) { 76 | return new Request(this.rpc, 'eth_getBlockTransactionCountByHash', [hash]) 77 | } 78 | 79 | getBlockTransactionCountByNumber (n) { 80 | return new Request(this.rpc, 'eth_getBlockTransactionCountByNumber', [n]) 81 | } 82 | 83 | getCode (addr, from) { 84 | return new Request(this.rpc, 'eth_getCode', from ? [addr, from] : [addr]) 85 | } 86 | 87 | getFilterChanges (id) { 88 | return new Request(this.rpc, 'eth_getFilterChanges', [id]) 89 | } 90 | 91 | getFilterLogs (id) { 92 | return new Request(this.rpc, 'eth_getFilterLogs', [id]) 93 | } 94 | 95 | getLogs (obj) { 96 | return new Request(this.rpc, 'eth_getLogs', [obj]) 97 | } 98 | 99 | getStorageAt (addr, pos, from) { 100 | return new Request(this.rpc, 'eth_getStorageAt', from ? [addr, pos, from] : [addr, pos]) 101 | } 102 | 103 | getTransactionByBlockHashAndIndex (hash, pos) { 104 | return new Request(this.rpc, 'eth_getTransactionByBlockHashAndIndex', [hash, pos]) 105 | } 106 | 107 | getTransactionByBlockNumberAndIndex (hash, pos) { 108 | return new Request(this.rpc, 'eth_getTransactionByBlockNumberAndIndex', [hash, pos]) 109 | } 110 | 111 | getTransactionByHash (hash) { 112 | return new Request(this.rpc, 'eth_getTransactionByHash', [hash]) 113 | } 114 | 115 | getTransactionCount (addr, from) { 116 | return new Request(this.rpc, 'eth_getTransactionCount', from ? [addr, from] : [addr]) 117 | } 118 | 119 | getTransactionReceipt (hash) { 120 | return new Request(this.rpc, 'eth_getTransactionReceipt', [hash]) 121 | } 122 | 123 | getUncleByBlockHashAndIndex (hash, pos) { 124 | return new Request(this.rpc, 'eth_getUncleByBlockHashAndIndex', [hash, pos]) 125 | } 126 | 127 | getUncleByBlockNumberAndIndex (n, pos) { 128 | return new Request(this.rpc, 'eth_getUncleByBlockNumberAndIndex', [n, pos]) 129 | } 130 | 131 | getUncleCountByBlockHash (hash) { 132 | return new Request(this.rpc, 'eth_getUncleCountByBlockHash', [hash]) 133 | } 134 | 135 | getUncleCountByBlockNumber (hash) { 136 | return new Request(this.rpc, 'eth_getUncleCountByBlockNumber', [hash]) 137 | } 138 | 139 | getWork () { 140 | return new Request(this.rpc, 'eth_getWork', []) 141 | } 142 | 143 | hashrate () { 144 | return new Request(this.rpc, 'eth_hashrate', []) 145 | } 146 | 147 | mining () { 148 | return new Request(this.rpc, 'eth_mining', []) 149 | } 150 | 151 | newBlockFilter () { 152 | return new Request(this.rpc, 'eth_newBlockFilter', []) 153 | } 154 | 155 | newFilter (obj) { 156 | return new Request(this.rpc, 'eth_newFilter', [obj]) 157 | } 158 | 159 | newPendingTransactionFilter () { 160 | return new Request(this.rpc, 'eth_newPendingTransactionFilter', []) 161 | } 162 | 163 | protocolVersion () { 164 | return new Request(this.rpc, 'eth_protocolVersion', []) 165 | } 166 | 167 | sendRawTransaction (data) { 168 | return new Request(this.rpc, 'eth_sendRawTransaction', [data]) 169 | } 170 | 171 | sendTransaction (data) { 172 | return new Request(this.rpc, 'eth_sendTransaction', [data]) 173 | } 174 | 175 | sign (addr, data) { 176 | return new Request(this.rpc, 'eth_sign', [addr, data]) 177 | } 178 | 179 | signTransaction (obj) { 180 | return new Request(this.rpc, 'eth_signTransaction', [obj]) 181 | } 182 | 183 | submitHashrate (a, b) { 184 | return new Request(this.rpc, 'eth_submitHashrate', [a, b]) 185 | } 186 | 187 | submitWork (a, b, c) { 188 | return new Request(this.rpc, 'eth_submitWork', [a, b, c]) 189 | } 190 | 191 | syncing () { 192 | return new Request(this.rpc, 'eth_syncing', []) 193 | } 194 | 195 | uninstallFilter (id) { 196 | return new Request(this.rpc, 'eth_uninstallFilter', [id]) 197 | } 198 | 199 | end () { 200 | return this.rpc.end ? this.rpc.end() : Promise.resolve() 201 | } 202 | 203 | destroy () { 204 | if (this.rpc.destroy) this.rpc.destroy() 205 | } 206 | 207 | get destroyed () { 208 | return !!this.rpc.destroyed 209 | } 210 | 211 | static hexToBigInt (s) { 212 | return BigInt(s, 16) 213 | } 214 | 215 | static bigIntToHex (n) { 216 | return '0x' + n.toString(16) 217 | } 218 | 219 | static hexToNumber (s) { 220 | return Number(s, 16) 221 | } 222 | 223 | static numberToHex (n) { 224 | return '0x' + n.toString(16) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /ipc.js: -------------------------------------------------------------------------------- 1 | const net = require('net') 2 | const ETH = require('./index') 3 | 4 | module.exports = class IPC extends ETH { 5 | constructor (socket) { 6 | super(new RPC(socket)) 7 | } 8 | } 9 | 10 | class RPC { 11 | constructor (socket) { 12 | this.id = 0 13 | this.inflight = new Map() 14 | this.subscriptions = new Map() 15 | this.socket = typeof socket === 'string' ? net.connect(socket) : socket 16 | this.socket.unref() 17 | 18 | let buf = '' 19 | const self = this 20 | 21 | this.socket.setEncoding('utf-8') 22 | this.socket.on('data', ondata) 23 | this.socket.on('error', this.socket.destroy) 24 | this.socket.on('close', onclose) 25 | this.destroyed = false 26 | 27 | this.ending = null 28 | this.endingResolve = null 29 | 30 | function onclose () { 31 | self.destroyed = true 32 | self.socket = null 33 | for (const [resolve, reject] of self.inflight.values()) { 34 | reject(new Error('Socket destroyed')) 35 | } 36 | if (self.endingResolve) self.endingResolve() 37 | } 38 | 39 | function onmessage (message) { 40 | let obj 41 | 42 | try { 43 | obj = JSON.parse(message) 44 | } catch (_) { 45 | return false 46 | } 47 | 48 | if (obj.method === 'parity_subscription') { 49 | const cb = self.subscriptions.get(obj.params.subscription) 50 | if (cb != null) { 51 | if (obj.params.error) { 52 | const err = new Error(obj.params.error.message) 53 | err.code = obj.params.error.code 54 | cb(err) 55 | return true 56 | } 57 | 58 | cb(null, obj.params.result) 59 | return true 60 | } 61 | } 62 | 63 | const p = self.inflight.get(obj.id) 64 | if (!p) return false 65 | 66 | self.inflight.delete(obj.id) 67 | if (!self.active()) { 68 | self.socket.unref() 69 | if (self.ending) self.socket.end() 70 | } 71 | 72 | if (obj.error) { 73 | const err = new Error(obj.error.message) 74 | err.code = obj.error.code 75 | p[1](err) 76 | return true 77 | } 78 | 79 | p[0](obj.result) 80 | return true 81 | } 82 | 83 | function ondata (data) { 84 | buf += data 85 | while (true) { 86 | const n = buf.indexOf('\n') 87 | if (n === -1) return 88 | if (!onmessage(buf.slice(0, n).trim())) { 89 | self.socket.destroy() 90 | return 91 | } 92 | buf = buf.slice(n + 1) 93 | } 94 | } 95 | } 96 | 97 | active () { 98 | return this.inflight.size > 0 || this.subscriptions.size > 0 99 | } 100 | 101 | request (method, params) { 102 | if (!this.socket) return Promise.reject(new Error('Socket destroyed')) 103 | 104 | const id = '' + ++this.id 105 | const obj = { jsonrpc: '2.0', id, method, params } 106 | 107 | return new Promise((resolve, reject) => { 108 | this.inflight.set(id, [resolve, reject]) 109 | if (this.active()) this.socket.ref() 110 | this.socket.write(JSON.stringify(obj) + '\n') 111 | }) 112 | } 113 | 114 | async subscribe (method, params, cb) { 115 | if (cb == null) return this.subscribe(method, [], params) 116 | 117 | const id = await this.request('parity_subscribe', [method, params]) 118 | this.subscriptions.set(id, cb) 119 | 120 | return async () => { 121 | await this.request('parity_unsubscribe', [id]) 122 | this.subscriptions.delete(id) 123 | } 124 | } 125 | 126 | end () { 127 | this.ending = new Promise(resolve => { 128 | if (!this.socket) return resolve() 129 | this.endingResolve = resolve 130 | if (!this.active()) this.socket.destroy() 131 | }) 132 | 133 | return this.ending 134 | } 135 | 136 | destroy () { 137 | if (this.socket) this.socket.destroy() 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /metamask.js: -------------------------------------------------------------------------------- 1 | const ETH = require('./') 2 | 3 | module.exports = class Metamask extends ETH { 4 | constructor () { 5 | super(new RPC()) 6 | } 7 | } 8 | 9 | class RPC { 10 | constructor () { 11 | this.enable = window.ethereum.enable() 12 | this.destroyed = false 13 | } 14 | 15 | request (method, params) { 16 | return this.enable.then(accounts => { 17 | return new Promise((resolve, reject) => { 18 | window.ethereum.sendAsync({ 19 | method, 20 | params, 21 | from: accounts[0] 22 | }, function (err, res) { 23 | if (err) { 24 | const error = new Error(err.message) 25 | error.code = err.code 26 | return reject(error) 27 | } 28 | resolve(res.result) 29 | }) 30 | }) 31 | }) 32 | } 33 | 34 | subscribe () { 35 | throw new Error('Metamask does not support pubsub') 36 | } 37 | 38 | destroy () {} 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nanoeth", 3 | "version": "2.6.1", 4 | "description": "Tiny RPC module to request methods on an ETH node", 5 | "main": "index.js", 6 | "dependencies": { 7 | "got": "^11.5.2" 8 | }, 9 | "devDependencies": {}, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/hyperdivision/nanoeth.git" 16 | }, 17 | "keywords": [ 18 | "eth", 19 | "parity" 20 | ], 21 | "author": "Mathias Buus (@mafintosh)", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/hyperdivision/nanoeth/issues" 25 | }, 26 | "homepage": "https://github.com/hyperdivision/nanoeth#readme" 27 | } 28 | -------------------------------------------------------------------------------- /ws.js: -------------------------------------------------------------------------------- 1 | const ETH = require('./index') 2 | 3 | module.exports = class WS extends ETH { 4 | constructor (socket) { 5 | super(new RPC(socket)) 6 | } 7 | } 8 | 9 | class RPC { 10 | constructor (socket) { 11 | this.id = 0 12 | this.inflight = new Map() 13 | this.subscriptions = new Map() 14 | this.socket = socket 15 | 16 | const self = this 17 | 18 | on(this.socket, 'message', onmessage) 19 | on(this.socket, 'close', onclose) 20 | on(this.socket, 'open', onopen) 21 | 22 | this.destroyed = false 23 | this.ending = null 24 | this.endingResolve = null 25 | this.queued = [] 26 | this.opened = false 27 | 28 | function onopen () { 29 | self.opened = true 30 | for (const s of self.queued) self.socket.send(s) 31 | } 32 | 33 | function onclose () { 34 | self.destroyed = true 35 | self.socket = null 36 | for (const [resolve, reject] of self.inflight.values()) { 37 | reject(new Error('WebSocket destroyed')) 38 | } 39 | if (self.endingResolve) self.endingResolve() 40 | } 41 | 42 | function onmessage (message) { 43 | let obj 44 | 45 | try { 46 | obj = JSON.parse(message) 47 | } catch (_) { 48 | return false 49 | } 50 | 51 | if (obj.method === 'parity_subscription') { 52 | const cb = self.subscriptions.get(obj.params.subscription) 53 | if (cb != null) { 54 | if (obj.params.error) { 55 | const err = new Error(obj.params.error.message) 56 | err.code = obj.params.error.code 57 | cb(err) 58 | return true 59 | } 60 | 61 | cb(null, obj.params.result) 62 | return true 63 | } 64 | } 65 | 66 | const p = self.inflight.get(obj.id) 67 | if (!p) return false 68 | 69 | self.inflight.delete(obj.id) 70 | if (self.inflight.size === 0) { 71 | if (self.ending) self.socket.close() 72 | } 73 | 74 | if (obj.error) { 75 | const err = new Error(obj.error.message) 76 | err.code = obj.error.code 77 | p[1](err) 78 | return true 79 | } 80 | 81 | p[0](obj.result) 82 | return true 83 | } 84 | } 85 | 86 | request (method, params) { 87 | if (!this.socket) return Promise.reject(new Error('Socket destroyed')) 88 | 89 | const id = '' + ++this.id 90 | const obj = { jsonrpc: '2.0', id, method, params } 91 | 92 | return new Promise((resolve, reject) => { 93 | this.inflight.set(id, [resolve, reject]) 94 | 95 | const s = JSON.stringify(obj) 96 | if (!this.opened) this.queued.push(s) 97 | else this.socket.send(s) 98 | }) 99 | } 100 | 101 | async subscribe (method, params, cb) { 102 | if (cb == null) return this.subscribe(method, [], params) 103 | 104 | const id = await this.request('parity_subscribe', [method, params]) 105 | this.subscriptions.set(id, cb) 106 | 107 | return async () => { 108 | await this.request('parity_unsubscribe', [id]) 109 | this.subscriptions.delete(id) 110 | } 111 | } 112 | 113 | end () { 114 | this.ending = new Promise(resolve => { 115 | if (!this.socket) return resolve() 116 | this.endingResolve = resolve 117 | if (this.inflight.size === 0) this.socket.close() 118 | }) 119 | 120 | return this.ending 121 | } 122 | 123 | destroy () { 124 | if (this.socket) this.socket.close() 125 | } 126 | } 127 | 128 | function on (e, name, fn) { 129 | if (e.on) e.on(name, fn) 130 | else if (e.addEventListener) e.addEventListener(name, fn) 131 | } 132 | --------------------------------------------------------------------------------