Skip to content

Commit 7fe9a2b

Browse files
authored
Multi-ERC20 Paymaster Support (#45)
Signed-off-by: Firekeeper <[email protected]>
1 parent 0dd395e commit 7fe9a2b

File tree

3 files changed

+101
-30
lines changed

3 files changed

+101
-30
lines changed

Thirdweb.Console/Program.cs

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,37 @@
2222
Console.WriteLine($"Contract read result: {readResult}");
2323

2424
// Create wallets (this is an advanced use case, typically one wallet is plenty)
25-
var privateKeyWallet = await PrivateKeyWallet.Create(client: client, privateKeyHex: privateKey);
25+
// var privateKeyWallet = await PrivateKeyWallet.Create(client: client, privateKeyHex: privateKey);
26+
var privateKeyWallet = await PrivateKeyWallet.Generate(client: client);
2627
var walletAddress = await privateKeyWallet.GetAddress();
28+
Console.WriteLine($"PK Wallet address: {walletAddress}");
29+
30+
var erc20SmartWalletSepolia = await SmartWallet.Create(
31+
personalWallet: privateKeyWallet,
32+
chainId: 11155111, // sepolia
33+
gasless: true,
34+
erc20PaymasterAddress: "0xEc87d96E3F324Dcc828750b52994C6DC69C8162b", // deposit paymaster
35+
erc20PaymasterToken: "0x94a9D9AC8a22534E3FaCa9F4e7F2E2cf85d5E4C8" // usdc
36+
);
37+
var erc20SmartWalletSepoliaAddress = await erc20SmartWalletSepolia.GetAddress();
38+
Console.WriteLine($"ERC20 Smart Wallet Sepolia address: {erc20SmartWalletSepoliaAddress}");
39+
40+
var selfTransfer = await ThirdwebTransaction.Create(
41+
wallet: erc20SmartWalletSepolia,
42+
txInput: new ThirdwebTransactionInput() { From = erc20SmartWalletSepoliaAddress, To = erc20SmartWalletSepoliaAddress, },
43+
chainId: 11155111
44+
);
45+
46+
var estimateGas = await ThirdwebTransaction.EstimateGasCosts(selfTransfer);
47+
Console.WriteLine($"Self transfer gas estimate: {estimateGas.ether}");
48+
Console.WriteLine("Make sure you have enough USDC!");
49+
Console.ReadLine();
50+
51+
var receipt = await ThirdwebTransaction.SendAndWaitForTransactionReceipt(selfTransfer);
52+
Console.WriteLine($"Self transfer receipt: {JsonConvert.SerializeObject(receipt, Formatting.Indented)}");
53+
54+
// var chainData = await Utils.FetchThirdwebChainDataAsync(client, 421614);
55+
// Console.WriteLine($"Chain data: {JsonConvert.SerializeObject(chainData, Formatting.Indented)}");
2756
Console.WriteLine($"Wallet address: {walletAddress}");
2857

2958
// // Self transfer 0 on chain 842
@@ -139,7 +168,7 @@
139168
// }
140169

141170

142-
var inAppWallet = await InAppWallet.Create(client: client, email: "[email protected]"); // or email: null, phoneNumber: "+1234567890"
171+
// var inAppWallet = await InAppWallet.Create(client: client, email: "[email protected]"); // or email: null, phoneNumber: "+1234567890"
143172

144173
// var inAppWallet = await InAppWallet.Create(client: client, authprovider: AuthProvider.Google); // or email: null, phoneNumber: "+1234567890"
145174

@@ -165,27 +194,27 @@
165194
// Console.WriteLine($"InAppWallet address: {address}");
166195
// }
167196

168-
if (await inAppWallet.IsConnected())
169-
{
170-
Console.WriteLine($"InAppWallet address: {await inAppWallet.GetAddress()}");
171-
return;
172-
}
173-
await inAppWallet.SendOTP();
174-
Console.WriteLine("Please submit the OTP.");
175-
retry:
176-
var otp = Console.ReadLine();
177-
(var inAppWalletAddress, var canRetry) = await inAppWallet.SubmitOTP(otp);
178-
if (inAppWalletAddress == null && canRetry)
179-
{
180-
Console.WriteLine("Please submit the OTP again.");
181-
goto retry;
182-
}
183-
if (inAppWalletAddress == null)
184-
{
185-
Console.WriteLine("OTP login failed. Please try again.");
186-
return;
187-
}
188-
Console.WriteLine($"InAppWallet address: {inAppWalletAddress}");
197+
// if (await inAppWallet.IsConnected())
198+
// {
199+
// Console.WriteLine($"InAppWallet address: {await inAppWallet.GetAddress()}");
200+
// return;
201+
// }
202+
// await inAppWallet.SendOTP();
203+
// Console.WriteLine("Please submit the OTP.");
204+
// retry:
205+
// var otp = Console.ReadLine();
206+
// (var inAppWalletAddress, var canRetry) = await inAppWallet.SubmitOTP(otp);
207+
// if (inAppWalletAddress == null && canRetry)
208+
// {
209+
// Console.WriteLine("Please submit the OTP again.");
210+
// goto retry;
211+
// }
212+
// if (inAppWalletAddress == null)
213+
// {
214+
// Console.WriteLine("OTP login failed. Please try again.");
215+
// return;
216+
// }
217+
// Console.WriteLine($"InAppWallet address: {inAppWalletAddress}");
189218
// }
190219

191220
// Prepare a transaction directly, or with Contract.Prepare

Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public static async Task<ThirdwebTransaction> Create(IThirdwebWallet wallet, Thi
6666
var address = await wallet.GetAddress().ConfigureAwait(false);
6767
txInput.From ??= address;
6868
txInput.Data ??= "0x";
69+
txInput.Value ??= new HexBigInteger(0);
6970

7071
if (address != txInput.From)
7172
{

Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ public class SmartWallet : IThirdwebWallet
2626
private BigInteger _chainId;
2727
private string _bundlerUrl;
2828
private string _paymasterUrl;
29+
private string _erc20PaymasterAddress;
30+
private string _erc20PaymasterToken;
31+
private bool _isApproving;
32+
private bool _isApproved;
33+
34+
private bool UseERC20Paymaster => !string.IsNullOrEmpty(_erc20PaymasterAddress) && !string.IsNullOrEmpty(_erc20PaymasterToken);
2935

3036
protected SmartWallet(
3137
IThirdwebWallet personalAccount,
@@ -35,7 +41,9 @@ protected SmartWallet(
3541
string paymasterUrl,
3642
ThirdwebContract entryPointContract,
3743
ThirdwebContract factoryContract,
38-
ThirdwebContract accountContract
44+
ThirdwebContract accountContract,
45+
string erc20PaymasterAddress,
46+
string erc20PaymasterToken
3947
)
4048
{
4149
Client = personalAccount.Client;
@@ -48,6 +56,8 @@ ThirdwebContract accountContract
4856
_entryPointContract = entryPointContract;
4957
_factoryContract = factoryContract;
5058
_accountContract = accountContract;
59+
_erc20PaymasterAddress = erc20PaymasterAddress;
60+
_erc20PaymasterToken = erc20PaymasterToken;
5161
}
5262

5363
public static async Task<SmartWallet> Create(
@@ -58,7 +68,9 @@ public static async Task<SmartWallet> Create(
5868
string accountAddressOverride = null,
5969
string entryPoint = null,
6070
string bundlerUrl = null,
61-
string paymasterUrl = null
71+
string paymasterUrl = null,
72+
string erc20PaymasterAddress = null,
73+
string erc20PaymasterToken = null
6274
)
6375
{
6476
if (!await personalWallet.IsConnected())
@@ -98,7 +110,7 @@ public static async Task<SmartWallet> Create(
98110
);
99111
}
100112

101-
return new SmartWallet(personalWallet, gasless, chainId, bundlerUrl, paymasterUrl, entryPointContract, factoryContract, accountContract);
113+
return new SmartWallet(personalWallet, gasless, chainId, bundlerUrl, paymasterUrl, entryPointContract, factoryContract, accountContract, erc20PaymasterAddress, erc20PaymasterToken);
102114
}
103115

104116
public async Task<bool> IsDeployed()
@@ -191,6 +203,31 @@ private async Task<UserOperation> SignUserOp(ThirdwebTransactionInput transactio
191203
{
192204
requestId ??= 1;
193205

206+
// Approve tokens if ERC20Paymaster
207+
if (UseERC20Paymaster && !_isApproving && !_isApproved && !simulation)
208+
{
209+
try
210+
{
211+
_isApproving = true;
212+
var tokenContract = await ThirdwebContract.Create(Client, _erc20PaymasterToken, _chainId);
213+
var approvedAmount = await tokenContract.ERC20_Allowance(_accountContract.Address, _erc20PaymasterAddress);
214+
if (approvedAmount == 0)
215+
{
216+
_ = await tokenContract.ERC20_Approve(this, _erc20PaymasterAddress, BigInteger.Pow(2, 96) - 1);
217+
}
218+
_isApproved = true;
219+
}
220+
catch (Exception e)
221+
{
222+
_isApproved = false;
223+
throw new Exception($"Approving tokens for ERC20Paymaster spending failed: {e.Message}");
224+
}
225+
finally
226+
{
227+
_isApproving = false;
228+
}
229+
}
230+
194231
var initCode = await GetInitCode();
195232

196233
// Wait until deployed to avoid double initCode
@@ -241,7 +278,7 @@ private async Task<UserOperation> SignUserOp(ThirdwebTransactionInput transactio
241278

242279
// Update paymaster data if any
243280

244-
partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestId, EncodeUserOperation(partialUserOp));
281+
partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestId, EncodeUserOperation(partialUserOp), simulation);
245282

246283
// Estimate gas
247284

@@ -252,7 +289,7 @@ private async Task<UserOperation> SignUserOp(ThirdwebTransactionInput transactio
252289

253290
// Update paymaster data if any
254291

255-
partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestId, EncodeUserOperation(partialUserOp));
292+
partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestId, EncodeUserOperation(partialUserOp), simulation);
256293

257294
// Hash, sign and encode the user operation
258295

@@ -310,9 +347,13 @@ private async Task<string> ZkBroadcastTransaction(object transactionInput)
310347
return result.transactionHash;
311348
}
312349

313-
private async Task<byte[]> GetPaymasterAndData(object requestId, UserOperationHexified userOp)
350+
private async Task<byte[]> GetPaymasterAndData(object requestId, UserOperationHexified userOp, bool simulation = false)
314351
{
315-
if (_gasless)
352+
if (UseERC20Paymaster && !_isApproving && !simulation)
353+
{
354+
return Utils.HexConcat(_erc20PaymasterAddress, _erc20PaymasterToken).HexToByteArray();
355+
}
356+
else if (_gasless)
316357
{
317358
var paymasterAndData = await BundlerClient.PMSponsorUserOperation(Client, _paymasterUrl, requestId, userOp, _entryPointContract.Address);
318359
return paymasterAndData.paymasterAndData.HexToByteArray();

0 commit comments

Comments
 (0)