Skip to content

Commit

Permalink
Lower Level Transaction Builder
Browse files Browse the repository at this point in the history
  • Loading branch information
0xFirekeeper committed Apr 12, 2024
1 parent 929228f commit 63278fa
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 92 deletions.
4 changes: 2 additions & 2 deletions Thirdweb.Tests/Thirdweb.Contracts.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ public async Task WriteTest_SmartAccount()
var data = new byte[] { };
var result = await ThirdwebContract.Write(smartAccount, contract, "claim", 0, receiver, quantity, currency, pricePerToken, allowlistProof, data);
Assert.NotNull(result);
var receipt = await Utils.GetTransactionReceipt(contract.Client, contract.Chain, result);
var receipt = await ThirdwebTransaction.WaitForTransactionReceipt(contract.Client, contract.Chain, result.TransactionHash);
Assert.NotNull(receipt);
Assert.Equal(result, receipt.TransactionHash);
Assert.Equal(result.TransactionHash, receipt.TransactionHash);
}

[Fact]
Expand Down
24 changes: 12 additions & 12 deletions Thirdweb.Tests/Thirdweb.Utils.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@ public async Task GetTransactionReceipt()
var aaTxHash = "0xbf76bd85e1759cf5cf9f4c7c52e76a74d32687f0b516017ff28192d04df50782";
var aaSilentRevertTxHash = "0x8ada86c63846da7a3f91b8c8332de03f134e7619886425df858ee5400a9d9958";

var normalReceipt = await Utils.GetTransactionReceipt(client, chainId, normalTxHash);
var normalReceipt = await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, normalTxHash);
Assert.NotNull(normalReceipt);

var failedReceipt = await Assert.ThrowsAsync<Exception>(async () => await Utils.GetTransactionReceipt(client, chainId, failedTxHash));
var failedReceipt = await Assert.ThrowsAsync<Exception>(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, failedTxHash));
Assert.Equal($"Transaction {failedTxHash} execution reverted.", failedReceipt.Message);

var aaReceipt = await Utils.GetTransactionReceipt(client, chainId, aaTxHash);
var aaReceipt = await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaTxHash);
Assert.NotNull(aaReceipt);

var aaFailedReceipt = await Assert.ThrowsAsync<Exception>(async () => await Utils.GetTransactionReceipt(client, chainId, aaSilentRevertTxHash));
var aaFailedReceipt = await Assert.ThrowsAsync<Exception>(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaSilentRevertTxHash));
Assert.StartsWith($"Transaction {aaSilentRevertTxHash} execution silently reverted", aaFailedReceipt.Message);
}

Expand All @@ -47,7 +47,7 @@ public async Task GetTransactionReceipt_AAReasonString()
var client = ThirdwebClient.Create(secretKey: _secretKey);
var chainId = 84532;
var aaSilentRevertTxHashWithReason = "0x5374743bbb749df47a279ac21e6ed472c30cd471923a7bc78db6a40e1b6924de";
var aaFailedReceiptWithReason = await Assert.ThrowsAsync<Exception>(async () => await Utils.GetTransactionReceipt(client, chainId, aaSilentRevertTxHashWithReason));
var aaFailedReceiptWithReason = await Assert.ThrowsAsync<Exception>(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaSilentRevertTxHashWithReason));
Assert.StartsWith($"Transaction {aaSilentRevertTxHashWithReason} execution silently reverted:", aaFailedReceiptWithReason.Message);
}

Expand All @@ -63,37 +63,37 @@ public async Task GetTransactionReceipt_CancellationToken()

var cts = new CancellationTokenSource();
cts.CancelAfter(10000);
var normalReceipt = await Utils.GetTransactionReceipt(client, chainId, normalTxHash, cts.Token);
var normalReceipt = await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, normalTxHash, cts.Token);
Assert.NotNull(normalReceipt);

cts = new CancellationTokenSource();
cts.CancelAfter(10000);
var failedReceipt = await Assert.ThrowsAsync<Exception>(async () => await Utils.GetTransactionReceipt(client, chainId, failedTxHash, cts.Token));
var failedReceipt = await Assert.ThrowsAsync<Exception>(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, failedTxHash, cts.Token));
Assert.Equal($"Transaction {failedTxHash} execution reverted.", failedReceipt.Message);

cts = new CancellationTokenSource();
cts.CancelAfter(10000);
var aaReceipt = await Utils.GetTransactionReceipt(client, chainId, aaTxHash, cts.Token);
var aaReceipt = await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaTxHash, cts.Token);
Assert.NotNull(aaReceipt);

cts = new CancellationTokenSource();
cts.CancelAfter(10000);
var aaFailedReceipt = await Assert.ThrowsAsync<Exception>(async () => await Utils.GetTransactionReceipt(client, chainId, aaSilentRevertTxHash, cts.Token));
var aaFailedReceipt = await Assert.ThrowsAsync<Exception>(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaSilentRevertTxHash, cts.Token));
Assert.StartsWith($"Transaction {aaSilentRevertTxHash} execution silently reverted", aaFailedReceipt.Message);

var infiniteTxHash = "0x55181384a4b908ddf6311cf0eb55ea0aa2b1ef4d9e0cc047eab9051fec284958";
cts = new CancellationTokenSource();
cts.CancelAfter(1);
var infiniteReceipt = await Assert.ThrowsAsync<TaskCanceledException>(async () => await Utils.GetTransactionReceipt(client, chainId, infiniteTxHash, cts.Token));
var infiniteReceipt = await Assert.ThrowsAsync<TaskCanceledException>(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, infiniteTxHash, cts.Token));
Assert.Equal("A task was canceled.", infiniteReceipt.Message);

cts = new CancellationTokenSource();
var infiniteReceipt2 = Assert.ThrowsAsync<TaskCanceledException>(() => Utils.GetTransactionReceipt(client, chainId, infiniteTxHash, cts.Token));
var infiniteReceipt2 = Assert.ThrowsAsync<TaskCanceledException>(() => ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, infiniteTxHash, cts.Token));
await Task.Delay(2000);
cts.Cancel();
Assert.Equal("A task was canceled.", (await infiniteReceipt2).Message);

var aaReceipt2 = await Utils.GetTransactionReceipt(client, chainId, aaTxHash, CancellationToken.None);
var aaReceipt2 = await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaTxHash, CancellationToken.None);
Assert.NotNull(aaReceipt2);
}

Expand Down
31 changes: 4 additions & 27 deletions Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,44 +63,21 @@ public static async Task<T> Read<T>(ThirdwebContract contract, string method, pa
return function.DecodeTypeOutput<T>(resultData);
}

public static async Task<string> Write(IThirdwebWallet wallet, ThirdwebContract contract, string method, BigInteger weiValue, params object[] parameters)
public static async Task<TransactionReceipt> Write(IThirdwebWallet wallet, ThirdwebContract contract, string method, BigInteger weiValue, params object[] parameters)
{
var rpc = ThirdwebRPC.GetRpcInstance(contract.Client, contract.Chain);

var service = new Nethereum.Contracts.Contract(null, contract.Abi, contract.Address);
var function = service.GetFunction(method);
var data = function.GetData(parameters);

var transaction = new TransactionInput
{
From = await wallet.GetAddress(),
To = contract.Address,
Data = data,
Value = new HexBigInteger(weiValue),
};

// TODO: Implement 1559
transaction.Gas = new HexBigInteger(await rpc.SendRequestAsync<string>("eth_estimateGas", transaction));
transaction.GasPrice = new HexBigInteger(await rpc.SendRequestAsync<string>("eth_gasPrice"));
transaction.GasPrice = new HexBigInteger(transaction.GasPrice.Value * 10 / 9);
transaction.Value = new HexBigInteger(weiValue);

string hash;
switch (wallet.AccountType)
{
case ThirdwebAccountType.PrivateKeyAccount:
transaction.Nonce = new HexBigInteger(await rpc.SendRequestAsync<string>("eth_getTransactionCount", await wallet.GetAddress(), "latest"));
var signedTx = await wallet.SignTransaction(transaction, contract.Chain);
hash = await rpc.SendRequestAsync<string>("eth_sendRawTransaction", signedTx);
break;
case ThirdwebAccountType.SmartAccount:
var smartAccount = wallet as SmartWallet;
hash = await smartAccount.SendTransaction(transaction);
break;
default:
throw new NotImplementedException("Account type not supported");
}
Console.WriteLine($"Transaction hash: {hash}");
return hash;
var thirdwebTx = await ThirdwebTransaction.Create(contract.Client, wallet, transaction, contract.Chain);
return await ThirdwebTransaction.SendAndWaitForTransactionReceipt(thirdwebTx);
}
}
}
196 changes: 196 additions & 0 deletions Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
using System.Numerics;
using Nethereum.Hex.HexTypes;
using Nethereum.RPC.Eth.DTOs;
using Nethereum.RPC.Eth.Transactions;
using Newtonsoft.Json;
using Nethereum.Contracts;
using Nethereum.ABI.FunctionEncoding;
using Nethereum.Hex.HexConvertors.Extensions;

namespace Thirdweb
{
public struct GasCosts
{
public string ether;
public BigInteger wei;
}

public class ThirdwebTransaction
{
public TransactionInput Input { get; private set; }

private readonly ThirdwebClient _client;
private readonly IThirdwebWallet _wallet;

private ThirdwebTransaction(ThirdwebClient client, IThirdwebWallet wallet, TransactionInput txInput, BigInteger chainId)
{
Input = txInput;
_client = client;
_wallet = wallet;
Input.ChainId = chainId.ToHexBigInteger();
}

public static async Task<ThirdwebTransaction> Create(ThirdwebClient client, IThirdwebWallet wallet, TransactionInput txInput, BigInteger chainId)
{
return await wallet.GetAddress() != txInput.From
? throw new ArgumentException("Transaction sender (from) must match wallet address")
: client == null
? throw new ArgumentNullException(nameof(client))
: wallet == null
? throw new ArgumentNullException(nameof(wallet))
: chainId == 0
? throw new ArgumentException("Invalid Chain ID")
: new ThirdwebTransaction(client, wallet, txInput, chainId);
}

public override string ToString()
{
return JsonConvert.SerializeObject(Input);
}

public ThirdwebTransaction SetTo(string to)
{
Input.To = to;
return this;
}

public ThirdwebTransaction SetData(string data)
{
Input.Data = data;
return this;
}

public ThirdwebTransaction SetValue(BigInteger weiValue)
{
Input.Value = weiValue.ToHexBigInteger();
return this;
}

public ThirdwebTransaction SetGasLimit(BigInteger gas)
{
Input.Gas = gas.ToHexBigInteger();
return this;
}

public ThirdwebTransaction SetGasPrice(BigInteger gasPrice)
{
Input.GasPrice = gasPrice.ToHexBigInteger();
return this;
}

public ThirdwebTransaction SetNonce(BigInteger nonce)
{
Input.Nonce = nonce.ToHexBigInteger();
return this;
}

public static async Task<GasCosts> EstimateTotalCosts(ThirdwebTransaction transaction)
{
var gasLimit = await EstimateGasLimit(transaction);
var gasPrice = await EstimateGasPrice(transaction._client, transaction.Input.ChainId.Value);
var gasCost = BigInteger.Multiply(gasLimit, gasPrice);
var gasCostWithValue = BigInteger.Add(gasCost, transaction.Input.Value?.Value ?? 0);
return new GasCosts { ether = gasCostWithValue.ToString().ToEth(18, false), wei = gasCostWithValue };
}

public static async Task<BigInteger> EstimateGasPrice(ThirdwebClient client, BigInteger chainId, bool withBump = true)
{
{
var rpc = ThirdwebRPC.GetRpcInstance(client, chainId);
var hex = new HexBigInteger(await rpc.SendRequestAsync<string>("eth_gasPrice"));
return withBump ? hex.Value * 10 / 9 : hex.Value;
}
}

public static async Task<BigInteger> EstimateGasLimit(ThirdwebTransaction transaction)
{
var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value);
var hex = await rpc.SendRequestAsync<string>("eth_estimateGas", transaction.Input);
return new HexBigInteger(hex).Value;
}

public static async Task<string> Send(ThirdwebTransaction transaction)
{
if (transaction.Input.To == null)
{
throw new ArgumentException("To address must be provided");
}

transaction.Input.From ??= await transaction._wallet.GetAddress();
transaction.Input.Gas ??= new HexBigInteger(await EstimateGasLimit(transaction));
transaction.Input.Value ??= new HexBigInteger(0);
transaction.Input.Data ??= "0x";
transaction.Input.GasPrice ??= new HexBigInteger(await EstimateGasPrice(transaction._client, transaction.Input.ChainId.Value));
transaction.Input.MaxFeePerGas = null;
transaction.Input.MaxPriorityFeePerGas = null;

var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value);
string hash;
switch (transaction._wallet.AccountType)
{
case ThirdwebAccountType.PrivateKeyAccount:
transaction.Input.Nonce ??= new HexBigInteger(await rpc.SendRequestAsync<string>("eth_getTransactionCount", await transaction._wallet.GetAddress(), "latest"));
var signedTx = await transaction._wallet.SignTransaction(transaction.Input, transaction.Input.ChainId.Value);
hash = await rpc.SendRequestAsync<string>("eth_sendRawTransaction", signedTx);
break;
case ThirdwebAccountType.SmartAccount:
var smartAccount = transaction._wallet as SmartWallet;
hash = await smartAccount.SendTransaction(transaction.Input);
break;
default:
throw new NotImplementedException("Account type not supported");
}
Console.WriteLine($"Transaction hash: {hash}");
return hash;
}

public static async Task<TransactionReceipt> SendAndWaitForTransactionReceipt(ThirdwebTransaction transaction)
{
var txHash = await Send(transaction);
return await WaitForTransactionReceipt(transaction._client, transaction.Input.ChainId.Value, txHash);
}

public static async Task<TransactionReceipt> WaitForTransactionReceipt(ThirdwebClient client, BigInteger chainId, string txHash, CancellationToken cancellationToken = default)
{
var rpc = ThirdwebRPC.GetRpcInstance(client, chainId);
var receipt = await rpc.SendRequestAsync<TransactionReceipt>("eth_getTransactionReceipt", txHash).ConfigureAwait(false);
while (receipt == null)
{
if (cancellationToken != CancellationToken.None)
{
await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
}
else
{
await Task.Delay(1000, CancellationToken.None).ConfigureAwait(false);
}

receipt = await rpc.SendRequestAsync<TransactionReceipt>("eth_getTransactionReceipt", txHash).ConfigureAwait(false);
}

if (receipt.Failed())
{
throw new Exception($"Transaction {txHash} execution reverted.");
}

var userOpEvent = receipt.DecodeAllEvents<AccountAbstraction.UserOperationEventEventDTO>();
if (userOpEvent != null && userOpEvent.Count > 0 && userOpEvent[0].Event.Success == false)
{
var revertReasonEvent = receipt.DecodeAllEvents<AccountAbstraction.UserOperationRevertReasonEventDTO>();
if (revertReasonEvent != null && revertReasonEvent.Count > 0)
{
var revertReason = revertReasonEvent[0].Event.RevertReason;
var revertReasonString = new FunctionCallDecoder().DecodeFunctionErrorMessage(revertReason.ToHex(true));
throw new Exception($"Transaction {txHash} execution silently reverted: {revertReasonString}");
}
else
{
throw new Exception($"Transaction {txHash} execution silently reverted with no reason string");
}
}

return receipt;
}
}
}
Loading

0 comments on commit 63278fa

Please sign in to comment.