Skip to content

Commit c85d3d7

Browse files
authored
ERC20 Paymaster Support (#141)
Signed-off-by: Firekeeper <0xFirekeeper@gmail.com>
1 parent 2c1ca1e commit c85d3d7

File tree

7 files changed

+96
-12
lines changed

7 files changed

+96
-12
lines changed

Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/BundlerClient.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@ public static class BundlerClient
1010
{
1111
// Bundler requests
1212

13-
public static async Task<EthGetUserOperationByHasResponse> EthGetUserOperationByHash(string bundlerUrl, string apiKey, object requestId, string userOpHash)
13+
public static async Task<EthGetUserOperationByHashResponse> EthGetUserOperationByHash(string bundlerUrl, string apiKey, object requestId, string userOpHash)
1414
{
1515
var response = await BundlerRequest(bundlerUrl, apiKey, requestId, "eth_getUserOperationByHash", userOpHash);
16-
return JsonConvert.DeserializeObject<EthGetUserOperationByHasResponse>(response.Result.ToString());
16+
return JsonConvert.DeserializeObject<EthGetUserOperationByHashResponse>(response.Result.ToString());
17+
}
18+
19+
public static async Task<EthGetUserOperationReceiptResponse> EthGetUserOperationReceipt(string bundlerUrl, string apiKey, object requestId, string userOpHash)
20+
{
21+
var response = await BundlerRequest(bundlerUrl, apiKey, requestId, "eth_getUserOperationReceipt", userOpHash);
22+
return JsonConvert.DeserializeObject<EthGetUserOperationReceiptResponse>(response.Result.ToString());
1723
}
1824

1925
public static async Task<string> EthSendUserOperation(string bundlerUrl, string apiKey, object requestId, UserOperationHexified userOp, string entryPoint)

Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/SmartWallet.cs

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public class SmartWallet
3838
private bool _deployed;
3939
private bool _deploying;
4040
private bool _initialized;
41+
private bool _approved;
42+
private bool _approving;
4143

4244
public List<string> Accounts { get; internal set; }
4345
public string PersonalAddress { get; internal set; }
@@ -53,6 +55,8 @@ public SmartWallet(Web3 personalWeb3, ThirdwebSDK.SmartWalletConfig config)
5355
{
5456
factoryAddress = config.factoryAddress,
5557
gasless = config.gasless,
58+
erc20PaymasterAddress = config.erc20PaymasterAddress,
59+
erc20TokenAddress = config.erc20TokenAddress,
5660
bundlerUrl = string.IsNullOrEmpty(config.bundlerUrl) ? $"https://{ThirdwebManager.Instance.SDK.session.CurrentChainData.chainName}.bundler.thirdweb.com" : config.bundlerUrl,
5761
paymasterUrl = string.IsNullOrEmpty(config.paymasterUrl) ? $"https://{ThirdwebManager.Instance.SDK.session.CurrentChainData.chainName}.bundler.thirdweb.com" : config.paymasterUrl,
5862
entryPointAddress = string.IsNullOrEmpty(config.entryPointAddress) ? Constants.DEFAULT_ENTRYPOINT_ADDRESS : config.entryPointAddress,
@@ -61,6 +65,8 @@ public SmartWallet(Web3 personalWeb3, ThirdwebSDK.SmartWalletConfig config)
6165
_deployed = false;
6266
_initialized = false;
6367
_deploying = false;
68+
_approved = false;
69+
_approving = false;
6470
}
6571

6672
internal async Task<string> GetPersonalAddress()
@@ -197,8 +203,6 @@ private async Task<RpcResponseMessage> CreateUserOpAndSend(RpcRequestMessage req
197203
var transactionInput = JsonConvert.DeserializeObject<TransactionInput>(JsonConvert.SerializeObject(paramList[0]));
198204
var dummySig = Constants.DUMMY_SIG;
199205

200-
var (initCode, gas) = await GetInitCode();
201-
202206
var executeFn = new AccountContract.ExecuteFunction
203207
{
204208
Target = transactionInput.To,
@@ -208,8 +212,37 @@ private async Task<RpcResponseMessage> CreateUserOpAndSend(RpcRequestMessage req
208212
};
209213
var executeInput = executeFn.CreateTransactionInput(Accounts[0]);
210214

215+
// Approve ERC20 tokens if any
216+
217+
if (!string.IsNullOrEmpty(Config.erc20PaymasterAddress) && !_approved && !_approving)
218+
{
219+
try
220+
{
221+
_approving = true;
222+
var tokenContract = ThirdwebManager.Instance.SDK.GetContract(Config.erc20TokenAddress);
223+
var approvedAmount = await tokenContract.ERC20.AllowanceOf(Accounts[0], Config.erc20PaymasterAddress);
224+
if (BigInteger.Parse(approvedAmount.value) == 0)
225+
{
226+
ThirdwebDebug.Log($"Approving tokens for ERC20Paymaster spending");
227+
_deploying = false;
228+
await tokenContract.ERC20.SetAllowance(Config.erc20PaymasterAddress, (BigInteger.Pow(2, 96) - 1).ToString().ToEth());
229+
}
230+
_approved = true;
231+
_approving = false;
232+
await UpdateDeploymentStatus();
233+
}
234+
catch (Exception e)
235+
{
236+
_approving = false;
237+
_approved = false;
238+
throw new Exception($"Approving tokens for ERC20Paymaster spending failed: {e.Message}");
239+
}
240+
}
241+
211242
// Create the user operation and its safe (hexified) version
212243

244+
var (initCode, gas) = await GetInitCode();
245+
213246
var gasPrices = await Utils.GetGasPriceAsync(ThirdwebManager.Instance.SDK.session.ChainId);
214247

215248
var partialUserOp = new EntryPointContract.UserOperation()
@@ -235,7 +268,9 @@ private async Task<RpcResponseMessage> CreateUserOpAndSend(RpcRequestMessage req
235268

236269
var gasEstimates = await BundlerClient.EthEstimateUserOperationGas(Config.bundlerUrl, apiKey, requestMessage.Id, partialUserOp.EncodeUserOperation(), Config.entryPointAddress);
237270
partialUserOp.CallGasLimit = 50000 + new HexBigInteger(gasEstimates.CallGasLimit).Value;
238-
partialUserOp.VerificationGasLimit = new HexBigInteger(gasEstimates.VerificationGas).Value;
271+
partialUserOp.VerificationGasLimit = string.IsNullOrEmpty(Config.erc20PaymasterAddress)
272+
? new HexBigInteger(gasEstimates.VerificationGas).Value
273+
: new HexBigInteger(gasEstimates.VerificationGas).Value * 3;
239274
partialUserOp.PreVerificationGas = new HexBigInteger(gasEstimates.PreVerificationGas).Value;
240275

241276
// Update paymaster data if any
@@ -258,8 +293,8 @@ private async Task<RpcResponseMessage> CreateUserOpAndSend(RpcRequestMessage req
258293
string txHash = null;
259294
while (txHash == null)
260295
{
261-
var getUserOpResponse = await BundlerClient.EthGetUserOperationByHash(Config.bundlerUrl, apiKey, requestMessage.Id, userOpHash);
262-
txHash = getUserOpResponse?.transactionHash;
296+
var userOpReceipt = await BundlerClient.EthGetUserOperationReceipt(Config.bundlerUrl, apiKey, requestMessage.Id, userOpHash);
297+
txHash = userOpReceipt?.receipt?.TransactionHash;
263298
await new WaitForSecondsRealtime(1f);
264299
}
265300
ThirdwebDebug.Log("Tx Hash: " + txHash);
@@ -297,9 +332,19 @@ private async Task<BigInteger> GetNonce()
297332

298333
private async Task<byte[]> GetPaymasterAndData(object requestId, UserOperationHexified userOp, string apiKey)
299334
{
300-
return Config.gasless
301-
? (await BundlerClient.PMSponsorUserOperation(Config.paymasterUrl, apiKey, requestId, userOp, Config.entryPointAddress)).paymasterAndData.HexStringToByteArray()
302-
: new byte[] { };
335+
if (!string.IsNullOrEmpty(Config.erc20PaymasterAddress) && !_approving)
336+
{
337+
return Config.erc20PaymasterAddress.HexToByteArray();
338+
}
339+
else if (Config.gasless)
340+
{
341+
var paymasterAndData = await BundlerClient.PMSponsorUserOperation(Config.paymasterUrl, apiKey, requestId, userOp, Config.entryPointAddress);
342+
return paymasterAndData.paymasterAndData.HexToByteArray();
343+
}
344+
else
345+
{
346+
return new byte[] { };
347+
}
303348
}
304349
}
305350
}

Assets/Thirdweb/Core/Scripts/AccountAbstraction/Core/Types.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Nethereum.RPC.Eth.DTOs;
2+
13
namespace Thirdweb.AccountAbstraction
24
{
35
public class EthEstimateUserOperationGasResponse
@@ -7,14 +9,19 @@ public class EthEstimateUserOperationGasResponse
79
public string CallGasLimit { get; set; }
810
}
911

10-
public class EthGetUserOperationByHasResponse
12+
public class EthGetUserOperationByHashResponse
1113
{
1214
public string entryPoint { get; set; }
1315
public string transactionHash { get; set; }
1416
public string blockHash { get; set; }
1517
public string blockNumber { get; set; }
1618
}
1719

20+
public class EthGetUserOperationReceiptResponse
21+
{
22+
public TransactionReceipt receipt { get; set; }
23+
}
24+
1825
public class EntryPointWrapper
1926
{
2027
public string entryPoint { get; set; }

Assets/Thirdweb/Core/Scripts/ThirdwebManager.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ public class ThirdwebManager : MonoBehaviour
9999
[Tooltip("Whether it should use a paymaster for gasless transactions or not")]
100100
public bool gasless;
101101

102+
[Tooltip("Optional - If you want to use a custom erc20 paymaster, you can provide the contract address here")]
103+
public string erc20PaymasterAddress;
104+
105+
[Tooltip("Optional - If you want to use a custom erc20 token for your paymaster, you can provide the contract address here")]
106+
public string erc20TokenAddress;
107+
102108
[Tooltip("Optional - If you want to use a custom relayer, you can provide the URL here")]
103109
public string bundlerUrl;
104110

@@ -281,6 +287,8 @@ public void Initialize(string chainIdentifier)
281287
{
282288
factoryAddress = factoryAddress,
283289
gasless = gasless,
290+
erc20PaymasterAddress = string.IsNullOrEmpty(erc20PaymasterAddress) ? null : erc20PaymasterAddress,
291+
erc20TokenAddress = string.IsNullOrEmpty(erc20TokenAddress) ? null : erc20TokenAddress,
284292
bundlerUrl = string.IsNullOrEmpty(bundlerUrl) ? $"https://{activeChainId}.bundler.thirdweb.com" : bundlerUrl,
285293
paymasterUrl = string.IsNullOrEmpty(paymasterUrl) ? $"https://{activeChainId}.bundler.thirdweb.com" : paymasterUrl,
286294
entryPointAddress = string.IsNullOrEmpty(entryPointAddress) ? Thirdweb.AccountAbstraction.Constants.DEFAULT_ENTRYPOINT_ADDRESS : entryPointAddress,

Assets/Thirdweb/Core/Scripts/ThirdwebSDK.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ public struct SmartWalletConfig
112112
/// </summary>
113113
public bool gasless;
114114

115+
/// <summary>
116+
/// The address of your ERC20 paymaster contract if used.
117+
/// </summary>
118+
public string erc20PaymasterAddress;
119+
120+
/// <summary>
121+
/// The address of your ERC20 token if using ERC20 paymaster.
122+
/// </summary>
123+
public string erc20TokenAddress;
124+
115125
/// <summary>
116126
/// The URL of the bundler service.
117127
/// </summary>

Assets/Thirdweb/Core/Scripts/Utils.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ public async static Task<GasPriceParameters> GetGasPriceAsync(BigInteger chainId
524524
chainId == 1 // mainnet
525525
|| chainId == 11155111 // sepolia
526526
|| chainId == 42161 // arbitrum
527-
|| chainId == 421613 // arbitrum goerli
527+
|| chainId == 421614 // arbitrum sepolia
528528
|| chainId == 534352 // scroll
529529
|| chainId == 534351 // scroll sepolia
530530
|| chainId == 5000 // mantle
@@ -534,6 +534,8 @@ public async static Task<GasPriceParameters> GetGasPriceAsync(BigInteger chainId
534534
|| chainId == 44787 // celo alfajores
535535
|| chainId == 43114 // avalanche
536536
|| chainId == 43113 // avalanche fuji
537+
|| chainId == 8453 // base
538+
|| chainId == 84532 // base sepolia
537539
)
538540
{
539541
gasPrice = BigInteger.Multiply(gasPrice, 10) / 9;

Assets/Thirdweb/Editor/ThirdwebManagerEditor.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public class ThirdwebManagerEditor : Editor
2727
private SerializedProperty walletConnectExplorerRecommendedWalletIdsProperty;
2828
private SerializedProperty factoryAddressProperty;
2929
private SerializedProperty gaslessProperty;
30+
private SerializedProperty erc20PaymasterAddressProperty;
31+
private SerializedProperty erc20TokenAddressProperty;
3032
private SerializedProperty bundlerUrlProperty;
3133
private SerializedProperty paymasterUrlProperty;
3234
private SerializedProperty entryPointAddressProperty;
@@ -67,6 +69,8 @@ private void OnEnable()
6769
walletConnectExplorerRecommendedWalletIdsProperty = serializedObject.FindProperty("walletConnectExplorerRecommendedWalletIds");
6870
factoryAddressProperty = serializedObject.FindProperty("factoryAddress");
6971
gaslessProperty = serializedObject.FindProperty("gasless");
72+
erc20PaymasterAddressProperty = serializedObject.FindProperty("erc20PaymasterAddress");
73+
erc20TokenAddressProperty = serializedObject.FindProperty("erc20TokenAddress");
7074
bundlerUrlProperty = serializedObject.FindProperty("bundlerUrl");
7175
paymasterUrlProperty = serializedObject.FindProperty("paymasterUrl");
7276
entryPointAddressProperty = serializedObject.FindProperty("entryPointAddress");
@@ -304,6 +308,8 @@ public override void OnInspectorGUI()
304308

305309
if (showSmartWalletOptionalFields)
306310
{
311+
EditorGUILayout.PropertyField(erc20PaymasterAddressProperty);
312+
EditorGUILayout.PropertyField(erc20TokenAddressProperty);
307313
EditorGUILayout.PropertyField(bundlerUrlProperty);
308314
EditorGUILayout.PropertyField(paymasterUrlProperty);
309315
EditorGUILayout.PropertyField(entryPointAddressProperty);

0 commit comments

Comments
 (0)