From 7fe9a2bb5f4080565f838c6c16af78c2c8c43d49 Mon Sep 17 00:00:00 2001 From: Firekeeper <0xFirekeeper@gmail.com> Date: Fri, 9 Aug 2024 05:16:40 +0300 Subject: [PATCH] Multi-ERC20 Paymaster Support (#45) Signed-off-by: Firekeeper <0xFirekeeper@gmail.com> --- Thirdweb.Console/Program.cs | 75 +++++++++++++------ .../ThirdwebTransaction.cs | 1 + .../SmartWallet/SmartWallet.cs | 55 ++++++++++++-- 3 files changed, 101 insertions(+), 30 deletions(-) diff --git a/Thirdweb.Console/Program.cs b/Thirdweb.Console/Program.cs index df93682..3224181 100644 --- a/Thirdweb.Console/Program.cs +++ b/Thirdweb.Console/Program.cs @@ -22,8 +22,37 @@ Console.WriteLine($"Contract read result: {readResult}"); // Create wallets (this is an advanced use case, typically one wallet is plenty) -var privateKeyWallet = await PrivateKeyWallet.Create(client: client, privateKeyHex: privateKey); +// var privateKeyWallet = await PrivateKeyWallet.Create(client: client, privateKeyHex: privateKey); +var privateKeyWallet = await PrivateKeyWallet.Generate(client: client); var walletAddress = await privateKeyWallet.GetAddress(); +Console.WriteLine($"PK Wallet address: {walletAddress}"); + +var erc20SmartWalletSepolia = await SmartWallet.Create( + personalWallet: privateKeyWallet, + chainId: 11155111, // sepolia + gasless: true, + erc20PaymasterAddress: "0xEc87d96E3F324Dcc828750b52994C6DC69C8162b", // deposit paymaster + erc20PaymasterToken: "0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8" // usdc +); +var erc20SmartWalletSepoliaAddress = await erc20SmartWalletSepolia.GetAddress(); +Console.WriteLine($"ERC20 Smart Wallet Sepolia address: {erc20SmartWalletSepoliaAddress}"); + +var selfTransfer = await ThirdwebTransaction.Create( + wallet: erc20SmartWalletSepolia, + txInput: new ThirdwebTransactionInput() { From = erc20SmartWalletSepoliaAddress, To = erc20SmartWalletSepoliaAddress, }, + chainId: 11155111 +); + +var estimateGas = await ThirdwebTransaction.EstimateGasCosts(selfTransfer); +Console.WriteLine($"Self transfer gas estimate: {estimateGas.ether}"); +Console.WriteLine("Make sure you have enough USDC!"); +Console.ReadLine(); + +var receipt = await ThirdwebTransaction.SendAndWaitForTransactionReceipt(selfTransfer); +Console.WriteLine($"Self transfer receipt: {JsonConvert.SerializeObject(receipt, Formatting.Indented)}"); + +// var chainData = await Utils.FetchThirdwebChainDataAsync(client, 421614); +// Console.WriteLine($"Chain data: {JsonConvert.SerializeObject(chainData, Formatting.Indented)}"); Console.WriteLine($"Wallet address: {walletAddress}"); // // Self transfer 0 on chain 842 @@ -139,7 +168,7 @@ // } -var inAppWallet = await InAppWallet.Create(client: client, email: "firekeeper+otpv2@thirdweb.com"); // or email: null, phoneNumber: "+1234567890" +// var inAppWallet = await InAppWallet.Create(client: client, email: "firekeeper+otpv2@thirdweb.com"); // or email: null, phoneNumber: "+1234567890" // var inAppWallet = await InAppWallet.Create(client: client, authprovider: AuthProvider.Google); // or email: null, phoneNumber: "+1234567890" @@ -165,27 +194,27 @@ // Console.WriteLine($"InAppWallet address: {address}"); // } -if (await inAppWallet.IsConnected()) -{ - Console.WriteLine($"InAppWallet address: {await inAppWallet.GetAddress()}"); - return; -} -await inAppWallet.SendOTP(); -Console.WriteLine("Please submit the OTP."); -retry: -var otp = Console.ReadLine(); -(var inAppWalletAddress, var canRetry) = await inAppWallet.SubmitOTP(otp); -if (inAppWalletAddress == null && canRetry) -{ - Console.WriteLine("Please submit the OTP again."); - goto retry; -} -if (inAppWalletAddress == null) -{ - Console.WriteLine("OTP login failed. Please try again."); - return; -} -Console.WriteLine($"InAppWallet address: {inAppWalletAddress}"); +// if (await inAppWallet.IsConnected()) +// { +// Console.WriteLine($"InAppWallet address: {await inAppWallet.GetAddress()}"); +// return; +// } +// await inAppWallet.SendOTP(); +// Console.WriteLine("Please submit the OTP."); +// retry: +// var otp = Console.ReadLine(); +// (var inAppWalletAddress, var canRetry) = await inAppWallet.SubmitOTP(otp); +// if (inAppWalletAddress == null && canRetry) +// { +// Console.WriteLine("Please submit the OTP again."); +// goto retry; +// } +// if (inAppWalletAddress == null) +// { +// Console.WriteLine("OTP login failed. Please try again."); +// return; +// } +// Console.WriteLine($"InAppWallet address: {inAppWalletAddress}"); // } // Prepare a transaction directly, or with Contract.Prepare diff --git a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs index 45fb924..f7227bd 100644 --- a/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs +++ b/Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs @@ -66,6 +66,7 @@ public static async Task Create(IThirdwebWallet wallet, Thi var address = await wallet.GetAddress().ConfigureAwait(false); txInput.From ??= address; txInput.Data ??= "0x"; + txInput.Value ??= new HexBigInteger(0); if (address != txInput.From) { diff --git a/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs b/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs index 063d628..abe4d82 100644 --- a/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs +++ b/Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs @@ -26,6 +26,12 @@ public class SmartWallet : IThirdwebWallet private BigInteger _chainId; private string _bundlerUrl; private string _paymasterUrl; + private string _erc20PaymasterAddress; + private string _erc20PaymasterToken; + private bool _isApproving; + private bool _isApproved; + + private bool UseERC20Paymaster => !string.IsNullOrEmpty(_erc20PaymasterAddress) && !string.IsNullOrEmpty(_erc20PaymasterToken); protected SmartWallet( IThirdwebWallet personalAccount, @@ -35,7 +41,9 @@ protected SmartWallet( string paymasterUrl, ThirdwebContract entryPointContract, ThirdwebContract factoryContract, - ThirdwebContract accountContract + ThirdwebContract accountContract, + string erc20PaymasterAddress, + string erc20PaymasterToken ) { Client = personalAccount.Client; @@ -48,6 +56,8 @@ ThirdwebContract accountContract _entryPointContract = entryPointContract; _factoryContract = factoryContract; _accountContract = accountContract; + _erc20PaymasterAddress = erc20PaymasterAddress; + _erc20PaymasterToken = erc20PaymasterToken; } public static async Task Create( @@ -58,7 +68,9 @@ public static async Task Create( string accountAddressOverride = null, string entryPoint = null, string bundlerUrl = null, - string paymasterUrl = null + string paymasterUrl = null, + string erc20PaymasterAddress = null, + string erc20PaymasterToken = null ) { if (!await personalWallet.IsConnected()) @@ -98,7 +110,7 @@ public static async Task Create( ); } - return new SmartWallet(personalWallet, gasless, chainId, bundlerUrl, paymasterUrl, entryPointContract, factoryContract, accountContract); + return new SmartWallet(personalWallet, gasless, chainId, bundlerUrl, paymasterUrl, entryPointContract, factoryContract, accountContract, erc20PaymasterAddress, erc20PaymasterToken); } public async Task IsDeployed() @@ -191,6 +203,31 @@ private async Task SignUserOp(ThirdwebTransactionInput transactio { requestId ??= 1; + // Approve tokens if ERC20Paymaster + if (UseERC20Paymaster && !_isApproving && !_isApproved && !simulation) + { + try + { + _isApproving = true; + var tokenContract = await ThirdwebContract.Create(Client, _erc20PaymasterToken, _chainId); + var approvedAmount = await tokenContract.ERC20_Allowance(_accountContract.Address, _erc20PaymasterAddress); + if (approvedAmount == 0) + { + _ = await tokenContract.ERC20_Approve(this, _erc20PaymasterAddress, BigInteger.Pow(2, 96) - 1); + } + _isApproved = true; + } + catch (Exception e) + { + _isApproved = false; + throw new Exception($"Approving tokens for ERC20Paymaster spending failed: {e.Message}"); + } + finally + { + _isApproving = false; + } + } + var initCode = await GetInitCode(); // Wait until deployed to avoid double initCode @@ -241,7 +278,7 @@ private async Task SignUserOp(ThirdwebTransactionInput transactio // Update paymaster data if any - partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestId, EncodeUserOperation(partialUserOp)); + partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestId, EncodeUserOperation(partialUserOp), simulation); // Estimate gas @@ -252,7 +289,7 @@ private async Task SignUserOp(ThirdwebTransactionInput transactio // Update paymaster data if any - partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestId, EncodeUserOperation(partialUserOp)); + partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestId, EncodeUserOperation(partialUserOp), simulation); // Hash, sign and encode the user operation @@ -310,9 +347,13 @@ private async Task ZkBroadcastTransaction(object transactionInput) return result.transactionHash; } - private async Task GetPaymasterAndData(object requestId, UserOperationHexified userOp) + private async Task GetPaymasterAndData(object requestId, UserOperationHexified userOp, bool simulation = false) { - if (_gasless) + if (UseERC20Paymaster && !_isApproving && !simulation) + { + return Utils.HexConcat(_erc20PaymasterAddress, _erc20PaymasterToken).HexToByteArray(); + } + else if (_gasless) { var paymasterAndData = await BundlerClient.PMSponsorUserOperation(Client, _paymasterUrl, requestId, userOp, _entryPointContract.Address); return paymasterAndData.paymasterAndData.HexToByteArray();