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.Transactions.Tests.cs b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs
new file mode 100644
index 0000000..bbe6b96
--- /dev/null
+++ b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs
@@ -0,0 +1,289 @@
+using System.Numerics;
+using Nethereum.Hex.HexTypes;
+using Nethereum.RPC.Eth.DTOs;
+
+namespace Thirdweb.Tests;
+
+public class TransactionTests : BaseTests
+{
+    public TransactionTests(ITestOutputHelper output)
+        : base(output) { }
+
+    private async Task<ThirdwebTransaction> 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_ThrowsOnInvalidAddress()
+    {
+        var client = ThirdwebClient.Create(secretKey: _secretKey);
+        var wallet = await PrivateKeyWallet.Create(client, _testPrivateKey);
+        var txInput = new TransactionInput() { From = "0x123" };
+        var ex = await Assert.ThrowsAsync<ArgumentException>(() => ThirdwebTransaction.Create(client, wallet, txInput, BigInteger.Zero));
+        Assert.Contains("Transaction sender (from) must match wallet address", ex.Message);
+    }
+
+    [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<ArgumentException>(() => 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()
+    {
+        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 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.SetGasPrice(gas);
+        Assert.Equal(gas.ToHexBigInteger(), transaction.Input.GasPrice);
+    }
+
+    [Fact]
+    public async Task Send_ThrowsIfToAddressNotProvided()
+    {
+        var transaction = await CreateSampleTransaction();
+        _ = transaction.SetTo(null);
+
+        _ = await Assert.ThrowsAsync<ArgumentException>(() => 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);
+        _ = 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);
+
+        Assert.NotEqual(BigInteger.Zero, costs.wei);
+    }
+
+    [Fact]
+    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.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);
+    }
+
+    [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()
+    {
+        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()
+    {
+        var transaction = await CreateSampleTransaction();
+        _ = transaction.SetValue(new BigInteger(1000000000000000000));
+        _ = transaction.SetGasLimit(21000);
+
+        var exception = await Assert.ThrowsAsync<Exception>(() => 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()
+    {
+        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<Exception>(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<Exception>(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<Exception>(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<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 ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, aaTxHash, cts.Token);
+        Assert.NotNull(aaReceipt);
+
+        cts = new CancellationTokenSource();
+        cts.CancelAfter(10000);
+        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 ThirdwebTransaction.WaitForTransactionReceipt(client, chainId, infiniteTxHash, cts.Token));
+        Assert.Equal("A task was canceled.", infiniteReceipt.Message);
+
+        cts = new CancellationTokenSource();
+        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 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 01f5fa8..475b90b 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
 {
@@ -18,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 Utils.GetTransactionReceipt(client, chainId, normalTxHash);
-        Assert.NotNull(normalReceipt);
-
-        var failedReceipt = await Assert.ThrowsAsync<Exception>(async () => await Utils.GetTransactionReceipt(client, chainId, failedTxHash));
-        Assert.Equal($"Transaction {failedTxHash} execution reverted.", failedReceipt.Message);
-
-        var aaReceipt = await Utils.GetTransactionReceipt(client, chainId, aaTxHash);
-        Assert.NotNull(aaReceipt);
-
-        var aaFailedReceipt = await Assert.ThrowsAsync<Exception>(async () => await Utils.GetTransactionReceipt(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<Exception>(async () => await Utils.GetTransactionReceipt(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 Utils.GetTransactionReceipt(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));
-        Assert.Equal($"Transaction {failedTxHash} execution reverted.", failedReceipt.Message);
-
-        cts = new CancellationTokenSource();
-        cts.CancelAfter(10000);
-        var aaReceipt = await Utils.GetTransactionReceipt(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));
-        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));
-        Assert.Equal("A task was canceled.", infiniteReceipt.Message);
-
-        cts = new CancellationTokenSource();
-        var infiniteReceipt2 = Assert.ThrowsAsync<TaskCanceledException>(() => Utils.GetTransactionReceipt(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);
-        Assert.NotNull(aaReceipt2);
-    }
-
     [Fact]
     public void HashPrefixedMessage()
     {
@@ -174,4 +97,109 @@ 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<ArgumentException>(() => Utils.ToWei(invalidEth));
+    }
+
+    [Fact]
+    public void ToWei_ThrowsExceptionForInvalidInput()
+    {
+        var invalidEth = "invalid";
+        _ = Assert.Throws<ArgumentException>(() => 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));
+    }
+
+    [Fact]
+    public void FormatERC20_ThrowsOnInvalidWei()
+    {
+        var invalidWei = "not_a_number";
+        Assert.Throws<ArgumentException>(() => Utils.FormatERC20(invalidWei, 4));
+    }
 }
diff --git a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs
index 3f3b3b7..70472d1 100644
--- a/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs
+++ b/Thirdweb/Thirdweb.Contracts/ThirdwebContract.cs
@@ -63,44 +63,26 @@ 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<ThirdwebTransaction> Prepare(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);
+            return await ThirdwebTransaction.Create(contract.Client, wallet, transaction, contract.Chain);
+        }
 
-            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;
+        public static async Task<TransactionReceipt> 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);
         }
     }
 }
diff --git a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs
new file mode 100644
index 0000000..d5411cf
--- /dev/null
+++ b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs
@@ -0,0 +1,226 @@
+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 TotalCosts
+    {
+        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)
+        {
+            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))
+                    : 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<TotalCosts> 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<TotalCosts> 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 TotalCosts { ether = gasCostWithValue.ToString().ToEth(18, false), wei = gasCostWithValue };
+        }
+
+        public static async Task<BigInteger> EstimateGasPrice(ThirdwebTransaction transaction, bool withBump = true)
+        {
+            {
+                var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value);
+                var hex = new HexBigInteger(await rpc.SendRequestAsync<string>("eth_gasPrice"));
+                return withBump ? hex.Value * 10 / 9 : hex.Value;
+            }
+        }
+
+        public static async Task<BigInteger> Simulate(ThirdwebTransaction transaction)
+        {
+            return await EstimateGasLimit(transaction, false);
+        }
+
+        public static async Task<BigInteger> EstimateGasLimit(ThirdwebTransaction transaction, bool overrideBalance = true)
+        {
+            var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value);
+            var from = transaction.Input.From;
+            var hex = overrideBalance
+                ? await rpc.SendRequestAsync<string>(
+                    "eth_estimateGas",
+                    transaction.Input,
+                    "latest",
+                    new Dictionary<string, Dictionary<string, string>>()
+                    {
+                        {
+                            from,
+                            new() { { "balance", "0xFFFFFFFFFFFFFFFFFFFF" } }
+                        }
+                    }
+                )
+                : await rpc.SendRequestAsync<string>("eth_estimateGas", transaction.Input, "latest");
+
+            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.Value ??= new HexBigInteger(0);
+            transaction.Input.Data ??= "0x";
+            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;
+            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;
+        }
+    }
+}
diff --git a/Thirdweb/Thirdweb.Utils/Utils.cs b/Thirdweb/Thirdweb.Utils/Utils.cs
index 280a789..9d3d94a 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<TransactionReceipt> GetTransactionReceipt(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;
-        }
-
         public static byte[] HashPrefixedMessage(this byte[] messageBytes)
         {
             var signer = new EthereumMessageSigner();
@@ -149,5 +104,39 @@ public static Dictionary<string, string> 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)
+        {
+            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 += ".";
+                format += new string('0', decimalsToDisplay);
+            }
+
+            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<IThirdwebWallet> 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<TransactionReceipt> AddAdmin(string admin)
@@ -378,7 +378,7 @@ public async Task<TransactionReceipt> 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<TransactionReceipt> RemoveAdmin(string admin)
@@ -406,7 +406,7 @@ public async Task<TransactionReceipt> 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<string> SignTypedDataV4(string json)