From 63278fa06924b7fbac01a1f78e4da8acadb67460 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Fri, 12 Apr 2024 21:32:40 +0300 Subject: [PATCH 01/11] Lower Level Transaction Builder --- Thirdweb.Tests/Thirdweb.Contracts.Tests.cs | 4 +- Thirdweb.Tests/Thirdweb.Utils.Tests.cs | 24 +-- .../Thirdweb.Contracts/ThirdwebContract.cs | 31 +-- .../ThirdwebTransaction.cs | 196 ++++++++++++++++++ Thirdweb/Thirdweb.Utils/Utils.cs | 88 ++++---- .../SmartWallet/SmartWallet.cs | 8 +- 6 files changed, 259 insertions(+), 92 deletions(-) create mode 100644 Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs diff --git a/Thirdweb.Tests/Thirdweb.Contracts.Tests.cs b/Thirdweb.Tests/Thirdweb.Contracts.Tests.cs index 0430c3c..83f9b64 100644 --- a/Thirdweb.Tests/Thirdweb.Contracts.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Contracts.Tests.cs @@ -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] diff --git a/Thirdweb.Tests/Thirdweb.Utils.Tests.cs b/Thirdweb.Tests/Thirdweb.Utils.Tests.cs index 01f5fa8..631e2a7 100644 --- a/Thirdweb.Tests/Thirdweb.Utils.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Utils.Tests.cs @@ -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(async () => await Utils.GetTransactionReceipt(client, chainId, failedTxHash)); + var failedReceipt = await Assert.ThrowsAsync(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(async () => await Utils.GetTransactionReceipt(client, chainId, aaSilentRevertTxHash)); + var aaFailedReceipt = await Assert.ThrowsAsync(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaSilentRevertTxHash)); Assert.StartsWith($"Transaction {aaSilentRevertTxHash} execution silently reverted", aaFailedReceipt.Message); } @@ -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(async () => await Utils.GetTransactionReceipt(client, chainId, aaSilentRevertTxHashWithReason)); + var aaFailedReceiptWithReason = await Assert.ThrowsAsync(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaSilentRevertTxHashWithReason)); Assert.StartsWith($"Transaction {aaSilentRevertTxHashWithReason} execution silently reverted:", aaFailedReceiptWithReason.Message); } @@ -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(async () => await Utils.GetTransactionReceipt(client, chainId, failedTxHash, cts.Token)); + var failedReceipt = await Assert.ThrowsAsync(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(async () => await Utils.GetTransactionReceipt(client, chainId, aaSilentRevertTxHash, cts.Token)); + var aaFailedReceipt = await Assert.ThrowsAsync(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(async () => await Utils.GetTransactionReceipt(client, chainId, infiniteTxHash, cts.Token)); + var infiniteReceipt = await Assert.ThrowsAsync(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, infiniteTxHash, cts.Token)); Assert.Equal("A task was canceled.", infiniteReceipt.Message); cts = new CancellationTokenSource(); - var infiniteReceipt2 = Assert.ThrowsAsync(() => Utils.GetTransactionReceipt(client, chainId, infiniteTxHash, cts.Token)); + var infiniteReceipt2 = Assert.ThrowsAsync(() => 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); } diff --git a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs index 3f3b3b7..c5c76e4 100644 --- a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs +++ b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs @@ -63,44 +63,21 @@ public static async Task Read(ThirdwebContract contract, string method, pa return function.DecodeTypeOutput(resultData); } - public static async Task Write(IThirdwebWallet wallet, ThirdwebContract contract, string method, BigInteger weiValue, params object[] parameters) + public static async Task 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("eth_estimateGas", transaction)); - transaction.GasPrice = new HexBigInteger(await rpc.SendRequestAsync("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("eth_getTransactionCount", await wallet.GetAddress(), "latest")); - var signedTx = await wallet.SignTransaction(transaction, contract.Chain); - hash = await rpc.SendRequestAsync("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); } } } diff --git a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs new file mode 100644 index 0000000..4df9de9 --- /dev/null +++ b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs @@ -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 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 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 EstimateGasPrice(ThirdwebClient client, BigInteger chainId, bool withBump = true) + { + { + var rpc = ThirdwebRPC.GetRpcInstance(client, chainId); + var hex = new HexBigInteger(await rpc.SendRequestAsync("eth_gasPrice")); + return withBump ? hex.Value * 10 / 9 : hex.Value; + } + } + + public static async Task EstimateGasLimit(ThirdwebTransaction transaction) + { + var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value); + var hex = await rpc.SendRequestAsync("eth_estimateGas", transaction.Input); + return new HexBigInteger(hex).Value; + } + + public static async Task 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("eth_getTransactionCount", await transaction._wallet.GetAddress(), "latest")); + var signedTx = await transaction._wallet.SignTransaction(transaction.Input, transaction.Input.ChainId.Value); + hash = await rpc.SendRequestAsync("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 SendAndWaitForTransactionReceipt(ThirdwebTransaction transaction) + { + var txHash = await Send(transaction); + return await WaitForTransactionReceipt(transaction._client, transaction.Input.ChainId.Value, txHash); + } + + public static async Task WaitForTransactionReceipt(ThirdwebClient client, BigInteger chainId, string txHash, CancellationToken cancellationToken = default) + { + var rpc = ThirdwebRPC.GetRpcInstance(client, chainId); + var receipt = await rpc.SendRequestAsync("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("eth_getTransactionReceipt", txHash).ConfigureAwait(false); + } + + if (receipt.Failed()) + { + throw new Exception($"Transaction {txHash} execution reverted."); + } + + var userOpEvent = receipt.DecodeAllEvents(); + if (userOpEvent != null && userOpEvent.Count > 0 && userOpEvent[0].Event.Success == false) + { + var revertReasonEvent = receipt.DecodeAllEvents(); + 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; + } + } +} diff --git a/Thirdweb/Thirdweb.Utils/Utils.cs b/Thirdweb/Thirdweb.Utils/Utils.cs index 280a789..3617140 100644 --- a/Thirdweb/Thirdweb.Utils/Utils.cs +++ b/Thirdweb/Thirdweb.Utils/Utils.cs @@ -1,10 +1,8 @@ -using System.Numerics; +using System.Globalization; +using System.Numerics; using System.Security.Cryptography; using System.Text; -using Nethereum.ABI.FunctionEncoding; -using Nethereum.Contracts; using Nethereum.Hex.HexConvertors.Extensions; -using Nethereum.RPC.Eth.DTOs; using Nethereum.Signer; namespace Thirdweb @@ -30,49 +28,6 @@ public static string HexConcat(params string[] hexStrings) return hex.ToString(); } - public static async Task GetTransactionReceipt(ThirdwebClient client, BigInteger chainId, string txHash, CancellationToken cancellationToken = default) - { - var rpc = ThirdwebRPC.GetRpcInstance(client, chainId); - var receipt = await rpc.SendRequestAsync("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("eth_getTransactionReceipt", txHash).ConfigureAwait(false); - } - - if (receipt.Failed()) - { - throw new Exception($"Transaction {txHash} execution reverted."); - } - - var userOpEvent = receipt.DecodeAllEvents(); - if (userOpEvent != null && userOpEvent.Count > 0 && userOpEvent[0].Event.Success == false) - { - var revertReasonEvent = receipt.DecodeAllEvents(); - 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; - } - public static byte[] HashPrefixedMessage(this byte[] messageBytes) { var signer = new EthereumMessageSigner(); @@ -149,5 +104,44 @@ public static Dictionary GetThirdwebHeaders(ThirdwebClient clien return headers; } + + public static string ToWei(this string eth) + { + if (!double.TryParse(eth, NumberStyles.Number, CultureInfo.InvariantCulture, out var ethDouble)) + { + throw new ArgumentException("Invalid eth value."); + } + + var wei = (BigInteger)(ethDouble * Constants.DECIMALS_18); + return wei.ToString(); + } + + public static string ToEth(this string wei, int decimalsToDisplay = 4, bool addCommas = false) + { + return FormatERC20(wei, decimalsToDisplay, 18, addCommas); + } + + public static string FormatERC20(this string wei, int decimalsToDisplay = 4, int decimals = 18, bool addCommas = false) + { + decimals = decimals == 0 ? 18 : decimals; + if (!BigInteger.TryParse(wei, out var weiBigInt)) + { + throw new ArgumentException("Invalid wei value."); + } + + var eth = (double)weiBigInt / Math.Pow(10.0, decimals); + var format = addCommas ? "#,0" : "#0"; + if (decimalsToDisplay > 0) + { + format += "."; + } + + for (var i = 0; i < decimalsToDisplay; i++) + { + format += "#"; + } + + return eth.ToString(format); + } } } diff --git a/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs b/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs index d08e406..fd08d9d 100644 --- a/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs @@ -246,7 +246,7 @@ public async Task ForceDeploy() { var input = new TransactionInput("0x", _accountContract.Address, new HexBigInteger(0)); var txHash = await SendTransaction(input); - _ = await Utils.GetTransactionReceipt(_client, _chainId, txHash); + _ = await ThirdwebTransaction.WaitForTransactionReceipt(_client, _chainId, txHash); } public Task GetPersonalAccount() @@ -350,7 +350,7 @@ string reqValidityEndTimestamp Data = data }; var txHash = await SendTransaction(txInput); - return await Utils.GetTransactionReceipt(_client, _chainId, txHash); + return await ThirdwebTransaction.WaitForTransactionReceipt(_client, _chainId, txHash); } public async Task AddAdmin(string admin) @@ -378,7 +378,7 @@ public async Task AddAdmin(string admin) Data = data }; var txHash = await SendTransaction(txInput); - return await Utils.GetTransactionReceipt(_client, _chainId, txHash); + return await ThirdwebTransaction.WaitForTransactionReceipt(_client, _chainId, txHash); } public async Task RemoveAdmin(string admin) @@ -406,7 +406,7 @@ public async Task RemoveAdmin(string admin) Data = data }; var txHash = await SendTransaction(txInput); - return await Utils.GetTransactionReceipt(_client, _chainId, txHash); + return await ThirdwebTransaction.WaitForTransactionReceipt(_client, _chainId, txHash); } public Task SignTypedDataV4(string json) From 9eb85b95141ae33074f0ce2e0bb51b7521a4c860 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Fri, 12 Apr 2024 21:35:13 +0300 Subject: [PATCH 02/11] ThirdwebContract.Prepare --- Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs index c5c76e4..70472d1 100644 --- a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs +++ b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs @@ -63,7 +63,7 @@ public static async Task Read(ThirdwebContract contract, string method, pa return function.DecodeTypeOutput(resultData); } - public static async Task Write(IThirdwebWallet wallet, ThirdwebContract contract, string method, BigInteger weiValue, params object[] parameters) + public static async Task Prepare(IThirdwebWallet wallet, ThirdwebContract contract, string method, BigInteger weiValue, params object[] parameters) { var service = new Nethereum.Contracts.Contract(null, contract.Abi, contract.Address); var function = service.GetFunction(method); @@ -76,7 +76,12 @@ public static async Task Write(IThirdwebWallet wallet, Third Value = new HexBigInteger(weiValue), }; - var thirdwebTx = await ThirdwebTransaction.Create(contract.Client, wallet, transaction, contract.Chain); + return await ThirdwebTransaction.Create(contract.Client, wallet, transaction, contract.Chain); + } + + public static async Task Write(IThirdwebWallet wallet, ThirdwebContract contract, string method, BigInteger weiValue, params object[] parameters) + { + var thirdwebTx = await Prepare(wallet, contract, method, weiValue, parameters); return await ThirdwebTransaction.SendAndWaitForTransactionReceipt(thirdwebTx); } } From 52e6c8017da17d74b2e886b7490436095c72a363 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Fri, 12 Apr 2024 23:16:02 +0300 Subject: [PATCH 03/11] Utils Tests --- Thirdweb.Tests/Thirdweb.Utils.Tests.cs | 102 ++++++++++++++++++++++++- Thirdweb/Thirdweb.Utils/Utils.cs | 12 +-- 2 files changed, 103 insertions(+), 11 deletions(-) diff --git a/Thirdweb.Tests/Thirdweb.Utils.Tests.cs b/Thirdweb.Tests/Thirdweb.Utils.Tests.cs index 631e2a7..df77c90 100644 --- a/Thirdweb.Tests/Thirdweb.Utils.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Utils.Tests.cs @@ -1,4 +1,6 @@ -namespace Thirdweb.Tests; +using System.Numerics; + +namespace Thirdweb.Tests; public class UtilsTests : BaseTests { @@ -174,4 +176,102 @@ public void ReplaceIPFS() replaced = Utils.ReplaceIPFS(uri, gateway); Assert.Equal("https://ipfs.io/ipfs/QmXn1b6Q7", replaced); } + + [Fact] + public void ToWei_ConvertsCorrectly() + { + var eth = "1.5"; + var expectedWei = "1500000000000000000"; + Assert.Equal(expectedWei, Utils.ToWei(eth)); + } + + [Fact] + public void ToWei_ThrowsOnInvalidInput() + { + var invalidEth = "abc"; + Assert.Throws(() => Utils.ToWei(invalidEth)); + } + + [Fact] + public void ToWei_ThrowsExceptionForInvalidInput() + { + var invalidEth = "invalid"; + Assert.Throws(() => Utils.ToWei(invalidEth)); + } + + [Fact] + public void ToWei_ConvertsNegativeValue() + { + var negativeEth = "-1.5"; + var expectedWei = new BigInteger(-1.5 * Math.Pow(10, 18)).ToString(); + Assert.Equal(expectedWei, Utils.ToWei(negativeEth)); + } + + [Fact] + public void ToWei_ConvertsLargeFloat() + { + var largeEth = "1234567890.123456789"; + var expectedWei = new BigInteger(1234567890.123456789 * Math.Pow(10, 18)).ToString(); + Assert.Equal(expectedWei, Utils.ToWei(largeEth)); + } + + [Fact] + public void ToEth_ConvertsCorrectly() + { + var wei = "1500000000000000000"; + var expectedEth = "1.5000"; + Assert.Equal(expectedEth, Utils.ToEth(wei)); + } + + [Fact] + public void ToEth_WithCommas() + { + var wei = "1234500000000000000000"; + var expectedEth = "1,234.5000"; + Assert.Equal(expectedEth, Utils.ToEth(wei, 4, true)); + } + + [Fact] + public void ToEth_ConvertsZeroWei() + { + var zeroWei = "0"; + Assert.Equal("0.0000", Utils.ToEth(zeroWei)); + } + + [Fact] + public void ToEth_ConvertsSmallWei() + { + var smallWei = "1234"; + Assert.Equal("0.0000", Utils.ToEth(smallWei)); + } + + [Fact] + public void FormatERC20_NoDecimalsNoCommas() + { + var wei = "1500000000000000000"; + var expectedEth = "2"; + Assert.Equal(expectedEth, Utils.FormatERC20(wei, 0)); + } + + [Fact] + public void FormatERC20_LargeNumberWithCommas() + { + var wei = "1000000000000000000000000"; + var expectedEth = "1,000,000"; + Assert.Equal(expectedEth, Utils.FormatERC20(wei, 0, 18, true)); + } + + [Fact] + public void FormatERC20_ConvertsZeroWei() + { + var zeroWei = "0"; + Assert.Equal("0", Utils.FormatERC20(zeroWei, 0)); + } + + [Fact] + public void FormatERC20_SmallFractionalWei() + { + var fractionalWei = "10"; + Assert.Equal("0.0000", Utils.FormatERC20(fractionalWei, 4)); + } } diff --git a/Thirdweb/Thirdweb.Utils/Utils.cs b/Thirdweb/Thirdweb.Utils/Utils.cs index 3617140..04fb562 100644 --- a/Thirdweb/Thirdweb.Utils/Utils.cs +++ b/Thirdweb/Thirdweb.Utils/Utils.cs @@ -130,16 +130,8 @@ public static string FormatERC20(this string wei, int decimalsToDisplay = 4, int } var eth = (double)weiBigInt / Math.Pow(10.0, decimals); - var format = addCommas ? "#,0" : "#0"; - if (decimalsToDisplay > 0) - { - format += "."; - } - - for (var i = 0; i < decimalsToDisplay; i++) - { - format += "#"; - } + var format = addCommas ? "#,0." : "#0."; + format += new string('0', decimalsToDisplay); return eth.ToString(format); } From fb93f92429a873a515e3a313c67545f263df8d55 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Fri, 12 Apr 2024 23:16:09 +0300 Subject: [PATCH 04/11] Update Thirdweb.Utils.Tests.cs --- Thirdweb.Tests/Thirdweb.Utils.Tests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Thirdweb.Tests/Thirdweb.Utils.Tests.cs b/Thirdweb.Tests/Thirdweb.Utils.Tests.cs index df77c90..56115f5 100644 --- a/Thirdweb.Tests/Thirdweb.Utils.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Utils.Tests.cs @@ -189,14 +189,14 @@ public void ToWei_ConvertsCorrectly() public void ToWei_ThrowsOnInvalidInput() { var invalidEth = "abc"; - Assert.Throws(() => Utils.ToWei(invalidEth)); + _ = Assert.Throws(() => Utils.ToWei(invalidEth)); } [Fact] public void ToWei_ThrowsExceptionForInvalidInput() { var invalidEth = "invalid"; - Assert.Throws(() => Utils.ToWei(invalidEth)); + _ = Assert.Throws(() => Utils.ToWei(invalidEth)); } [Fact] From 3cae9291ce826884e5e6810f709d714d09c0f4f7 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Fri, 12 Apr 2024 23:25:57 +0300 Subject: [PATCH 05/11] cover full utils create transactiontests --- Thirdweb.Tests/Thirdweb.Transactions.Tests.cs | 88 +++++++++++++++++++ Thirdweb.Tests/Thirdweb.Utils.Tests.cs | 86 ++---------------- Thirdweb/Thirdweb.Utils/Utils.cs | 9 +- 3 files changed, 101 insertions(+), 82 deletions(-) create mode 100644 Thirdweb.Tests/Thirdweb.Transactions.Tests.cs diff --git a/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs new file mode 100644 index 0000000..e055bcb --- /dev/null +++ b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs @@ -0,0 +1,88 @@ +using System.Numerics; + +namespace Thirdweb.Tests; + +public class TransactionTests : BaseTests +{ + public TransactionTests(ITestOutputHelper output) + : base(output) { } + + [Fact] + public async Task WaitForTransactionReceipt() + { + var client = ThirdwebClient.Create(secretKey: _secretKey); + var chainId = 421614; + var normalTxHash = "0x5a0b6cdb01ecfb25b368d3de1ac844414980ee3c330ec8c1435117b75027b5d7"; + var failedTxHash = "0xd2840219ffe172377c8a455c13d95e4dca204d5c0dd72232093e092eef412488"; + var aaTxHash = "0xbf76bd85e1759cf5cf9f4c7c52e76a74d32687f0b516017ff28192d04df50782"; + var aaSilentRevertTxHash = "0x8ada86c63846da7a3f91b8c8332de03f134e7619886425df858ee5400a9d9958"; + + var normalReceipt = await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, normalTxHash); + Assert.NotNull(normalReceipt); + + var failedReceipt = await Assert.ThrowsAsync(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, failedTxHash)); + Assert.Equal($"Transaction {failedTxHash} execution reverted.", failedReceipt.Message); + + var aaReceipt = await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaTxHash); + Assert.NotNull(aaReceipt); + + var aaFailedReceipt = await Assert.ThrowsAsync(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaSilentRevertTxHash)); + Assert.StartsWith($"Transaction {aaSilentRevertTxHash} execution silently reverted", aaFailedReceipt.Message); + } + + [Fact] + public async Task WaitForTransactionReceipt_AAReasonString() + { + var client = ThirdwebClient.Create(secretKey: _secretKey); + var chainId = 84532; + var aaSilentRevertTxHashWithReason = "0x5374743bbb749df47a279ac21e6ed472c30cd471923a7bc78db6a40e1b6924de"; + var aaFailedReceiptWithReason = await Assert.ThrowsAsync(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaSilentRevertTxHashWithReason)); + Assert.StartsWith($"Transaction {aaSilentRevertTxHashWithReason} execution silently reverted:", aaFailedReceiptWithReason.Message); + } + + [Fact] + public async Task WaitForTransactionReceipt_CancellationToken() + { + var client = ThirdwebClient.Create(secretKey: _secretKey); + var chainId = 421614; + var normalTxHash = "0x5a0b6cdb01ecfb25b368d3de1ac844414980ee3c330ec8c1435117b75027b5d7"; + var failedTxHash = "0xd2840219ffe172377c8a455c13d95e4dca204d5c0dd72232093e092eef412488"; + var aaTxHash = "0xbf76bd85e1759cf5cf9f4c7c52e76a74d32687f0b516017ff28192d04df50782"; + var aaSilentRevertTxHash = "0x8ada86c63846da7a3f91b8c8332de03f134e7619886425df858ee5400a9d9958"; + + var cts = new CancellationTokenSource(); + cts.CancelAfter(10000); + var normalReceipt = await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, normalTxHash, cts.Token); + Assert.NotNull(normalReceipt); + + cts = new CancellationTokenSource(); + cts.CancelAfter(10000); + var failedReceipt = await Assert.ThrowsAsync(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 ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaTxHash, cts.Token); + Assert.NotNull(aaReceipt); + + cts = new CancellationTokenSource(); + cts.CancelAfter(10000); + var aaFailedReceipt = await Assert.ThrowsAsync(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(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, infiniteTxHash, cts.Token)); + Assert.Equal("A task was canceled.", infiniteReceipt.Message); + + cts = new CancellationTokenSource(); + var infiniteReceipt2 = Assert.ThrowsAsync(() => 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 ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaTxHash, CancellationToken.None); + Assert.NotNull(aaReceipt2); + } +} diff --git a/Thirdweb.Tests/Thirdweb.Utils.Tests.cs b/Thirdweb.Tests/Thirdweb.Utils.Tests.cs index 56115f5..475b90b 100644 --- a/Thirdweb.Tests/Thirdweb.Utils.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Utils.Tests.cs @@ -20,85 +20,6 @@ public void HexConcat() Assert.Equal("0x1234567890AB", Utils.HexConcat(hexStrings)); } - [Fact] - public async Task GetTransactionReceipt() - { - var client = ThirdwebClient.Create(secretKey: _secretKey); - var chainId = 421614; - var normalTxHash = "0x5a0b6cdb01ecfb25b368d3de1ac844414980ee3c330ec8c1435117b75027b5d7"; - var failedTxHash = "0xd2840219ffe172377c8a455c13d95e4dca204d5c0dd72232093e092eef412488"; - var aaTxHash = "0xbf76bd85e1759cf5cf9f4c7c52e76a74d32687f0b516017ff28192d04df50782"; - var aaSilentRevertTxHash = "0x8ada86c63846da7a3f91b8c8332de03f134e7619886425df858ee5400a9d9958"; - - var normalReceipt = await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, normalTxHash); - Assert.NotNull(normalReceipt); - - var failedReceipt = await Assert.ThrowsAsync(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, failedTxHash)); - Assert.Equal($"Transaction {failedTxHash} execution reverted.", failedReceipt.Message); - - var aaReceipt = await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaTxHash); - Assert.NotNull(aaReceipt); - - var aaFailedReceipt = await Assert.ThrowsAsync(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaSilentRevertTxHash)); - Assert.StartsWith($"Transaction {aaSilentRevertTxHash} execution silently reverted", aaFailedReceipt.Message); - } - - [Fact] - public async Task GetTransactionReceipt_AAReasonString() - { - var client = ThirdwebClient.Create(secretKey: _secretKey); - var chainId = 84532; - var aaSilentRevertTxHashWithReason = "0x5374743bbb749df47a279ac21e6ed472c30cd471923a7bc78db6a40e1b6924de"; - var aaFailedReceiptWithReason = await Assert.ThrowsAsync(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaSilentRevertTxHashWithReason)); - Assert.StartsWith($"Transaction {aaSilentRevertTxHashWithReason} execution silently reverted:", aaFailedReceiptWithReason.Message); - } - - [Fact] - public async Task GetTransactionReceipt_CancellationToken() - { - var client = ThirdwebClient.Create(secretKey: _secretKey); - var chainId = 421614; - var normalTxHash = "0x5a0b6cdb01ecfb25b368d3de1ac844414980ee3c330ec8c1435117b75027b5d7"; - var failedTxHash = "0xd2840219ffe172377c8a455c13d95e4dca204d5c0dd72232093e092eef412488"; - var aaTxHash = "0xbf76bd85e1759cf5cf9f4c7c52e76a74d32687f0b516017ff28192d04df50782"; - var aaSilentRevertTxHash = "0x8ada86c63846da7a3f91b8c8332de03f134e7619886425df858ee5400a9d9958"; - - var cts = new CancellationTokenSource(); - cts.CancelAfter(10000); - var normalReceipt = await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, normalTxHash, cts.Token); - Assert.NotNull(normalReceipt); - - cts = new CancellationTokenSource(); - cts.CancelAfter(10000); - var failedReceipt = await Assert.ThrowsAsync(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 ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaTxHash, cts.Token); - Assert.NotNull(aaReceipt); - - cts = new CancellationTokenSource(); - cts.CancelAfter(10000); - var aaFailedReceipt = await Assert.ThrowsAsync(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(async () => await ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, infiniteTxHash, cts.Token)); - Assert.Equal("A task was canceled.", infiniteReceipt.Message); - - cts = new CancellationTokenSource(); - var infiniteReceipt2 = Assert.ThrowsAsync(() => 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 ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaTxHash, CancellationToken.None); - Assert.NotNull(aaReceipt2); - } - [Fact] public void HashPrefixedMessage() { @@ -274,4 +195,11 @@ public void FormatERC20_SmallFractionalWei() var fractionalWei = "10"; Assert.Equal("0.0000", Utils.FormatERC20(fractionalWei, 4)); } + + [Fact] + public void FormatERC20_ThrowsOnInvalidWei() + { + var invalidWei = "not_a_number"; + Assert.Throws(() => Utils.FormatERC20(invalidWei, 4)); + } } diff --git a/Thirdweb/Thirdweb.Utils/Utils.cs b/Thirdweb/Thirdweb.Utils/Utils.cs index 04fb562..9d3d94a 100644 --- a/Thirdweb/Thirdweb.Utils/Utils.cs +++ b/Thirdweb/Thirdweb.Utils/Utils.cs @@ -123,15 +123,18 @@ public static string ToEth(this string wei, int decimalsToDisplay = 4, bool addC public static string FormatERC20(this string wei, int decimalsToDisplay = 4, int decimals = 18, bool addCommas = false) { - decimals = decimals == 0 ? 18 : decimals; if (!BigInteger.TryParse(wei, out var weiBigInt)) { throw new ArgumentException("Invalid wei value."); } var eth = (double)weiBigInt / Math.Pow(10.0, decimals); - var format = addCommas ? "#,0." : "#0."; - format += new string('0', decimalsToDisplay); + var format = addCommas ? "#,0" : "#0"; + if (decimalsToDisplay > 0) + { + format += "."; + format += new string('0', decimalsToDisplay); + } return eth.ToString(format); } From 07f4eaaf1765240a7983c1410b59ab4851b30d7f Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Sat, 13 Apr 2024 00:32:10 +0300 Subject: [PATCH 06/11] EstimateGasLimit overrides balance, Simulate doesn't Additional tests --- Thirdweb.Tests/Thirdweb.Transactions.Tests.cs | 105 ++++++++++++++++++ .../ThirdwebTransaction.cs | 37 ++++-- 2 files changed, 134 insertions(+), 8 deletions(-) diff --git a/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs index e055bcb..2787953 100644 --- a/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs @@ -1,4 +1,6 @@ using System.Numerics; +using Nethereum.Hex.HexTypes; +using Nethereum.RPC.Eth.DTOs; namespace Thirdweb.Tests; @@ -7,6 +9,109 @@ public class TransactionTests : BaseTests public TransactionTests(ITestOutputHelper output) : base(output) { } + private async Task CreateSampleTransaction() + { + var client = ThirdwebClient.Create(secretKey: _secretKey); + var wallet = await PrivateKeyWallet.Create(client, _testPrivateKey); + var chainId = new BigInteger(1); + + var transaction = await ThirdwebTransaction.Create(client, wallet, new TransactionInput(), chainId); + return transaction; + } + + [Fact] + public async Task Create_ValidatesInputParameters() + { + var client = ThirdwebClient.Create(secretKey: _secretKey); + var wallet = await PrivateKeyWallet.Create(client, _testPrivateKey); + var txInput = new TransactionInput() { From = await wallet.GetAddress() }; + var chainId = new BigInteger(1); + var transaction = await ThirdwebTransaction.Create(client, wallet, txInput, chainId); + Assert.NotNull(transaction); + } + + [Fact] + public async Task Create_ThrowsOnInvalidChainId() + { + var client = ThirdwebClient.Create(secretKey: _secretKey); + var wallet = await PrivateKeyWallet.Create(client, _testPrivateKey); + var txInput = new TransactionInput() { From = "0x123" }; + _ = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(client, wallet, txInput, BigInteger.Zero)); + } + + [Fact] + public async Task SetTo_UpdatesToAddress() + { + var transaction = await CreateSampleTransaction(); + transaction.SetTo("0x456"); + Assert.Equal("0x456", transaction.Input.To); + } + + [Fact] + public async Task SetValue_SetsValue() + { + var transaction = await CreateSampleTransaction(); + var value = new BigInteger(1000); + transaction.SetValue(value); + Assert.Equal(value.ToHexBigInteger(), transaction.Input.Value); + } + + [Fact] + public async Task Send_ThrowsIfToAddressNotProvided() + { + var transaction = await CreateSampleTransaction(); + transaction.SetTo(null); + + _ = await Assert.ThrowsAsync(() => ThirdwebTransaction.Send(transaction)); + } + + [Fact] + public async Task Send_CorrectlyHandlesNonce() + { + var transaction = await CreateSampleTransaction(); + _ = transaction.SetNonce(123); + + Assert.Equal("0x7b", transaction.Input.Nonce.HexValue); + Assert.Equal("123", transaction.Input.Nonce.Value.ToString()); + } + + [Fact] + public async Task EstimateTotalCosts_CalculatesCostsCorrectly() + { + var transaction = await CreateSampleTransaction(); + _ = transaction.SetValue(new BigInteger(1000)); + _ = transaction.SetGasLimit(21000); + + var costs = await ThirdwebTransaction.EstimateTotalCosts(transaction); + + Assert.NotEqual(BigInteger.Zero, costs.wei); + } + + [Fact] + public async Task Simulate_ThrowsInsufficientFunds() + { + var transaction = await CreateSampleTransaction(); + _ = transaction.SetValue(new BigInteger(1000000000000000000)); + _ = transaction.SetGasLimit(21000); + + var exception = await Assert.ThrowsAsync(() => ThirdwebTransaction.Simulate(transaction)); + Assert.Contains("insufficient funds", exception.Message); + } + + [Fact] + public async Task Simulate_ReturnsGasEstimate() + { + var client = ThirdwebClient.Create(secretKey: _secretKey); + var privateKeyAccount = await PrivateKeyWallet.Create(client, _testPrivateKey); + var smartAccount = await SmartWallet.Create(client, personalWallet: privateKeyAccount, factoryAddress: "0xbf1C9aA4B1A085f7DA890a44E82B0A1289A40052", gasless: true, chainId: 421614); + var transaction = await ThirdwebTransaction.Create(client, smartAccount, new TransactionInput(), 421614); + _ = transaction.SetValue(new BigInteger(0)); + _ = transaction.SetGasLimit(250000); + + var gas = await ThirdwebTransaction.Simulate(transaction); + Assert.NotEqual(BigInteger.Zero, gas); + } + [Fact] public async Task WaitForTransactionReceipt() { diff --git a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs index 4df9de9..df0060b 100644 --- a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs +++ b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs @@ -32,6 +32,7 @@ private ThirdwebTransaction(ThirdwebClient client, IThirdwebWallet wallet, Trans public static async Task Create(ThirdwebClient client, IThirdwebWallet wallet, TransactionInput txInput, BigInteger chainId) { + txInput.From ??= await wallet.GetAddress(); return await wallet.GetAddress() != txInput.From ? throw new ArgumentException("Transaction sender (from) must match wallet address") : client == null @@ -86,26 +87,46 @@ public ThirdwebTransaction SetNonce(BigInteger nonce) public static async Task EstimateTotalCosts(ThirdwebTransaction transaction) { - var gasLimit = await EstimateGasLimit(transaction); - var gasPrice = await EstimateGasPrice(transaction._client, transaction.Input.ChainId.Value); + var gasPrice = transaction.Input.GasPrice?.Value ?? await EstimateGasPrice(transaction); + var gasLimit = transaction.Input.Gas?.Value ?? await EstimateGasLimit(transaction, true); 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 EstimateGasPrice(ThirdwebClient client, BigInteger chainId, bool withBump = true) + public static async Task EstimateGasPrice(ThirdwebTransaction transaction, bool withBump = true) { { - var rpc = ThirdwebRPC.GetRpcInstance(client, chainId); + var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value); var hex = new HexBigInteger(await rpc.SendRequestAsync("eth_gasPrice")); return withBump ? hex.Value * 10 / 9 : hex.Value; } } - public static async Task EstimateGasLimit(ThirdwebTransaction transaction) + public static async Task Simulate(ThirdwebTransaction transaction) + { + return await EstimateGasLimit(transaction, false); + } + + public static async Task EstimateGasLimit(ThirdwebTransaction transaction, bool overrideBalance = true) { var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value); - var hex = await rpc.SendRequestAsync("eth_estimateGas", transaction.Input); + var from = transaction.Input.From; + var hex = overrideBalance + ? await rpc.SendRequestAsync( + "eth_estimateGas", + transaction.Input, + "latest", + new Dictionary>() + { + { + from, + new() { { "balance", "0xFFFFFFFFFFFFFFFFFFFF" } } + } + } + ) + : await rpc.SendRequestAsync("eth_estimateGas", transaction.Input, "latest"); + return new HexBigInteger(hex).Value; } @@ -117,12 +138,12 @@ public static async Task Send(ThirdwebTransaction transaction) } 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.GasPrice ??= new HexBigInteger(await EstimateGasPrice(transaction)); transaction.Input.MaxFeePerGas = null; transaction.Input.MaxPriorityFeePerGas = null; + transaction.Input.Gas ??= new HexBigInteger(await EstimateGasLimit(transaction)); var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value); string hash; From 51eab6b3162611541c849483d9297f322e1111fb Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Sat, 13 Apr 2024 00:44:33 +0300 Subject: [PATCH 07/11] Update Thirdweb.Transactions.Tests.cs --- Thirdweb.Tests/Thirdweb.Transactions.Tests.cs | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs index 2787953..e03616a 100644 --- a/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs @@ -31,7 +31,7 @@ public async Task Create_ValidatesInputParameters() } [Fact] - public async Task Create_ThrowsOnInvalidChainId() + public async Task Create_ThrowsOnInvalidAddress() { var client = ThirdwebClient.Create(secretKey: _secretKey); var wallet = await PrivateKeyWallet.Create(client, _testPrivateKey); @@ -39,6 +39,23 @@ public async Task Create_ThrowsOnInvalidChainId() _ = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(client, wallet, txInput, BigInteger.Zero)); } + [Fact] + public async Task Create_ThrowsOnInvalidChainId() + { + var client = ThirdwebClient.Create(secretKey: _secretKey); + var wallet = await PrivateKeyWallet.Create(client, _testPrivateKey); + var txInput = new TransactionInput(); + _ = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(client, wallet, txInput, BigInteger.Zero)); + } + + [Fact] + public async Task ToString_OverridesCorrectly() + { + var transaction = await CreateSampleTransaction(); + Assert.NotNull(transaction.ToString()); + Assert.StartsWith("{", transaction.ToString()); + } + [Fact] public async Task SetTo_UpdatesToAddress() { @@ -56,6 +73,24 @@ public async Task SetValue_SetsValue() Assert.Equal(value.ToHexBigInteger(), transaction.Input.Value); } + [Fact] + public async Task SetValue_SetsData() + { + var transaction = await CreateSampleTransaction(); + var data = "0x123456"; + _ = transaction.SetData(data); + Assert.Equal(data, transaction.Input.Data); + } + + [Fact] + public async Task SetValue_SetsGasPrice() + { + var transaction = await CreateSampleTransaction(); + var gas = new BigInteger(1000); + transaction.SetGasLimit(gas); + Assert.Equal(gas.ToHexBigInteger(), transaction.Input.Gas); + } + [Fact] public async Task Send_ThrowsIfToAddressNotProvided() { @@ -87,6 +122,16 @@ public async Task EstimateTotalCosts_CalculatesCostsCorrectly() Assert.NotEqual(BigInteger.Zero, costs.wei); } + [Fact] + public async Task EstimateGasPrice_BumpsCorrectly() + { + var transaction = await CreateSampleTransaction(); + var gasPrice = await ThirdwebTransaction.EstimateGasPrice(transaction, withBump: false); + var gasPriceWithBump = await ThirdwebTransaction.EstimateGasPrice(transaction, withBump: true); + Assert.NotEqual(gasPrice, gasPriceWithBump); + Assert.True(gasPriceWithBump > gasPrice); + } + [Fact] public async Task Simulate_ThrowsInsufficientFunds() { From e4cad1ed7aae8cc7fafbb9d869ab4a93f1660587 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Sat, 13 Apr 2024 00:46:19 +0300 Subject: [PATCH 08/11] TotalCosts --- Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs index df0060b..f464f4f 100644 --- a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs +++ b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs @@ -9,7 +9,7 @@ namespace Thirdweb { - public struct GasCosts + public struct TotalCosts { public string ether; public BigInteger wei; @@ -85,13 +85,13 @@ public ThirdwebTransaction SetNonce(BigInteger nonce) return this; } - public static async Task EstimateTotalCosts(ThirdwebTransaction transaction) + public static async Task EstimateTotalCosts(ThirdwebTransaction transaction) { var gasPrice = transaction.Input.GasPrice?.Value ?? await EstimateGasPrice(transaction); var gasLimit = transaction.Input.Gas?.Value ?? await EstimateGasLimit(transaction, true); 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 }; + return new TotalCosts { ether = gasCostWithValue.ToString().ToEth(18, false), wei = gasCostWithValue }; } public static async Task EstimateGasPrice(ThirdwebTransaction transaction, bool withBump = true) From 22f72c7b591111d14ed77a1e83723620cd753f75 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Sat, 13 Apr 2024 00:48:42 +0300 Subject: [PATCH 09/11] EstimateGasCosts + EstimateTotalCosts --- Thirdweb.Tests/Thirdweb.Transactions.Tests.cs | 26 +++++++++++++++++++ .../ThirdwebTransaction.cs | 8 ++++++ 2 files changed, 34 insertions(+) diff --git a/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs index e03616a..8856ef1 100644 --- a/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs @@ -122,6 +122,32 @@ public async Task EstimateTotalCosts_CalculatesCostsCorrectly() Assert.NotEqual(BigInteger.Zero, costs.wei); } + [Fact] + public async Task EstimateGasCosts_CalculatesCostsCorrectly() + { + var transaction = await CreateSampleTransaction(); + _ = transaction.SetValue(new BigInteger(1000)); + _ = transaction.SetGasLimit(21000); + + var costs = await ThirdwebTransaction.EstimateTotalCosts(transaction); + + Assert.NotEqual(BigInteger.Zero, costs.wei); + } + + [Fact] + public async Task EstimateTotalCosts_HigherThanGasCostsByValue() + { + var transaction = await CreateSampleTransaction(); + _ = transaction.SetValue(new BigInteger(1000)); + _ = transaction.SetGasLimit(21000); + + var totalCosts = await ThirdwebTransaction.EstimateTotalCosts(transaction); + var gasCosts = await ThirdwebTransaction.EstimateGasCosts(transaction); + + Assert.True(totalCosts.wei > gasCosts.wei); + Assert.True(totalCosts.wei - gasCosts.wei == transaction.Input.Value.Value); + } + [Fact] public async Task EstimateGasPrice_BumpsCorrectly() { diff --git a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs index f464f4f..c7cd2f4 100644 --- a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs +++ b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs @@ -85,6 +85,14 @@ public ThirdwebTransaction SetNonce(BigInteger nonce) return this; } + public static async Task EstimateGasCosts(ThirdwebTransaction transaction) + { + var gasPrice = transaction.Input.GasPrice?.Value ?? await EstimateGasPrice(transaction); + var gasLimit = transaction.Input.Gas?.Value ?? await EstimateGasLimit(transaction, true); + var gasCost = BigInteger.Multiply(gasLimit, gasPrice); + return new TotalCosts { ether = gasCost.ToString().ToEth(18, false), wei = gasCost }; + } + public static async Task EstimateTotalCosts(ThirdwebTransaction transaction) { var gasPrice = transaction.Input.GasPrice?.Value ?? await EstimateGasPrice(transaction); From 6cda515df70da0ca5771282eaaade69f872d5266 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Sat, 13 Apr 2024 00:54:19 +0300 Subject: [PATCH 10/11] add unset gas/price estimation tests --- Thirdweb.Tests/Thirdweb.Transactions.Tests.cs | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs index 8856ef1..b815b78 100644 --- a/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs @@ -60,7 +60,7 @@ public async Task ToString_OverridesCorrectly() public async Task SetTo_UpdatesToAddress() { var transaction = await CreateSampleTransaction(); - transaction.SetTo("0x456"); + _ = transaction.SetTo("0x456"); Assert.Equal("0x456", transaction.Input.To); } @@ -69,7 +69,7 @@ public async Task SetValue_SetsValue() { var transaction = await CreateSampleTransaction(); var value = new BigInteger(1000); - transaction.SetValue(value); + _ = transaction.SetValue(value); Assert.Equal(value.ToHexBigInteger(), transaction.Input.Value); } @@ -87,15 +87,15 @@ public async Task SetValue_SetsGasPrice() { var transaction = await CreateSampleTransaction(); var gas = new BigInteger(1000); - transaction.SetGasLimit(gas); - Assert.Equal(gas.ToHexBigInteger(), transaction.Input.Gas); + _ = transaction.SetGasPrice(gas); + Assert.Equal(gas.ToHexBigInteger(), transaction.Input.GasPrice); } [Fact] public async Task Send_ThrowsIfToAddressNotProvided() { var transaction = await CreateSampleTransaction(); - transaction.SetTo(null); + _ = transaction.SetTo(null); _ = await Assert.ThrowsAsync(() => ThirdwebTransaction.Send(transaction)); } @@ -116,6 +116,18 @@ public async Task EstimateTotalCosts_CalculatesCostsCorrectly() var transaction = await CreateSampleTransaction(); _ = transaction.SetValue(new BigInteger(1000)); _ = transaction.SetGasLimit(21000); + _ = transaction.SetGasPrice(new BigInteger(1000000000)); + + var costs = await ThirdwebTransaction.EstimateTotalCosts(transaction); + + Assert.NotEqual(BigInteger.Zero, costs.wei); + } + + [Fact] + public async Task EstimateTotalCosts_WithoutSetting_CalculatesCostsCorrectly() + { + var transaction = await CreateSampleTransaction(); + _ = transaction.SetValue(new BigInteger(1000)); var costs = await ThirdwebTransaction.EstimateTotalCosts(transaction); @@ -128,8 +140,20 @@ public async Task EstimateGasCosts_CalculatesCostsCorrectly() var transaction = await CreateSampleTransaction(); _ = transaction.SetValue(new BigInteger(1000)); _ = transaction.SetGasLimit(21000); + _ = transaction.SetGasPrice(new BigInteger(1000000000)); - var costs = await ThirdwebTransaction.EstimateTotalCosts(transaction); + var costs = await ThirdwebTransaction.EstimateGasCosts(transaction); + + Assert.NotEqual(BigInteger.Zero, costs.wei); + } + + [Fact] + public async Task EstimateGasCosts_WithoutSetting_CalculatesCostsCorrectly() + { + var transaction = await CreateSampleTransaction(); + _ = transaction.SetValue(new BigInteger(1000)); + + var costs = await ThirdwebTransaction.EstimateGasCosts(transaction); Assert.NotEqual(BigInteger.Zero, costs.wei); } From af54507c6e0f08d0cdf3b7def3351d0d949e3433 Mon Sep 17 00:00:00 2001 From: 0xFirekeeper <0xFirekeeper@gmail.com> Date: Sat, 13 Apr 2024 00:56:35 +0300 Subject: [PATCH 11/11] ex from --- Thirdweb.Tests/Thirdweb.Transactions.Tests.cs | 3 ++- Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs index b815b78..bbe6b96 100644 --- a/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs @@ -36,7 +36,8 @@ public async Task Create_ThrowsOnInvalidAddress() var client = ThirdwebClient.Create(secretKey: _secretKey); var wallet = await PrivateKeyWallet.Create(client, _testPrivateKey); var txInput = new TransactionInput() { From = "0x123" }; - _ = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(client, wallet, txInput, BigInteger.Zero)); + var ex = await Assert.ThrowsAsync(() => ThirdwebTransaction.Create(client, wallet, txInput, BigInteger.Zero)); + Assert.Contains("Transaction sender (from) must match wallet address", ex.Message); } [Fact] diff --git a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs index c7cd2f4..d5411cf 100644 --- a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs +++ b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs @@ -32,8 +32,9 @@ private ThirdwebTransaction(ThirdwebClient client, IThirdwebWallet wallet, Trans public static async Task Create(ThirdwebClient client, IThirdwebWallet wallet, TransactionInput txInput, BigInteger chainId) { - txInput.From ??= await wallet.GetAddress(); - return await wallet.GetAddress() != txInput.From + var address = await wallet.GetAddress(); + txInput.From ??= address; + return address != txInput.From ? throw new ArgumentException("Transaction sender (from) must match wallet address") : client == null ? throw new ArgumentNullException(nameof(client))