Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

Bug fix: Provide reason for calls #2340

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 additions & 12 deletions packages/truffle-contract/lib/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,22 +99,26 @@ var execute = {
* @return {Promise} Return value of the call.
*/
call: function(fn, methodABI, address) {
var constructor = this;
const constructor = this;
const web3 = constructor.web3;

return function() {
var defaultBlock = "latest";
var args = Array.prototype.slice.call(arguments);
var lastArg = args[args.length - 1];
var promiEvent = new Web3PromiEvent();
let defaultBlock = "latest";
let args = Array.prototype.slice.call(arguments);
const lastArg = args[args.length - 1];
const promiEvent = new Web3PromiEvent();

// Extract defaultBlock parameter
if (execute.hasDefaultBlock(args, lastArg, methodABI.inputs)) {
defaultBlock = args.pop();
}

let params;
execute
.prepareCall(constructor, methodABI, args)
.then(async ({ args, params }) => {
.then(async data => {
args = data.args;
params = data.params;
let result;

params.to = address;
Expand All @@ -135,7 +139,14 @@ var execute = {
);
return promiEvent.resolve(result);
})
.catch(promiEvent.reject);
.catch(async error => {
const reason = await override.getErrorReason(params, web3);
if (reason) {
error.reason = reason;
error.message += ` -- Reason given: ${reason}.`;
}
promiEvent.reject(error);
});

return promiEvent.eventEmitter;
};
Expand All @@ -149,17 +160,17 @@ var execute = {
* @return {PromiEvent} Resolves a transaction receipt (via the receipt handler)
*/
send: function(fn, methodABI, address) {
var constructor = this;
var web3 = constructor.web3;
const constructor = this;
const web3 = constructor.web3;

return function() {
var deferred;
var promiEvent = new Web3PromiEvent();
let deferred;
const promiEvent = new Web3PromiEvent();

execute
.prepareCall(constructor, methodABI, arguments)
.then(async ({ args, params, network }) => {
var context = {
const context = {
contract: constructor, // Can't name this field `constructor` or `_constructor`
promiEvent: promiEvent,
params: params
Expand Down
78 changes: 41 additions & 37 deletions packages/truffle-contract/lib/override.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,38 @@
var Reason = require('./reason');
var handlers = require('./handlers');
var Reason = require("./reason");
var handlers = require("./handlers");

var override = {

timeoutMessage: 'not mined within', // Substring of timeout err fired by web3
defaultMaxBlocks: 50, // Max # of blocks web3 will wait for a tx
const override = {
timeoutMessage: "not mined within", // Substring of timeout err fired by web3
defaultMaxBlocks: 50, // Max # of blocks web3 will wait for a tx
pollingInterval: 1000,

/**
* Attempts to extract receipt object from Web3 error message
* @param {Object} message web3 error
* @return {Object|undefined} receipt
*/
extractReceipt(message){
const hasReceipt = message &&
message.includes('{');
message.includes('}');
extractReceipt(message) {
const hasReceipt = message && message.includes("{");
message.includes("}");

if (hasReceipt){
const receiptString = '{' + message.split('{')[1].trim();
if (hasReceipt) {
const receiptString = "{" + message.split("{")[1].trim();
try {
return JSON.parse(receiptString);
} catch (err){
} catch (err) {
// ignore
}
}
},

getErrorReason: async function(params, web3) {
// This will run if there's a reason and no status field
// e.g: revert with reason ganache-cli --vmErrorsOnRPCResponse=true
const reason = await Reason.get(params, web3);
if (reason) return reason;
return null;
},

/**
* Fired after web3 ceases to support subscriptions if user has specified
* a higher block wait time than web3's 50 blocks limit. Opens a subscription to listen
Expand All @@ -36,30 +42,30 @@ var override = {
* @param {Object} context execution state
* @param {Object} err error
*/
start: async function(context, web3Error){
var constructor = this;
var blockNumber = null;
var currentBlock = override.defaultMaxBlocks;
var maxBlocks = constructor.timeoutBlocks;
start: async function(context, web3Error) {
const constructor = this;
let currentBlock = override.defaultMaxBlocks;
const maxBlocks = constructor.timeoutBlocks;

var timedOut = web3Error.message && web3Error.message.includes(override.timeoutMessage);
var shouldWait = maxBlocks > currentBlock;
const timedOut =
web3Error.message && web3Error.message.includes(override.timeoutMessage);
const shouldWait = maxBlocks > currentBlock;

// Reject after attempting to get reason string if we shouldn't be waiting.
if (!timedOut || !shouldWait){

if (!timedOut || !shouldWait) {
// We might have been routed here in web3 >= beta.34 by their own status check
// error. We want to extract the receipt, emit a receipt event
// and reject it ourselves.
var receipt = override.extractReceipt(web3Error.message);
if (receipt){
const receipt = override.extractReceipt(web3Error.message);
if (receipt) {
await handlers.receipt(context, receipt);
return;
}

// This will run if there's a reason and no status field
// e.g: revert with reason ganache-cli --vmErrorsOnRPCResponse=true
var reason = await Reason.get(context.params, constructor.web3);
const reason = await override.getErrorReason(
context.params,
constructor.web3
);
if (reason) {
web3Error.reason = reason;
web3Error.message += ` -- Reason given: ${reason}.`;
Expand All @@ -69,27 +75,25 @@ var override = {
}

// This will run every block from now until contract.timeoutBlocks
var listener = function(pollID){
var self = this;
const listener = function(pollID) {
currentBlock++;

if (currentBlock > constructor.timeoutBlocks){
if (currentBlock > constructor.timeoutBlocks) {
clearInterval(pollID);
return;
}

constructor.web3.eth.getTransactionReceipt(context.transactionHash)
constructor.web3.eth
.getTransactionReceipt(context.transactionHash)
.then(result => {
if (!result) return;

(result.contractAddress)
result.contractAddress
? constructor
.at(result.contractAddress)
.then(context.promiEvent.resolve)
.catch(context.promiEvent.reject)

: constructor.promiEvent.resolve(result);

})
.catch(err => {
clearInterval(pollID);
Expand All @@ -100,15 +104,15 @@ var override = {
// Start polling
let currentPollingBlock = await constructor.web3.eth.getBlockNumber();

const pollID = setInterval(async() => {
const pollID = setInterval(async () => {
const newBlock = await constructor.web3.eth.getBlockNumber();

if(newBlock > currentPollingBlock){
if (newBlock > currentPollingBlock) {
currentPollingBlock = newBlock;
listener(pollID);
}
}, override.pollingInterval);
},
}
};

module.exports = override;
35 changes: 35 additions & 0 deletions packages/truffle-contract/test/revertReasons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const { assert } = require("chai");
const util = require("./util");

describe("revert reasons", function() {
let RevertingContract;
const providerOptions = { vmErrorsOnRPCResponse: true }; // <--- TRUE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eggplantzzz If you set this option to false (e.g emulate production nodes) does the view test pass?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The view test fails when I set it to false. I think it is true by default as it passes when I don't give any options to util.setUpProvider...weird. Actually this got lifted from the errors test in truffle-contract. Maybe it should be removed.

Copy link
Contributor Author

@eggplantzzz eggplantzzz Aug 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the point here I suppose is that I wasn't aware that this was production node behavior, in which case this PR probably shouldn't go through. Although I can see value in it for testing purposes I suppose. I dunno, what do you think @gnidan? You can make the call.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the tests are crazy because there are two clients (geth and ganache) and then ganache can be run in these two modes. And responses vary across those. IIRC responses varied depending on whether ganache was run as a provider or a server when these tests were written, so it's a mess. There was also a lot of discussion about what ganache's default run mode should be . . .

I think the risk here is that TC makes a design pattern seem like it will work in the wild and people don't do due diligence re: their tests passing vs. a reference client.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that is my worry as well. It seems like it could be useful but it also doesn't seem good to create expectations about what will happen in prod.

Copy link
Contributor

@gnidan gnidan Aug 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gosh I forgot that the JSON RPC completely excludes any kind of status information at all for eth_call.

I concur with @cgewecke about this being a dangerous idea. I think we should hold off until we do something like run the call though ethereumjs-vm ourselves to get this data.

(cc @haltman-at to try to put the idea in your head)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh geez, I didn't realize this was an issue -- thanks for notifying me, because this actually potentially affects some stuff I've been thinking about.

But, I'm a little confused here. At least with Ganache, running normally, on a revert, the response -- whether to a call or a sendTransaction -- does not have the usual form specified by the RPC, but rather puts all its info in an error field, and if you dig into that you can find the revert message (both decoded by Ganache and in its raw binary form). In the case of a sendTransaction, this allows getting the return value (which normally isn't possible); and in the case of a call, this allows getting the status (which normally isn't possible).

Being largely unfamiliar with the other ethereum clients, I didn't realize that this behavior was not universal, or could be turned off in Ganache, forcing JSON-RPC-conforming (but less informative) responses even on a revert. Is that indeed the case? Am I understanding this correctly? @cgewecke, can you provide any clarity here? I would really like to know when I should expect a standard RPC response even on error, vs when I should expect this other error format. (Or are there other alternatives out there that I haven't even seen...?) Or at the least I would like to know for a certainty whether I do potentially have to handle both. (Not that I'm going to get to this stuff for quite a while, but...)

Anyway thank you! I'd been wondering about this question so I'm glad I was tagged here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@haltman-at The production clients do not attach an error field. That's a convenience ganache/testrpc added in order to be an effective chain simulator for tests in the days before transaction receipts included a status field.

Truffle has fixtures to run tests against an insta-mining geth client (@CruzMolina knows all about) and ganache has a flag you can enable to make it more geth-like as well. (See above)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, I totally didn't realize. So that's not a standard thing at all, huh. OK, thanks a bunch, I'll have to account for that!


before(async function() {
this.timeout(10000);

RevertingContract = await util.createRevertingContract();

await util.setUpProvider(RevertingContract, providerOptions);
});

it("provides a reason when a function reverts", async () => {
try {
let instance = await RevertingContract.new();
await instance.revertingFunction();
assert.fail();
} catch (error) {
assert(error.reason, "Too reverty of a function");
}
});

it("provides a reason when a view function (call) reverts", async () => {
try {
let instance = await RevertingContract.new();
await instance.revertingView();
assert.fail();
} catch (error) {
assert(error.reason, "Too reverty of a view");
}
});
});
17 changes: 17 additions & 0 deletions packages/truffle-contract/test/sources/RevertingContract.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
pragma solidity ^0.5.0;

contract RevertingContract {
uint256 public value;

constructor() public {
value = 0;
}

function revertingView() public view {
require(value > 0, "Too reverty of a view");
}

function revertingFunction() public {
require(value > 0, "Too reverty of a function");
}
}
7 changes: 7 additions & 0 deletions packages/truffle-contract/test/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ var util = {
);
},

createRevertingContract: async function() {
return await util._createContractInstance(
path.join(__dirname, "sources", "RevertingContract.sol"),
"RevertingContract"
);
},

_createContractInstance: async function(sourcePath, contractName) {
var contractObj;
const sources = {
Expand Down
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5775,7 +5775,7 @@ [email protected], ethjs-util@^0.1.3, ethjs-util@^0.1.6:
is-hex-prefixed "1.0.0"
strip-hex-prefix "1.0.0"

ethpm-registry@^0.1.0-next.3:
[email protected]:
version "0.1.0-next.3"
resolved "https://registry.yarnpkg.com/ethpm-registry/-/ethpm-registry-0.1.0-next.3.tgz#e335e7f57cda3c1cead388db6833903121773e9c"
integrity sha512-PZZ1wo7lf2J3xWfvdEHzG6gJzh2wJiw8jzBcLgqOicLQoxVzgzCCcF6GZKakIeDZfMilrP/0fm7uu5PviZzECg==
Expand Down