From 2bc7f65887bbc33d5c4313ce20e55ba43d16e532 Mon Sep 17 00:00:00 2001 From: Firekeeper <0xFirekeeper@gmail.com> Date: Tue, 16 Apr 2024 02:45:52 +0300 Subject: [PATCH] ThirdwebTransaction.Sign + Modify Estimation/Simulation Flows (#17) * ThirdwebTransaction.Sign + Modify Estimation/Simulation Flows * t sign * Update Program.cs * t wallets --- Thirdweb.Console/Program.cs | 39 +++++++++++ .../Thirdweb.PrivateKeyWallet.Tests.cs | 17 +++++ Thirdweb.Tests/Thirdweb.SmartWallet.Tests.cs | 10 +++ Thirdweb.Tests/Thirdweb.Transactions.Tests.cs | 51 +++++++++++++- .../ThirdwebTransaction.cs | 66 +++++++++---------- .../SmartWallet/SmartWallet.cs | 12 +++- 6 files changed, 155 insertions(+), 40 deletions(-) diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs index 318a83d..a6207d9 100644 --- a/Thirdweb.Console/Program.cs +++ b/Thirdweb.Console/Program.cs @@ -1,6 +1,9 @@ using System.Numerics; using Thirdweb; using dotenv.net; +using Newtonsoft.Json; +using Nethereum.RPC.Eth.DTOs; +using Nethereum.Hex.HexTypes; DotEnv.Load(); @@ -90,6 +93,42 @@ var balanceAfter = await ThirdwebContract.Read(contract, "balanceOf", await smartWallet.GetAddress()); Console.WriteLine($"Balance after mint: {balanceAfter}"); +// Transaction Builder +var preparedTx = await ThirdwebContract.Prepare(wallet: smartWallet, contract: contract, method: "mintTo", weiValue: 0, parameters: new object[] { await smartWallet.GetAddress(), 100 }); +Console.WriteLine($"Prepared transaction: {preparedTx}"); +var estimatedCosts = await ThirdwebTransaction.EstimateGasCosts(preparedTx); +Console.WriteLine($"Estimated ETH gas cost: {estimatedCosts.ether}"); +var totalCosts = await ThirdwebTransaction.EstimateTotalCosts(preparedTx); +Console.WriteLine($"Estimated ETH total cost: {totalCosts.ether}"); +var simulationData = await ThirdwebTransaction.Simulate(preparedTx); +Console.WriteLine($"Simulation data: {simulationData}"); +var txHash = await ThirdwebTransaction.Send(preparedTx); +Console.WriteLine($"Transaction hash: {txHash}"); +var receipt = await ThirdwebTransaction.WaitForTransactionReceipt(client, 421614, txHash); +Console.WriteLine($"Transaction receipt: {JsonConvert.SerializeObject(receipt)}"); + +// Transaction Builder - raw transfer +var rawTx = new TransactionInput +{ + From = await smartWallet.GetAddress(), + To = await smartWallet.GetAddress(), + Value = new HexBigInteger(BigInteger.Zero), + Data = "0x", +}; +var preparedRawTx = await ThirdwebTransaction.Create(client: client, wallet: smartWallet, txInput: rawTx, chainId: 421614); +Console.WriteLine($"Prepared raw transaction: {preparedRawTx}"); +var estimatedCostsRaw = await ThirdwebTransaction.EstimateGasCosts(preparedRawTx); +Console.WriteLine($"Estimated ETH gas cost: {estimatedCostsRaw.ether}"); +var totalCostsRaw = await ThirdwebTransaction.EstimateTotalCosts(preparedRawTx); +Console.WriteLine($"Estimated ETH total cost: {totalCostsRaw.ether}"); +var simulationDataRaw = await ThirdwebTransaction.Simulate(preparedRawTx); +Console.WriteLine($"Simulation data: {simulationDataRaw}"); +var txHashRaw = await ThirdwebTransaction.Send(preparedRawTx); +Console.WriteLine($"Raw transaction hash: {txHashRaw}"); +var receiptRaw = await ThirdwebTransaction.WaitForTransactionReceipt(client, 421614, txHashRaw); +Console.WriteLine($"Raw transaction receipt: {JsonConvert.SerializeObject(receiptRaw)}"); + + // Storage actions // // Will download from IPFS or normal urls diff --git a/Thirdweb.Tests/Thirdweb.PrivateKeyWallet.Tests.cs b/Thirdweb.Tests/Thirdweb.PrivateKeyWallet.Tests.cs index 779930f..383e16c 100644 --- a/Thirdweb.Tests/Thirdweb.PrivateKeyWallet.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.PrivateKeyWallet.Tests.cs @@ -169,6 +169,23 @@ public async Task SignTransaction_Success() Assert.NotNull(signature); } + [Fact] + public async Task SignTransaction_NoFrom_Success() + { + var account = await GetAccount(); + var transaction = new TransactionInput + { + To = Constants.ADDRESS_ZERO, + // Value = new HexBigInteger(0), + Gas = new HexBigInteger(21000), + Data = "0x", + Nonce = new HexBigInteger(99999999999), + GasPrice = new HexBigInteger(10000000000) + }; + var signature = await account.SignTransaction(transaction, 421614); + Assert.NotNull(signature); + } + [Fact] public async Task SignTransaction_NullTransaction() { diff --git a/Thirdweb.Tests/Thirdweb.SmartWallet.Tests.cs b/Thirdweb.Tests/Thirdweb.SmartWallet.Tests.cs index 31923b2..2efb62e 100644 --- a/Thirdweb.Tests/Thirdweb.SmartWallet.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.SmartWallet.Tests.cs @@ -36,6 +36,16 @@ public async Task Initialization_Fail() Assert.Equal("SmartAccount.Connect: Personal account must be connected.", ex.Message); } + [Fact] + public async Task ForceDeploy_Success() + { + 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); + await smartAccount.ForceDeploy(); + Assert.True(await smartAccount.IsDeployed()); + } + [Fact] public async Task IsDeployed_True() { diff --git a/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs index e1a28c9..2f5b9f2 100644 --- a/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs +++ b/Thirdweb.Tests/Thirdweb.Transactions.Tests.cs @@ -92,6 +92,27 @@ public async Task SetValue_SetsGasPrice() Assert.Equal(gas.ToHexBigInteger(), transaction.Input.GasPrice); } + [Fact] + public async Task Sign_SmartWallet_SignsTransaction() + { + 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() + { + To = Constants.ADDRESS_ZERO, + Value = new HexBigInteger(0), + Data = "0x" + }, + 421614 + ); + var signed = await ThirdwebTransaction.Sign(transaction); + Assert.NotNull(signed); + } + [Fact] public async Task Send_ThrowsIfToAddressNotProvided() { @@ -128,6 +149,7 @@ public async Task EstimateTotalCosts_CalculatesCostsCorrectly() public async Task EstimateTotalCosts_WithoutSetting_CalculatesCostsCorrectly() { var transaction = await CreateSampleTransaction(); + transaction.Input.From = Constants.ADDRESS_ZERO; _ = transaction.SetValue(new BigInteger(1000)); var costs = await ThirdwebTransaction.EstimateTotalCosts(transaction); @@ -162,6 +184,7 @@ public async Task EstimateGasCosts_CalculatesCostsCorrectly() public async Task EstimateGasCosts_WithoutSetting_CalculatesCostsCorrectly() { var transaction = await CreateSampleTransaction(); + transaction.Input.From = Constants.ADDRESS_ZERO; _ = transaction.SetValue(new BigInteger(1000)); var costs = await ThirdwebTransaction.EstimateGasCosts(transaction); @@ -169,6 +192,28 @@ public async Task EstimateGasCosts_WithoutSetting_CalculatesCostsCorrectly() Assert.NotEqual(BigInteger.Zero, costs.wei); } + [Fact] + public async Task EstimateGasCosts_SmartWalletHigherThanPrivateKeyWallet() + { + 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.SetTo(Constants.ADDRESS_ZERO); + _ = transaction.SetValue(new BigInteger(1000)); + + var smartCosts = await ThirdwebTransaction.EstimateGasCosts(transaction); + + transaction = await ThirdwebTransaction.Create(client, privateKeyAccount, new TransactionInput(), 421614); + _ = transaction.SetTo(Constants.ADDRESS_ZERO); + _ = transaction.SetValue(new BigInteger(1000)); + + var privateCosts = await ThirdwebTransaction.EstimateGasCosts(transaction); + + Assert.True(smartCosts.wei > privateCosts.wei); + } + [Fact] public async Task EstimateTotalCosts_HigherThanGasCostsByValue() { @@ -205,7 +250,7 @@ public async Task Simulate_ThrowsInsufficientFunds() } [Fact] - public async Task Simulate_ReturnsGasEstimate() + public async Task Simulate_ReturnsData() { var client = ThirdwebClient.Create(secretKey: _secretKey); var privateKeyAccount = await PrivateKeyWallet.Create(client, _testPrivateKey); @@ -214,8 +259,8 @@ public async Task Simulate_ReturnsGasEstimate() _ = transaction.SetValue(new BigInteger(0)); _ = transaction.SetGasLimit(250000); - var gas = await ThirdwebTransaction.Simulate(transaction); - Assert.NotEqual(BigInteger.Zero, gas); + var data = await ThirdwebTransaction.Simulate(transaction); + Assert.NotNull(data); } [Fact] diff --git a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs index d5411cf..8979dac 100644 --- a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs +++ b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs @@ -1,7 +1,6 @@ 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; @@ -34,6 +33,7 @@ public static async Task Create(ThirdwebClient client, IThi { var address = await wallet.GetAddress(); txInput.From ??= address; + txInput.Data ??= "0x"; return address != txInput.From ? throw new ArgumentException("Transaction sender (from) must match wallet address") : client == null @@ -89,54 +89,50 @@ public ThirdwebTransaction SetNonce(BigInteger nonce) 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 gasLimit = transaction.Input.Gas?.Value ?? await EstimateGasLimit(transaction); 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); - 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 }; + var gasCosts = await EstimateGasCosts(transaction); + var value = transaction.Input.Value?.Value ?? 0; + return new TotalCosts { ether = (value + gasCosts.wei).ToString().ToEth(18, false), wei = value + gasCosts.wei }; } public static async Task EstimateGasPrice(ThirdwebTransaction transaction, bool withBump = true) { - { - 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; - } + 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 Simulate(ThirdwebTransaction transaction) + public static async Task Simulate(ThirdwebTransaction transaction) { - return await EstimateGasLimit(transaction, false); + var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value); + var data = await rpc.SendRequestAsync("eth_call", transaction.Input, "latest"); + return data; } - public static async Task EstimateGasLimit(ThirdwebTransaction transaction, bool overrideBalance = true) + public static async Task EstimateGasLimit(ThirdwebTransaction transaction) { - var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value); - 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; + if (transaction._wallet.AccountType == ThirdwebAccountType.SmartAccount) + { + var smartAccount = transaction._wallet as SmartWallet; + return await smartAccount.EstimateUserOperationGas(transaction.Input, transaction.Input.ChainId.Value); + } + else + { + var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value); + var hex = await rpc.SendRequestAsync("eth_estimateGas", transaction.Input, "latest"); + return new HexBigInteger(hex).Value; + } + } + + public static async Task Sign(ThirdwebTransaction transaction) + { + return await transaction._wallet.SignTransaction(transaction.Input, transaction.Input.ChainId.Value); } public static async Task Send(ThirdwebTransaction transaction) @@ -149,10 +145,10 @@ public static async Task Send(ThirdwebTransaction transaction) 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)); + transaction.Input.GasPrice ??= new HexBigInteger(await EstimateGasPrice(transaction)); var rpc = ThirdwebRPC.GetRpcInstance(transaction._client, transaction.Input.ChainId.Value); string hash; @@ -160,7 +156,7 @@ public static async Task Send(ThirdwebTransaction transaction) { 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); + var signedTx = await Sign(transaction); hash = await rpc.SendRequestAsync("eth_sendRawTransaction", signedTx); break; case ThirdwebAccountType.SmartAccount: diff --git a/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs b/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs index fd08d9d..137e45b 100644 --- a/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs @@ -5,6 +5,7 @@ using Nethereum.Hex.HexConvertors.Extensions; using Nethereum.Hex.HexTypes; using Nethereum.RPC.Eth.DTOs; +using Newtonsoft.Json; using Thirdweb.AccountAbstraction; namespace Thirdweb @@ -420,9 +421,16 @@ public Task SignTypedDataV4(T data, TypedData typed return _personalAccount.SignTypedDataV4(data, typedData); } - public Task SignTransaction(TransactionInput transaction, BigInteger chainId) + public async Task EstimateUserOperationGas(TransactionInput transaction, BigInteger chainId) { - return _personalAccount.SignTransaction(transaction, chainId); + var signedOp = await SignUserOp(transaction); + var cost = signedOp.CallGasLimit + signedOp.VerificationGasLimit + signedOp.PreVerificationGas; + return cost; + } + + public async Task SignTransaction(TransactionInput transaction, BigInteger chainId) + { + return JsonConvert.SerializeObject(EncodeUserOperation(await SignUserOp(transaction))); } public Task IsConnected()