Skip to content

Instantly share code, notes, and snippets.

@Dave-Whiffin
Created January 22, 2020 11:52
Show Gist options
  • Save Dave-Whiffin/b26aa5d1c0df5e112cae1a9bd3523421 to your computer and use it in GitHub Desktop.
Save Dave-Whiffin/b26aa5d1c0df5e112cae1a9bd3523421 to your computer and use it in GitHub Desktop.
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