├── README.md ├── index.js └── package.json /README.md: -------------------------------------------------------------------------------- 1 | Ethers Web3 Provider Bridge 2 | =========================== 3 | 4 | This package has been deprecated in favor of the [Eeip1193Bridge](https://docs.ethers.io/v5/api/experimental/#experimental-eip1193bridge). Please use that class in the [@ethersproject/experimental](https://www.npmjs.com/package/@ethersproject/experimental) package instead. 5 | 6 | See the [previous version of the README.md](https://github.com/ethers-io/ethers-web3-bridge/blob/3b3929b253959dbd97185e2233246a59d79495d8/README.md) for documentation. 7 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var providers = require('ethers-providers'); 4 | var utils = require('ethers-utils'); 5 | 6 | var Errors = { 7 | InternalError: -32603, 8 | InvalidRequest: -32600, 9 | ParseError: -32700, 10 | MethodNotFound: -32601, 11 | InvalidParams: -32602, 12 | }; 13 | 14 | // Some implementations of things do not play well with leading zeros 15 | function smallHexlify(value) { 16 | value = utils.hexlify(value) 17 | while (value.length > 3 && value.substring(0, 3) === '0x0') { 18 | value = '0x' + value.substring(3); 19 | } 20 | return value; 21 | } 22 | 23 | // Convert a Web3 Transaction into an ethers.js Transaction 24 | function makeTransaction(tx) { 25 | var result = {}; 26 | ['data', 'from', 'gasPrice', 'to', 'value'].forEach(function(key) { 27 | if (tx[key] == null) { return; } 28 | result[key] = tx[key]; 29 | }); 30 | if (tx.gas != null) { result.gasLimit = tx.gas; } 31 | return result; 32 | } 33 | 34 | function fillCompact(values, result, keys, keepNull) { 35 | keys.forEach(function(key) { 36 | var value = values[key]; 37 | if (value == null) { 38 | if (!keepNull) { return; } 39 | value = null; 40 | } else { 41 | value = smallHexlify(value); 42 | } 43 | result[key] = value; 44 | }); 45 | } 46 | 47 | function fillCopy(values, result, keys, keepNull) { 48 | keys.forEach(function(key) { 49 | var value = values[key]; 50 | if (value == null) { 51 | if (!keepNull) { return; } 52 | value = null; 53 | } 54 | result[key] = value; 55 | }); 56 | } 57 | 58 | // Convert ethers.js Block into Web3 Block 59 | function formatBlock(block) { 60 | var result = {}; 61 | 62 | fillCompact(block, result, ['difficulty', 'gasLimit', 'gasUsed', 'number', 'timestamp']); 63 | 64 | fillCopy(block, result, ['extraData', 'miner', 'parentHash']); 65 | 66 | fillCompact(block, result, ['number'], true); 67 | 68 | fillCopy(block, result, ['hash', 'nonce'], true); 69 | 70 | return result; 71 | } 72 | 73 | // Convert ethers.js Transaction into Web3 Transaction 74 | function formatTransaction(tx) { 75 | var result = {}; 76 | 77 | if (tx.gasLimit) { result.gas = smallHexlify(tx.gasLimit); } 78 | result.input = (tx.data || '0x'); 79 | 80 | fillCompact(tx, result, ['blockNumber', 'gasPrice', 'nonce', 'transactionIndex', 'value'], true); 81 | 82 | fillCopy(tx, result, ['blockHash', 'from', 'hash', 'to'], true); 83 | 84 | return result; 85 | } 86 | 87 | 88 | // Convert ethers.js Transaction Receiptinto Web3 Transaction 89 | function formatReceipt(receipt) { 90 | var result = { logs: [] }; 91 | 92 | fillCompact(receipt, result, ['blockNumber', 'cumulativeGasUsed', 'gasPrice', 'gasUsed', 'transactionIndex'], true); 93 | 94 | fillCopy(receipt, result, ['blockHash', 'contractAddress', 'from', 'logsBloom', 'transactionHash', 'root', 'to'], true); 95 | 96 | (receipt.logs || []).forEach(function(log) { 97 | var log = { }; 98 | result.logs.push(log); 99 | 100 | if (receipt.removed != null) { log.removed = receipt.removed; } 101 | if (receipt.topics != null) { log.topics = receipt.topics; } 102 | 103 | fillCompact(receipt, log, ['blockNumber', 'logIndex', 'transactionIndex'], true); 104 | 105 | fillCopy(receipt, log, ['address', 'blockHash', 'data', 'transactionHash'], true); 106 | }); 107 | 108 | return result; 109 | } 110 | // Convert ethers.js Log into Web3 Log 111 | function formatLog(log) { 112 | var result = {}; 113 | ['blockNumber', 'logIndex', 'transactionIndex'].forEach(function(key) { 114 | if (log[key] == null) { return; } 115 | result[key] = smallHexlify(log[key]); 116 | }); 117 | ['address', 'blockHash', 'data', 'topics', 'transactionHash'].forEach(function(key) { 118 | if (log[key] == null) { return; } 119 | result[key] = log[key]; 120 | }); 121 | return log; 122 | } 123 | 124 | 125 | function FilterManager() { 126 | utils.defineProperty(this, 'filters', {}); 127 | 128 | var nextFilterId = 1; 129 | utils.defineProperty(this, '_getFilterId', function() { 130 | return nextFilterId++; 131 | }); 132 | } 133 | 134 | FilterManager.prototype.addFilter = function(onblock, getLogs) { 135 | if (!getLogs) { getLogs = function() { return Promise.resolve([]); } } 136 | 137 | var filterId = this._getFilterId(); 138 | 139 | var seq = Promise.resolve([]); 140 | 141 | function emitBlock(blockNumber) { 142 | seq = seq.then(function(result) { 143 | return new Promise(function(resolve, reject) { 144 | function check() { 145 | provider.getBlock(blockNumber).then(function(block) { 146 | onblock(block, result).then(function(result) { 147 | resolve(result); 148 | }); 149 | }, function (error) { 150 | // Does not exist yet; try again in a second 151 | setTimeout(check, 1000); 152 | }); 153 | } 154 | check(); 155 | }); 156 | }); 157 | } 158 | 159 | this.filters[smallHexlify(filterId)] = { 160 | getChanges: function() { 161 | var result = seq; 162 | 163 | // Reset the filter results 164 | seq = Promise.resolve([]); 165 | return result; 166 | }, 167 | getLogs: getLogs, 168 | lastPoll: now(), 169 | uninstall: function() { 170 | provider.removeListener('block', emitBlock); 171 | seq = null; 172 | } 173 | }; 174 | 175 | provider.on('block', emitBlock); 176 | 177 | return smallHexlify(filterId); 178 | } 179 | 180 | FilterManager.prototype.removeFilter = function(filterId) { 181 | var filter = this.filters[smallHexlify(filterId)]; 182 | if (!filter) { return false; } 183 | filter.uninstall(); 184 | return true; 185 | } 186 | 187 | FilterManager.prototype.getChanges = function(filterId) { 188 | var filter = this.filters[smallHexlify(filterId)]; 189 | if (!filter) { Promise.resolve([]); } 190 | return filter.getChanges(); 191 | } 192 | 193 | FilterManager.prototype.getLogs = function(filterId) { 194 | var filter = this.filters[smallHexlify(filterId)]; 195 | if (!filter) { return Promise.resolve([]); } 196 | return filter.getLogs(); 197 | } 198 | 199 | var version = require('./package.json').version; 200 | 201 | function ProviderBridge(provider, signer) { 202 | if (!(this instanceof ProviderBridge)) { throw new Error('missing new'); } 203 | this._provider = provider || null; 204 | this._signer = signer || null; 205 | 206 | var self = this; 207 | setInterval(function() { 208 | if (!this._signer) { 209 | this._address = null; 210 | return; 211 | } 212 | 213 | this._signer.getAddress().then(function(address) { 214 | this._address = address; 215 | }, function(error) { 216 | this._address = null; 217 | }); 218 | }, 1000); 219 | 220 | this._queue = []; 221 | 222 | utils.defineProperty(this, 'isMetaMask', true); 223 | utils.defineProperty(this, 'isEthers', true); 224 | utils.defineProperty(this, 'isConnected', true); 225 | utils.defineProperty(this, 'ethersVersion', version); 226 | utils.defineProperty(this, 'client', 'ethers/' + version); 227 | 228 | utils.defineProperty(this, 'filterManager', new FilterManager()); 229 | } 230 | 231 | utils.defineProperty(ProviderBridge.prototype, '_drainQueue', function() { 232 | var self = this; 233 | this._queue.forEach(function(operation) { 234 | setTimeout(function() { 235 | self._sendAsync(JSON.parse(operation.payload), operation.callback); 236 | }, 0); 237 | }); 238 | }); 239 | 240 | utils.defineProperty(ProviderBridge.prototype, '_connectWeb3', function(web3) { 241 | this._web3 = web3; 242 | this._drainQueue(); 243 | }); 244 | 245 | utils.defineProperty(ProviderBridge.prototype, '_connectEthers', function(provider, signer) { 246 | if (!signer) { 247 | var missingSigner = function() { 248 | return Promise.reject('no signer connected'); 249 | }; 250 | 251 | signer = { 252 | getAddress: missingSigner, 253 | sendTransaction: missingSigner, 254 | signMessage: missingSigner, 255 | }; 256 | } 257 | 258 | this._provider = provider; 259 | this._signer = signer; 260 | 261 | this._drainQueue(); 262 | }); 263 | 264 | utils.defineProperty(ProviderBridge.prototype, 'sendAsync', function(payload, callback) { 265 | if (!(this._provider || this._web3)) { 266 | this._queue.push({ 267 | payload: JSON.stringify(payload), 268 | callback: callback 269 | }); 270 | return; 271 | } 272 | this._sendAsync(payload, callback); 273 | }); 274 | 275 | utils.defineProperty(ProviderBridge.prototype, '_sendAsync', function(payload, callback) { 276 | if (this._web3) { 277 | this._web3.sendAsync(payload, callback); 278 | return; 279 | } 280 | 281 | var self = this; 282 | 283 | if (Array.isArray(payload)) { 284 | 285 | var promises = []; 286 | payload.forEach(function(payload) { 287 | promises.push(new Promise(function(resolve, reject) { 288 | self.sendAsync(payload, function(error, result) { 289 | resolve(error || result); 290 | }); 291 | })); 292 | }); 293 | 294 | Promise.all(promises).then(function(result) { 295 | callback(null, result); 296 | }); 297 | 298 | return; 299 | } 300 | 301 | 302 | function respondError(message, code) { 303 | if (!code) { code = Errors.InternalError; } 304 | 305 | callback(null, { 306 | id: payload.id, 307 | jsonrpc: "2.0", 308 | error: { 309 | code: code, 310 | message: message 311 | } 312 | }); 313 | } 314 | 315 | function respond(result) { 316 | callback(null, { 317 | id: payload.id, 318 | jsonrpc: "2.0", 319 | result: result 320 | }); 321 | } 322 | 323 | if (payload == null || typeof(payload.method) !== 'string' || typeof(payload.id) !== 'number' || !Array.isArray(payload.params)) { 324 | respondError('invalid sendAsync parameters', Errors.InvalidRequest); 325 | return; 326 | } 327 | 328 | var signer = this._signer; 329 | var provider = this._provider; 330 | 331 | var params = payload.params; 332 | switch (payload.method) { 333 | 334 | // Account Actions 335 | 336 | case 'eth_accounts': 337 | signer.getAddress().then(function(address) { 338 | respond([ address.toLowerCase() ]); 339 | }, function (error) { 340 | respond([]); 341 | }); 342 | break; 343 | 344 | case 'eth_sign': 345 | params = [ params[1], params[0] ]; 346 | // Fall-through 347 | 348 | case 'personal_sign': 349 | signer.getAddress().then(function(address) { 350 | if (utils.getAddress(params[1]) !== address) { 351 | respondError('invalid from address', Errors.InvalidParams); 352 | return; 353 | } 354 | 355 | signer.signMessage(params[0]).then(function(signature) { 356 | respond(signature); 357 | }, function(error) { 358 | respondError('eth_sign error', Errors.InternalError); 359 | }); 360 | }, function(error) { 361 | respondError('no account', Errors.InvalidParams); 362 | }); 363 | 364 | break; 365 | 366 | case 'eth_sendTransaction': 367 | signer.getAddress().then(function(address) { 368 | if (utils.getAddress(params[0].from) !== address) { 369 | respondError('invalid from address', Errors.InvalidParams); 370 | } 371 | return signer.sendTransaction(params[0]) 372 | }, function(error) { 373 | respondError('eth_sendTransaction error', Errors.InternalError); 374 | }); 375 | break; 376 | 377 | 378 | // Client State (mostly just default values we can pull from sync) 379 | 380 | case 'eth_coinbase': 381 | case 'eth_getCompilers': 382 | case 'eth_hashrate': 383 | case 'eth_mining': 384 | case 'eth_syncing': 385 | case 'net_listening': 386 | case 'net_peerCount': 387 | case 'net_version': 388 | case 'eth_protocolVersion': 389 | setTimeout(function() { 390 | respond(self.send(payload).result); 391 | }, 0); 392 | break; 393 | 394 | // Blockchain state 395 | 396 | case 'eth_blockNumber': 397 | provider.getBlockNumber().then(function(blockNumber) { 398 | respond(smallHexlify(blockNumber)); 399 | }); 400 | break; 401 | 402 | case 'eth_gasPrice': 403 | provider.getGasPrice().then(function(gasPrice) { 404 | respond(smallHexlify(gasPrice)); 405 | }); 406 | break; 407 | 408 | 409 | // Accounts Actions 410 | 411 | case 'eth_getBalance': 412 | provider.getBalance(params[0], params[1]).then(function(balance) { 413 | respond(smallHexlify(balance)); 414 | }); 415 | break; 416 | 417 | case 'eth_getCode': 418 | provider.getCode(params[0], params[1]).then(function(code) { 419 | respond(code); 420 | }); 421 | break; 422 | 423 | case 'eth_getTransactionCount': 424 | provider.getTransactionCount(params[0], params[1]).then(function(nonce) { 425 | respond(smallHexlify(nonce)); 426 | }); 427 | break; 428 | 429 | 430 | // Execution (read-only) 431 | 432 | case 'eth_call': 433 | provider.call(makeTransaction(params[0]), params[1]).then(function(data) { 434 | respond(data); 435 | }); 436 | break; 437 | 438 | case 'eth_estimateGas': 439 | provider.estimateGas(makeTransaction(params[0]), params[1]).then(function(data) { 440 | respond(data); 441 | }); 442 | break; 443 | 444 | case 'eth_getStorageAt': 445 | provider.getStorageAt(params[0], params[1], params[2]).then(function(data) { 446 | respond(data); 447 | }); 448 | break; 449 | 450 | 451 | // Blockchain Queries 452 | 453 | case 'eth_getBlockByHash': 454 | case 'eth_getBlockByNumber': 455 | provider.getBlock(params[0]).then(function(block) { 456 | var result = formatBlock(block); 457 | 458 | if (params[1]) { 459 | result.transactions = []; 460 | 461 | var seq = Promise.resolve(); 462 | 463 | if (block.transactions) { 464 | block.transactions.forEach(function(hash) { 465 | return provider.getTransaction(hash).then(function(tx) { 466 | result.transactions.push(tx); 467 | }); 468 | }); 469 | } 470 | 471 | seq.then(function() { 472 | respond(result); 473 | }); 474 | 475 | } else { 476 | if (block.transactions) { result.transactions = block.transactions; } 477 | respond(result); 478 | } 479 | }); 480 | break; 481 | 482 | case 'eth_getBlockTransactionCountByHash': 483 | case 'eth_getBlockTransactionCountByNumber': 484 | provider.getBlock(params[0]).then(function(block) { 485 | respond(smallHexlify(block.transactions ? block.transactions.length: 0)); 486 | }); 487 | break; 488 | 489 | case 'eth_getTransactionByHash': 490 | provider.getTransaction(params[0]).then(function(tx) { 491 | if (tx != null) { tx = formatTransaction(tx); } 492 | respond(tx); 493 | }); 494 | break; 495 | 496 | case 'eth_getTransactionByBlockHashAndIndex': 497 | case 'eth_getTransactionByBlockNumberAndIndex': 498 | provider.getBlock(params[0]).then(function(block) { 499 | if (block == null) { block = {}; } 500 | if (block.transactions == null) { block.transactions = []; } 501 | var hash = block.transactions[params[1]]; 502 | if (hash) { 503 | provider.getTransaction(hash).then(function(tx) { 504 | if (tx != null) { tx = formatTransaction(tx); } 505 | respond(tx); 506 | }); 507 | } else { 508 | respond(null); 509 | } 510 | }); 511 | break; 512 | 513 | case 'eth_getTransactionReceipt': 514 | provider.getTransactionReceipt(params[0]).then(function(receipt) { 515 | if (receipt != null) { receipt = formatReceipt(receipt); } 516 | respond(receipt); 517 | }); 518 | break; 519 | 520 | 521 | // Blockchain Manipulation 522 | 523 | case 'eth_sendRawTransaction': 524 | provider.sendTransaction(params[0]).then(function(hash) { 525 | respond(hash); 526 | }); 527 | break; 528 | 529 | 530 | // Unsupported methods 531 | case 'eth_getUncleByBlockHashAndIndex': 532 | case 'eth_getUncleByBlockNumberAndIndex': 533 | 534 | case 'eth_getUncleCountByBlockHash': 535 | case 'eth_getUncleCountByBlockNumber': 536 | respondError('unsupported method', { method: payload.method }); 537 | break; 538 | 539 | // Filters 540 | 541 | case 'eth_newFilter': 542 | (function(filter) { 543 | function getLogs(filter) { 544 | return provider.getLogs(filter).then(function(result) { 545 | for (var i = 0; i < result.length; i++) { 546 | result[i] = formatLog(result[i]); 547 | } 548 | return result; 549 | }); 550 | } 551 | 552 | respond(self.filterManager.addFilter(function(block, result) { 553 | var blockFilter = { 554 | fromBlock: block.number, 555 | toBlock: block.number 556 | } 557 | if (filter.address) { blockFilter.address = filter.address; } 558 | if (filter.topics) { blockFilter.topics = filter.topics; } 559 | return provider.getLogs(blockFilter).then(function(logs) { 560 | logs.forEach(function(log) { 561 | log.blockHash = block.hash; 562 | result.push(formatLog(log)); 563 | }); 564 | return result; 565 | }); 566 | 567 | }, function() { 568 | return provider.getLogs(filter).then(function(logs) { 569 | var seq = Promise.resolve(logs); 570 | logs.forEach(function(log) { 571 | seq = seq.then(function() { 572 | return provider.getBlock(log.blockNumber).then(function(block) { 573 | log.blockHash = block.hash; 574 | return logs; 575 | }); 576 | }); 577 | }); 578 | return seq; 579 | }); 580 | })); 581 | })(params[0]); 582 | break; 583 | 584 | case 'eth_newPendingTransactionFilter': 585 | respond(this.filterManager.addFilter(function(block, result) { 586 | (block.transactions || []).forEach(function(hash) { 587 | result.push(hash); 588 | }); 589 | result.push(block.hash); 590 | return Promise.resolve(result); 591 | })); 592 | break; 593 | 594 | case 'eth_newBlockFilter': 595 | respond(this.filterManager.addFilter(function(block, result) { 596 | result.push(block.hash); 597 | return Promise.resolve(result); 598 | })); 599 | break; 600 | 601 | case 'eth_uninstallFilter': 602 | respond(this.filterManager.removeFilter(params[0])); 603 | break; 604 | 605 | case 'eth_getFilterChanges': 606 | this.filterManager.getChanges(params[0]).then(function(result) { 607 | respond(result); 608 | }); 609 | break; 610 | 611 | case 'eth_getFilterLogs': 612 | this.filterManager.getLogs(params[0]).then(function(result) { 613 | respond(result); 614 | }); 615 | break; 616 | 617 | default: 618 | respondError('unknown method', Errors.MethodNotFound); 619 | } 620 | }); 621 | 622 | utils.defineProperty(ProviderBridge.prototype, 'send', function(payload) { 623 | if (this._web3) { 624 | return this._web3.send(payload); 625 | } 626 | 627 | var provider = this._provider; 628 | 629 | var result = null; 630 | switch(payload.method) { 631 | case 'eth_accounts': 632 | if (this._address) { 633 | result = [ this._address.toLowerCase() ]; 634 | } else { 635 | result = [ ]; 636 | } 637 | break; 638 | 639 | case 'eth_coinbase': 640 | result = null; 641 | break; 642 | 643 | case 'eth_getCompilers': 644 | result = []; 645 | break; 646 | 647 | case 'eth_hashrate': 648 | case 'net_peerCount': 649 | result = "0x0"; 650 | break; 651 | 652 | case 'eth_mining': 653 | case 'eth_syncing': 654 | case 'net_listening': 655 | result = false; 656 | break; 657 | 658 | case 'net_version': 659 | result = String(provider.chainId); 660 | break; 661 | 662 | // Using Parity/v1.8.0-beta-9882902-20171015/x86_64-macos/rustc1.20.0: 663 | // /Users/ethers> curl -H 'Content-Type: application/json' -X POST --data '{"jsonrpc":"2.0","method":"eth_protocolVersion","params":[],"id":67}' http://localhost:8545 664 | // {"jsonrpc":"2.0","result":"63","id":67} 665 | case 'eth_protocolVersion': 666 | result = "63"; 667 | break; 668 | 669 | default: 670 | throw new Error('sync unsupported'); 671 | } 672 | 673 | return { 674 | id: payload.id, 675 | jsonrpc: "2.0", 676 | result: result 677 | }; 678 | }); 679 | 680 | module.exports = ProviderBridge; 681 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethers-web3-bridge", 3 | "version": "0.0.2", 4 | "description": "Enables Web3 instances to connect using an ethers.js Provider.", 5 | "main": "index.js", 6 | "dependencies": { 7 | "ethers-providers": "^2.1.19", 8 | "ethers-utils": "^2.1.11" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1" 12 | }, 13 | "keywords": [ 14 | "ethereum", 15 | "web3", 16 | "ethers", 17 | "dapp" 18 | ], 19 | "author": "Richard Moore ", 20 | "license": "MIT", 21 | "repository": { 22 | "type": "git", 23 | "url": "git@github.com:ethers-io/ethers-web3-bridge.git" 24 | } 25 | } 26 | --------------------------------------------------------------------------------