Skip to content

Commit

Permalink
Multi-ERC20 Paymaster Support (#45)
Browse files Browse the repository at this point in the history
Signed-off-by: Firekeeper <[email protected]>
  • Loading branch information
0xFirekeeper authored Aug 9, 2024
1 parent 0dd395e commit 7fe9a2b
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 30 deletions.
75 changes: 52 additions & 23 deletions Thirdweb.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -139,7 +168,7 @@
// }


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

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

Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions Thirdweb/Thirdweb.Transactions/ThirdwebTransaction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public static async Task<ThirdwebTransaction> 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)
{
Expand Down
55 changes: 48 additions & 7 deletions Thirdweb/Thirdweb.Wallets/SmartWallet/SmartWallet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -35,7 +41,9 @@ protected SmartWallet(
string paymasterUrl,
ThirdwebContract entryPointContract,
ThirdwebContract factoryContract,
ThirdwebContract accountContract
ThirdwebContract accountContract,
string erc20PaymasterAddress,
string erc20PaymasterToken
)
{
Client = personalAccount.Client;
Expand All @@ -48,6 +56,8 @@ ThirdwebContract accountContract
_entryPointContract = entryPointContract;
_factoryContract = factoryContract;
_accountContract = accountContract;
_erc20PaymasterAddress = erc20PaymasterAddress;
_erc20PaymasterToken = erc20PaymasterToken;
}

public static async Task<SmartWallet> Create(
Expand All @@ -58,7 +68,9 @@ public static async Task<SmartWallet> 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())
Expand Down Expand Up @@ -98,7 +110,7 @@ public static async Task<SmartWallet> 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<bool> IsDeployed()
Expand Down Expand Up @@ -191,6 +203,31 @@ private async Task<UserOperation> 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
Expand Down Expand Up @@ -241,7 +278,7 @@ private async Task<UserOperation> 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

Expand All @@ -252,7 +289,7 @@ private async Task<UserOperation> 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

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

private async Task<byte[]> GetPaymasterAndData(object requestId, UserOperationHexified userOp)
private async Task<byte[]> 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();
Expand Down

0 comments on commit 7fe9a2b

Please sign in to comment.