Created
January 22, 2020 11:52
-
-
Save Dave-Whiffin/b26aa5d1c0df5e112cae1a9bd3523421 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Nethereum.ABI.FunctionEncoding; | |
using Nethereum.ABI.FunctionEncoding.Attributes; | |
using Nethereum.Contracts; | |
using Nethereum.Contracts.ContractHandlers; | |
using Nethereum.JsonRpc.Client; | |
using Nethereum.RPC.Eth.DTOs; | |
using Nethereum.Web3; | |
using Nethereum.Web3.Accounts; | |
using System; | |
using System.Numerics; | |
using System.Threading; | |
using System.Threading.Tasks; | |
namespace RevertHandling | |
{ | |
class Program | |
{ | |
static async Task Main(string[] args) | |
{ | |
// The purpose of this sample code is to demonstrate how to retrieve revert messages | |
// This can be vital when trying to debug a smart contract, particularly in early stage development | |
// It may also be useful for logging errors at runtime | |
// But obviously it's best to avoid triggering reverted transactions if you can! | |
// CAVEAT: at the time of writing (January 2020) only Geth returns these revert messages, Parity does not. | |
// These samples depend on the Solidity code and generated .Net code at the bottom of this file | |
// It's a simple smart contract with one function called "Execute" | |
// We're using it as a test harness to simulate revert behaviour | |
// The function accepts a flag which dictates whether a revert is triggered | |
var url = "http://testchain.nethereum.com:8545"; | |
var privateKey = "0x7580e7fb49df1c861f0050fae31c2224c6aba908e116b8da44ee8cd927b990b0"; | |
var account = new Account(privateKey); | |
var web3 = new Web3(account, url); | |
string contractAddress = await DeployAsync(web3); | |
// 1. Demonstrate creating a transaction successfuly - no revert, no error handling | |
await HappyPathAsync(web3, contractAddress); | |
// 2. Demonstrate a transaction attempt which will revert and retrieving the revert message | |
// this is useful when you're not anticipating errors but you need to handle them if they occur | |
// this example uses auto gas estimation (the default in Nethereum) | |
// this changes the way the revert message is retrieved as it happens during estimation | |
await RetrievingRevertMessageFromSendAttempt(web3, contractAddress); | |
// 3. As above - but specifying gas | |
// specifying gas prevents estimation | |
// this means the revert error happens during Send | |
// and results in writing a failed transaction to the chain | |
// the revert message can then be retrieved via the transaction hash | |
await RetrievingRevertMessageFromTransactionHash(web3, contractAddress); | |
// 4. as above but using a code generated service for the contract to send the transaction | |
await RetrievingRevertMessageFromTransactionHash_UsingAService(web3, contractAddress); | |
// 5. Demonstrate a pre check | |
// i.e. sending the same function args as a "Query" before invoking "Send" | |
// if errors occur during Query then either the function args or contract state may be invalid | |
// this provides the option of not invoking "Send" and saving gas which would be wasted on a failed transaction | |
await SendWithPreCheck_Simulate_Success(web3, contractAddress); | |
await SendWithPreCheck_Simulate_Revert_Error(web3, contractAddress); | |
} | |
private static async Task HappyPathAsync(Web3 web3, string contractAddress) | |
{ | |
Console.WriteLine("====================================================================================="); | |
Console.WriteLine($"{nameof(HappyPathAsync)} - check state, execute, check new state"); | |
// the contract maintains a count of successful executions as a public variable | |
// we expect this to go up after a successful transaction | |
// get current count - so we can compare after our new transaction | |
Console.WriteLine("* Checking execution count from contract state"); | |
var currentExecutionCount = await web3.Eth.GetContractQueryHandler<ExecutionCountFunction>().QueryAsync<long>(contractAddress); | |
Console.WriteLine($" [Execution Count (before)]: {currentExecutionCount}"); | |
Console.WriteLine("* Execute - with function args which will not trigger a revert"); | |
var args = new ExecuteFunction { Revert = false, RevertMessage = string.Empty }; | |
var executeFunctionHandler = web3.Eth.GetContractTransactionHandler<ExecuteFunction>(); | |
var receipt = await executeFunctionHandler.SendRequestAndWaitForReceiptAsync(contractAddress, args); | |
Console.WriteLine($" [Tx Created] Hash: {receipt.TransactionHash}, Success : {receipt.Succeeded()}"); | |
// retrieve state again - it should have changed | |
Console.WriteLine("* Checking execution count from contract state"); | |
var newExecutionCount = await web3.Eth.GetContractQueryHandler<ExecutionCountFunction>().QueryAsync<long>(contractAddress); | |
Console.WriteLine($" [Execution Count (after)]: {newExecutionCount}"); | |
} | |
private static async Task RetrievingRevertMessageFromSendAttempt(Web3 web3, string contractAddress) | |
{ | |
Console.WriteLine("====================================================================================="); | |
Console.WriteLine($"{nameof(RetrievingRevertMessageFromSendAttempt)} - send, catch exception and retrieve revert message"); | |
// set up the args for the function so that a revert is triggered | |
// we're not setting any gas | |
// this will cause Nethereum to Estimate gas | |
// Estimating runs the smart contract code with the same parameters | |
// Logic in the smart contract code may trigger a revert | |
// Any errors during estimation will prevent a transaction from being created | |
var args = new ExecuteFunction { Revert = true, RevertMessage = "Fake Exception" }; | |
string actualRevertMessage = null; | |
try | |
{ | |
// pretend we don't know this will fail, send the transaction | |
Console.WriteLine("* Execute - with args that will trigger a revert"); | |
var executeFunctionHandler = web3.Eth.GetContractTransactionHandler<ExecuteFunction>(); | |
await executeFunctionHandler.SendRequestAndWaitForReceiptAsync(contractAddress, args); | |
} | |
// conditional catch | |
catch (RpcResponseException rpcResponseException) | |
when (rpcResponseException.RpcError.Message.StartsWith("gas required exceeds allowance")) | |
{ | |
// a tx hash not been created because the error happened during estimation | |
Console.WriteLine($" [RpcResponseException]. Message: {rpcResponseException.Message}, Code:{rpcResponseException.RpcError.Code}"); | |
// let's attempt to get the revert reason by querying with the same args | |
var revertException = await TryGetRevertMessage(web3, contractAddress, args); | |
if (revertException != null) | |
{ | |
actualRevertMessage = revertException.Message; | |
} | |
} | |
Console.WriteLine($" [Expected Revert Reason]: {args.RevertMessage}"); | |
Console.WriteLine($" [Actual Revert Reason]: {actualRevertMessage}"); | |
} | |
private static async Task RetrievingRevertMessageFromSendAttempt_UsingAService(Web3 web3, string contractAddress) | |
{ | |
Console.WriteLine("====================================================================================="); | |
Console.WriteLine($"{nameof(RetrievingRevertMessageFromTransactionHash_UsingAService)} - send, catch exception and retrieve revert message"); | |
// the service is a wrapper for all of the functions, events and variables on a contract | |
var service = new RevertSampleService(web3, contractAddress); | |
// set up the args for the function so that a revert is triggered | |
// we're not setting any gas | |
// this will cause Nethereum to Estimate gas | |
// Estimating runs the smart contract code with the same parameters | |
// Logic in the smart contract code may trigger a revert | |
// Any errors during estimation will prevent a transaction from being created | |
var args = new ExecuteFunction { Revert = true, RevertMessage = "Fake Exception" }; | |
string actualRevertMessage = null; | |
try | |
{ | |
// pretend we don't know this will fail, send the transaction | |
Console.WriteLine("* Execute - with args that will trigger a revert"); | |
await service.ExecuteRequestAndWaitForReceiptAsync(args); | |
} | |
// conditional catch | |
catch (RpcResponseException rpcResponseException) | |
when (rpcResponseException.RpcError.Message.StartsWith("gas required exceeds allowance")) | |
{ | |
// a tx hash not been created because the error happened during estimation | |
Console.WriteLine($" [RpcResponseException]. Message: {rpcResponseException.Message}, Code:{rpcResponseException.RpcError.Code}"); | |
// let's attempt to get the revert reason by replaying with the same args | |
// this doesn't use the service, as the service doesn't provide a direct means of querying a function | |
var revertException = await TryGetRevertMessage(web3, contractAddress, args); | |
if (revertException != null) | |
{ | |
actualRevertMessage = revertException.Message; | |
} | |
} | |
Console.WriteLine($" [Expected Revert Reason]: {args.RevertMessage}"); | |
Console.WriteLine($" [Actual Revert Reason]: {actualRevertMessage}"); | |
} | |
private static async Task RetrievingRevertMessageFromTransactionHash(Web3 web3, string contractAddress) | |
{ | |
Console.WriteLine("====================================================================================="); | |
Console.WriteLine($"{nameof(RetrievingRevertMessageFromTransactionHash)} - send with defined gas creating failed tx, retrieve revert message"); | |
// set up the args for the function so that a revert is triggered | |
// setting the gas means that nethereum will not auto estimate the gas | |
// estimating the gas executes the smart contract code without a transaction | |
// if the smart contract code reverts during estimation, this will prevent a transaction from being sent | |
// in this example we want a transaction to be put on chain - but for it to fail | |
// then we can do a call to retrieve the reason via the transaction hash | |
var args = new ExecuteFunction { Gas = 42574, GasPrice = 1000000000, Revert = true, RevertMessage = "Fake Exception" }; | |
// pretend we don't know this will fail, send the transaction | |
// we're not auto estimating, so we should be given a receipt with a tx hash | |
Console.WriteLine("* Execute - with args that will trigger a reverted transaction"); | |
var executeFunctionHandler = web3.Eth.GetContractTransactionHandler<ExecuteFunction>(); | |
var receipt = await executeFunctionHandler.SendRequestAndWaitForReceiptAsync(contractAddress, args); | |
Console.WriteLine($" [Transaction Receipt Received]. Tx Hash: {receipt.TransactionHash}, Status: {receipt.Status.Value}, Succeeded: {receipt.Succeeded()}"); | |
Console.WriteLine("* Retrieving Revert Message"); | |
var revertMessage = await web3.Eth.GetContractTransactionErrorReason.SendRequestAsync(receipt.TransactionHash); | |
Console.WriteLine($" [Expected Revert Message]: {args.RevertMessage}"); | |
Console.WriteLine($" [Actual Revert Message]: {revertMessage}"); | |
} | |
private static async Task RetrievingRevertMessageFromTransactionHash_UsingAService(Web3 web3, string contractAddress) | |
{ | |
Console.WriteLine("====================================================================================="); | |
Console.WriteLine($"{nameof(RetrievingRevertMessageFromTransactionHash_UsingAService)} - send with defined gas creating failed tx, retrieve revert message"); | |
// the service is a wrapper for all the properties and functions on a contract | |
var service = new RevertSampleService(web3, contractAddress); | |
// set up the args for the function so that a revert is triggered | |
// setting the gas means that nethereum will not auto estimate the gas | |
// estimating the gas executes the smart contract code without a transaction | |
// if the smart contract code reverts during estimation, this will prevent a transaction from being sent | |
// in this example we want a transaction to be put on chain - but for it to fail | |
// then we can do a call to retrieve the reason via the transaction hash | |
var args = new ExecuteFunction { Gas = 42574, GasPrice = 1000000000, Revert = true, RevertMessage = "Fake Exception" }; | |
// pretend we don't know this will fail, send the transaction | |
// we're not auto estimating, so we should be given a receipt with a tx hash | |
Console.WriteLine("* Execute - with args that will trigger a reverted transaction"); | |
var receipt = await service.ExecuteRequestAndWaitForReceiptAsync(args); | |
Console.WriteLine($" [Transaction Receipt Received]. Tx Hash: {receipt.TransactionHash}, Status: {receipt.Status.Value}, Succeeded: {receipt.Succeeded()}"); | |
Console.WriteLine("* Retrieving Revert Message"); | |
var revertMessage = await service | |
.ContractHandler | |
.EthApiContractService | |
.GetContractTransactionErrorReason.SendRequestAsync(receipt.TransactionHash); | |
// it is not necessary to use the service to get the revert reason | |
// we can use the web3 instance directly as an alternative | |
// var revertMessage = web3.Eth.GetContractTransactionErrorReason.SendRequestAsync(receipt.TransactionHash); | |
Console.WriteLine($" [Expected Revert Message]: {args.RevertMessage}"); | |
Console.WriteLine($" [Actual Revert Message]: {revertMessage}"); | |
} | |
private static async Task SendWithPreCheck_Simulate_Success(Web3 web3, string contractAddress) | |
{ | |
Console.WriteLine("====================================================================================="); | |
Console.WriteLine($"{nameof(SendWithPreCheck_Simulate_Success)} - test the function without creating a transaction - simulate success"); | |
// we'll set this up so there is no revert | |
var functionArgs = new ExecuteFunction { Revert = false, RevertMessage = string.Empty }; | |
var receipt = await SendRequestWithPreCheckAndWaitForReceiptAsync(web3, contractAddress, functionArgs); | |
Console.WriteLine($" [Success]: Tx Hash: {receipt.TransactionHash}"); | |
} | |
private static async Task SendWithPreCheck_Simulate_Revert_Error(Web3 web3, string contractAddress) | |
{ | |
Console.WriteLine("====================================================================================="); | |
Console.WriteLine($"{nameof(SendWithPreCheck_Simulate_Revert_Error)} - test the function without creating a transaction - simulate revert"); | |
// set up the args so that a revert is triggered | |
var functionArgs = new ExecuteFunction { Revert = true, RevertMessage = "Fake Error" }; | |
string actualRevertMessage = null; | |
try | |
{ | |
await SendRequestWithPreCheckAndWaitForReceiptAsync(web3, contractAddress, functionArgs); | |
} | |
catch (SmartContractRevertException revertException) | |
{ | |
actualRevertMessage = revertException.Message; | |
} | |
Console.WriteLine($" [Expected Revert Exception]: {functionArgs.RevertMessage}"); | |
Console.WriteLine($" [Actual Revert Exception]: {actualRevertMessage}"); | |
} | |
private static async Task<TransactionReceipt> | |
SendRequestWithPreCheckAndWaitForReceiptAsync<TFunctionMessage>(Web3 web3, string contractAddress, TFunctionMessage functionMessage) | |
where TFunctionMessage : FunctionMessage, new() | |
{ | |
Console.WriteLine("* Querying the function as a pre check for revert exceptions"); | |
var queryHandler = web3.Eth.GetContractQueryHandler<TFunctionMessage>(); | |
// we don't care about the return value (if there is one) | |
await queryHandler.QueryAsync<bool>(contractAddress, functionMessage); | |
Console.WriteLine(" [Query did not throw an error]"); | |
Console.WriteLine(" Therefore we assume the function args and current contract state are ok"); | |
Console.WriteLine(" (CAVEAT: contract state could still change before we attempt the transaction)"); | |
Console.WriteLine("* Sending the transaction"); | |
var sendHandler = web3.Eth.GetContractTransactionHandler<TFunctionMessage>(); | |
return await sendHandler.SendRequestAndWaitForReceiptAsync(contractAddress, functionMessage); | |
} | |
private static async Task<string> DeployAsync(Web3 web3) | |
{ | |
Console.WriteLine("====================================================================================="); | |
Console.WriteLine("Deploy the test contract"); | |
var deploymentHandler = web3.Eth.GetContractDeploymentHandler<RevertSampleDeployment>(); | |
var receipt = await deploymentHandler.SendRequestAndWaitForReceiptAsync(); | |
Console.WriteLine($" [Deployed] Contract Address: {receipt.ContractAddress}, Tx Hash: {receipt.TransactionHash}"); | |
return receipt.ContractAddress; | |
} | |
private static async Task<SmartContractRevertException> TryGetRevertMessage<TFunction>( | |
Web3 web3, string contractAddress, TFunction functionArgs, BlockParameter blockParameter = null) | |
where TFunction : FunctionMessage, new() | |
{ | |
try | |
{ | |
Console.WriteLine($"* Querying Function {typeof(TFunction).Name}"); | |
// instead of sending a transaction again, we do a query with the same function parameters | |
// the smart contract code will be executed but no changes will be made on chain | |
var functionHandler = web3.Eth.GetContractQueryHandler<TFunction>(); | |
// we're not bothered about the return value here | |
// we'd only get that if it was successful | |
// we only want the revert reason which we'll get from the exception | |
// we cant use QueryRaw as that will never throw a SmartContractRevertException | |
await functionHandler.QueryAsync<bool>(contractAddress, functionArgs, blockParameter); | |
// if we got here there was no revert message to retrieve | |
return null; | |
} | |
catch(SmartContractRevertException revertException) | |
{ | |
return revertException; | |
} | |
} | |
// The Solidity code for our sample smart contract | |
// It has one function and the caller defines if a revert exception is to be thrown | |
// It has one state variable so we can track the number of successful (non reverted) calls | |
/* | |
pragma solidity ^0.5.0; | |
contract RevertSample { | |
uint public executionCount; | |
function execute(bool _revert, string memory _revertMessage) public payable { | |
require(!_revert, _revertMessage); | |
executionCount ++; | |
} | |
} | |
*/ | |
// GENERATED CODE STARTS HERE | |
// C# Code created from the contract abi | |
// http://playground.nethereum.com/ | |
// See the Abi Auto Gen Option | |
// abi | |
// [{"constant":false,"inputs":[{"internalType":"bool","name":"_revert","type":"bool"},{"internalType":"string","name":"_revertMessage","type":"string"}],"name":"execute","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[],"name":"executionCount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}] | |
// bytecode | |
// 608060405234801561001057600080fd5b506101da806100206000396000f3fe6080604052600436106100295760003560e01c80638c270ed91461002e578063a17ecef3146100df575b600080fd5b6100dd6004803603604081101561004457600080fd5b81351515919081019060408101602082013564010000000081111561006857600080fd5b82018360208201111561007a57600080fd5b8035906020019184600183028401116401000000008311171561009c57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550610106945050505050565b005b3480156100eb57600080fd5b506100f461019f565b60408051918252519081900360200190f35b8082156101915760405162461bcd60e51b81526004018080602001828103825283818151815260200191508051906020019080838360005b8381101561015657818101518382015260200161013e565b50505050905090810190601f1680156101835780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b505060008054600101905550565b6000548156fea265627a7a72315820d801f8774c2221639ba87dc3b96b4a264cc67a4d4d8d4054a0fb4efcf290b75664736f6c634300050b0032 | |
public partial class RevertSampleDeployment : RevertSampleDeploymentBase | |
{ | |
public RevertSampleDeployment() : base(BYTECODE) { } | |
public RevertSampleDeployment(string byteCode) : base(byteCode) { } | |
} | |
public class RevertSampleDeploymentBase : ContractDeploymentMessage | |
{ | |
public static string BYTECODE = "608060405234801561001057600080fd5b506101da806100206000396000f3fe6080604052600436106100295760003560e01c80638c270ed91461002e578063a17ecef3146100df575b600080fd5b6100dd6004803603604081101561004457600080fd5b81351515919081019060408101602082013564010000000081111561006857600080fd5b82018360208201111561007a57600080fd5b8035906020019184600183028401116401000000008311171561009c57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600092019190915250929550610106945050505050565b005b3480156100eb57600080fd5b506100f461019f565b60408051918252519081900360200190f35b8082156101915760405162461bcd60e51b81526004018080602001828103825283818151815260200191508051906020019080838360005b8381101561015657818101518382015260200161013e565b50505050905090810190601f1680156101835780820380516001836020036101000a031916815260200191505b509250505060405180910390fd5b505060008054600101905550565b6000548156fea265627a7a72315820d801f8774c2221639ba87dc3b96b4a264cc67a4d4d8d4054a0fb4efcf290b75664736f6c634300050b0032"; | |
public RevertSampleDeploymentBase() : base(BYTECODE) { } | |
public RevertSampleDeploymentBase(string byteCode) : base(byteCode) { } | |
} | |
public partial class ExecuteFunction : ExecuteFunctionBase { } | |
[Function("execute")] | |
public class ExecuteFunctionBase : FunctionMessage | |
{ | |
[Parameter("bool", "_revert", 1)] | |
public virtual bool Revert { get; set; } | |
[Parameter("string", "_revertMessage", 2)] | |
public virtual string RevertMessage { get; set; } | |
} | |
public partial class ExecutionCountFunction : ExecutionCountFunctionBase { } | |
[Function("executionCount", "uint256")] | |
public class ExecutionCountFunctionBase : FunctionMessage | |
{ | |
} | |
public partial class ExecutionCountOutputDTO : ExecutionCountOutputDTOBase { } | |
[FunctionOutput] | |
public class ExecutionCountOutputDTOBase : IFunctionOutputDTO | |
{ | |
[Parameter("uint256", "", 1)] | |
public virtual BigInteger ReturnValue1 { get; set; } | |
} | |
public partial class RevertSampleService | |
{ | |
public static Task<TransactionReceipt> DeployContractAndWaitForReceiptAsync(Nethereum.Web3.Web3 web3, RevertSampleDeployment revertSampleDeployment, CancellationTokenSource cancellationTokenSource = null) | |
{ | |
return web3.Eth.GetContractDeploymentHandler<RevertSampleDeployment>().SendRequestAndWaitForReceiptAsync(revertSampleDeployment, cancellationTokenSource); | |
} | |
public static Task<string> DeployContractAsync(Nethereum.Web3.Web3 web3, RevertSampleDeployment revertSampleDeployment) | |
{ | |
return web3.Eth.GetContractDeploymentHandler<RevertSampleDeployment>().SendRequestAsync(revertSampleDeployment); | |
} | |
public static async Task<RevertSampleService> DeployContractAndGetServiceAsync(Nethereum.Web3.Web3 web3, RevertSampleDeployment revertSampleDeployment, CancellationTokenSource cancellationTokenSource = null) | |
{ | |
var receipt = await DeployContractAndWaitForReceiptAsync(web3, revertSampleDeployment, cancellationTokenSource); | |
return new RevertSampleService(web3, receipt.ContractAddress); | |
} | |
protected Nethereum.Web3.Web3 Web3 { get; } | |
public ContractHandler ContractHandler { get; } | |
public RevertSampleService(Nethereum.Web3.Web3 web3, string contractAddress) | |
{ | |
Web3 = web3; | |
ContractHandler = web3.Eth.GetContractHandler(contractAddress); | |
} | |
public Task<string> ExecuteRequestAsync(ExecuteFunction executeFunction) | |
{ | |
return ContractHandler.SendRequestAsync(executeFunction); | |
} | |
public Task<TransactionReceipt> ExecuteRequestAndWaitForReceiptAsync(ExecuteFunction executeFunction, CancellationTokenSource cancellationToken = null) | |
{ | |
return ContractHandler.SendRequestAndWaitForReceiptAsync(executeFunction, cancellationToken); | |
} | |
public Task<string> ExecuteRequestAsync(bool revert, string revertMessage) | |
{ | |
var executeFunction = new ExecuteFunction(); | |
executeFunction.Revert = revert; | |
executeFunction.RevertMessage = revertMessage; | |
return ContractHandler.SendRequestAsync(executeFunction); | |
} | |
public Task<TransactionReceipt> ExecuteRequestAndWaitForReceiptAsync(bool revert, string revertMessage, CancellationTokenSource cancellationToken = null) | |
{ | |
var executeFunction = new ExecuteFunction(); | |
executeFunction.Revert = revert; | |
executeFunction.RevertMessage = revertMessage; | |
return ContractHandler.SendRequestAndWaitForReceiptAsync(executeFunction, cancellationToken); | |
} | |
public Task<BigInteger> ExecutionCountQueryAsync(ExecutionCountFunction executionCountFunction, BlockParameter blockParameter = null) | |
{ | |
return ContractHandler.QueryAsync<ExecutionCountFunction, BigInteger>(executionCountFunction, blockParameter); | |
} | |
public Task<BigInteger> ExecutionCountQueryAsync(BlockParameter blockParameter = null) | |
{ | |
return ContractHandler.QueryAsync<ExecutionCountFunction, BigInteger>(null, blockParameter); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment