diff --git a/error.js b/error.js new file mode 100644 index 0000000..fdcc06c --- /dev/null +++ b/error.js @@ -0,0 +1,23 @@ +var inherits = require("util").inherits; +var TruffleError = require("truffle-error"); + +// HACK: string comparison seems to be only way to identify being unable to +// connect to RPC node. +var NOT_CONNECTED_MESSAGE = 'Invalid JSON RPC response: ""'; + +function ProviderError(message, error) { + if (message == NOT_CONNECTED_MESSAGE) { + message = "Could not connect to your Ethereum client. " + + "Please check that your Ethereum client:\n" + + " - is running\n" + + " - is accepting RPC connections (i.e., \"--rpc\" option is used in geth)\n" + + " - is accessible over the network\n" + + " - is properly configured in your Truffle configuration file (truffle.js)\n"; + } + ProviderError.super_.call(this, message); + this.message = message; +} + +inherits(ProviderError, TruffleError); + +module.exports = ProviderError; diff --git a/index.js b/index.js index 124282a..af8c844 100644 --- a/index.js +++ b/index.js @@ -1,34 +1,10 @@ var Web3 = require("web3"); +var wrapper = require('./wrapper'); + module.exports = { wrap: function(provider, options) { - options = options || {}; - - if (options.verbose || options.verboseRpc) { - this.makeVerbose(provider, options.logger); - } - return provider; - }, - - makeVerbose: function(provider, logger) { - logger = logger || console; - - // // If you want to see what web3 is sending and receiving. - var oldAsync = provider.sendAsync; - - if (oldAsync.is_verbose) return; - - provider.sendAsync = function(options, callback) { - logger.log(" > " + JSON.stringify(options, null, 2).split("\n").join("\n > ")); - oldAsync.call(provider, options, function(error, result) { - if (error == null) { - logger.log(" < " + JSON.stringify(result, null, 2).split("\n").join("\n < ")); - } - callback(error, result) - }); - }; - - provider.sendAsync.is_verbose = true; + return wrapper.wrap(provider, options); }, create: function(options) { diff --git a/package.json b/package.json index ae823ab..97b3db6 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ }, "homepage": "https://github.com/trufflesuite/truffle-provider#readme", "dependencies": { + "truffle-error": "0.0.2", "web3": "^0.18.0" }, "devDependencies": { diff --git a/wrapper.js b/wrapper.js new file mode 100644 index 0000000..1a4655e --- /dev/null +++ b/wrapper.js @@ -0,0 +1,138 @@ +var ProviderError = require('./error'); + +module.exports = { + /* + * Web3.js Transport Wrapper + * + * Wraps an underlying web3 provider's RPC transport methods (send/sendAsync) + * for Truffle-specific purposes, mainly for logging / request verbosity. + */ + wrap: function(provider, options) { + /* wrapping should be idempotent */ + if (provider._alreadyWrapped) return provider; + + /* setup options defaults */ + options = options || {}; + // custom logger + options.logger = options.logger || console; + // to see what web3 is sending and receiving. + options.verbose = options.verbose || options.verboseRpc || false; + + /* create wrapper functions for before/after send */ + var preHook = this.preHook(options); + var postHook = this.postHook(options); + + var originalSend = provider.send.bind(provider); + var originalSendAsync = provider.sendAsync.bind(provider); + + /* overwrite methods */ + provider.send = this.send(originalSend, preHook, postHook); + provider.sendAsync = this.sendAsync(originalSendAsync, preHook, postHook); + + /* mark as wrapped */ + provider._alreadyWrapped = true; + + return provider; + }, + + /* + * Transport Hook Generators + * + * Used to wrap underlying web3.js behavior before/after sending request + * payloads to the RPC. + * + * Transport hooks may be used to perform additional operations before/after + * sending, and/or to modify request/response data. + * + * Each generator accepts an `options` argument and uses it to construct + * and return a function. + * + * Returned functions accept relevant arguments and return potentially new + * versions of those arguments (for payload/result/error overrides) + */ + + // before send/sendAsync + preHook: function(options) { + return function(payload) { + if (options.verbose) { + // for request payload debugging + options.logger.log(" > " + JSON.stringify(payload, null, 2).split("\n").join("\n > ")); + } + + return payload; + }; + }, + + // after send/sendAsync + postHook: function(options) { + return function(payload, error, result) { + if (error != null) { + // wrap errors in internal error class + error = new ProviderError(error.message, error); + return [payload, error, result]; + } + + if (options.verbose) { + options.logger.log(" < " + JSON.stringify(result, null, 2).split("\n").join("\n < ")); + } + + return [payload, error, result]; + }; + }, + + /* + * Transport Method Generators + * + * Generate wrapped versions of `send`/`sendAsync`, given original method and + * transport hooks. + * + * Pre-condition: originals are bound correctly (`send.bind(provider)`) + * + * Return the wrapped function matching the original function's signature. + */ + + // wrap a `provider.send` function with behavior hooks + // returns a function(payload) to replace `provider.send` + send: function(originalSend, preHook, postHook) { + return function(payload) { + var result = null; + var error = null; + + payload = preHook(payload); + + try { + result = originalSend(payload); + } catch (e) { + error = e; + } + + var modified = postHook(payload, error, result); + payload = modified[0]; + error = modified[1]; + result = modified[2]; + + if (error != null) { + throw error; + } + + return result; + } + }, + + // wrap a `provider.sendAsync` function with behavior hooks + // returns a function(payload, callback) to replace `provider.sendAsync` + sendAsync: function(originalSendAsync, preHook, postHook) { + return function(payload, callback) { + payload = preHook(payload); + + originalSendAsync(payload, function(error, result) { + var modified = postHook(payload, error, result); + payload = modified[0]; + error = modified[1]; + result = modified[2]; + + callback(error, result); + }); + }; + } +}