Skip to content

Commit d543a60

Browse files
authored
[AA] Use bundler estimations, implement custom gas pricing (#140)
1 parent 402c86a commit d543a60

File tree

7 files changed

+174
-100
lines changed

7 files changed

+174
-100
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public static async Task<PMSponsorOperationResponse> PMSponsorUserOperation(stri
4141
private static async Task<RpcResponseMessage> BundlerRequest(string url, string apiKey, object requestId, string method, params object[] args)
4242
{
4343
using HttpClient client = new HttpClient();
44-
ThirdwebDebug.Log($"Bundler Request: {method}({string.Join(", ", args)})");
44+
ThirdwebDebug.Log($"Bundler Request: {method}({JsonConvert.SerializeObject(args)}");
4545
var requestMessage = new RpcRequestMessage(requestId, method, args);
4646
string requestMessageJson = JsonConvert.SerializeObject(requestMessage);
4747

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ namespace Thirdweb.AccountAbstraction
33
public static class Constants
44
{
55
public const string DEFAULT_ENTRYPOINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; // v0.6
6-
public const int DUMMY_SIG_LENGTH = 65;
6+
public const string DUMMY_SIG = "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c";
77
public const string DUMMY_PAYMASTER_AND_DATA_HEX =
88
"0x0101010101010101010101010101010101010101000000000000000000000000000000000000000000000000000001010101010100000000000000000000000000000000000000000000000000000000000000000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101";
99
}

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

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,7 @@ private async Task<RpcResponseMessage> CreateUserOpAndSend(RpcRequestMessage req
189189

190190
var paramList = JsonConvert.DeserializeObject<List<object>>(JsonConvert.SerializeObject(requestMessage.RawParameters));
191191
var transactionInput = JsonConvert.DeserializeObject<TransactionInput>(JsonConvert.SerializeObject(paramList[0]));
192-
var latestBlock = await Blocks.GetBlock(await Blocks.GetLatestBlockNumber());
193-
var dummySig = new byte[Constants.DUMMY_SIG_LENGTH];
194-
for (int i = 0; i < Constants.DUMMY_SIG_LENGTH; i++)
195-
dummySig[i] = 0x01;
192+
var dummySig = Constants.DUMMY_SIG;
196193

197194
var (initCode, gas) = await GetInitCode();
198195

@@ -207,37 +204,47 @@ private async Task<RpcResponseMessage> CreateUserOpAndSend(RpcRequestMessage req
207204

208205
// Create the user operation and its safe (hexified) version
209206

207+
var gasPrices = await Utils.GetGasPriceAsync(ThirdwebManager.Instance.SDK.session.ChainId);
208+
210209
var partialUserOp = new EntryPointContract.UserOperation()
211210
{
212211
Sender = Accounts[0],
213212
Nonce = await GetNonce(),
214213
InitCode = initCode,
215214
CallData = executeInput.Data.HexStringToByteArray(),
216-
CallGasLimit = 50000 + (transactionInput.Gas != null ? (transactionInput.Gas.Value < 21000 ? 100000 : transactionInput.Gas.Value) : 100000),
217-
VerificationGasLimit = 100000 + gas,
218-
PreVerificationGas = 21000,
219-
MaxFeePerGas = latestBlock.BaseFeePerGas.Value * 2 + BigInteger.Parse("1500000000"),
220-
MaxPriorityFeePerGas = BigInteger.Parse("1500000000"),
221-
PaymasterAndData = Constants.DUMMY_PAYMASTER_AND_DATA_HEX.HexStringToByteArray(),
222-
Signature = dummySig,
215+
CallGasLimit = 0,
216+
VerificationGasLimit = 0,
217+
PreVerificationGas = 0,
218+
MaxFeePerGas = gasPrices.MaxFeePerGas,
219+
MaxPriorityFeePerGas = gasPrices.MaxPriorityFeePerGas,
220+
PaymasterAndData = new byte[] { },
221+
Signature = dummySig.HexStringToByteArray(),
223222
};
224-
partialUserOp.PreVerificationGas = partialUserOp.CalcPreVerificationGas();
225-
var partialUserOpHexified = partialUserOp.EncodeUserOperation();
226223

227224
// Update paymaster data if any
228225

229-
partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestMessage.Id, partialUserOpHexified, apiKey);
226+
partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestMessage.Id, partialUserOp.EncodeUserOperation(), apiKey);
227+
228+
// Estimate gas
229+
230+
var gasEstimates = await BundlerClient.EthEstimateUserOperationGas(Config.bundlerUrl, apiKey, requestMessage.Id, partialUserOp.EncodeUserOperation(), Config.entryPointAddress);
231+
partialUserOp.CallGasLimit = 50000 + new HexBigInteger(gasEstimates.CallGasLimit).Value;
232+
partialUserOp.VerificationGasLimit = new HexBigInteger(gasEstimates.VerificationGas).Value;
233+
partialUserOp.PreVerificationGas = new HexBigInteger(gasEstimates.PreVerificationGas).Value;
234+
235+
// Update paymaster data if any
236+
237+
partialUserOp.PaymasterAndData = await GetPaymasterAndData(requestMessage.Id, partialUserOp.EncodeUserOperation(), apiKey);
230238

231239
// Hash, sign and encode the user operation
232240

233241
partialUserOp.Signature = await partialUserOp.HashAndSignUserOp(Config.entryPointAddress);
234-
partialUserOpHexified = partialUserOp.EncodeUserOperation();
235242

236243
// Send the user operation
237244

238245
ThirdwebDebug.Log("Valid UserOp: " + JsonConvert.SerializeObject(partialUserOp));
239-
ThirdwebDebug.Log("Valid Encoded UserOp: " + JsonConvert.SerializeObject(partialUserOpHexified));
240-
var userOpHash = await BundlerClient.EthSendUserOperation(Config.bundlerUrl, apiKey, requestMessage.Id, partialUserOpHexified, Config.entryPointAddress);
246+
ThirdwebDebug.Log("Valid Encoded UserOp: " + JsonConvert.SerializeObject(partialUserOp.EncodeUserOperation()));
247+
var userOpHash = await BundlerClient.EthSendUserOperation(Config.bundlerUrl, apiKey, requestMessage.Id, partialUserOp.EncodeUserOperation(), Config.entryPointAddress);
241248
ThirdwebDebug.Log("UserOp Hash: " + userOpHash);
242249

243250
// Wait for the transaction to be mined

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

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -64,75 +64,5 @@ public static UserOperationHexified EncodeUserOperation(this EntryPointContract.
6464
signature = userOperation.Signature.ByteArrayToHexString()
6565
};
6666
}
67-
68-
public static int CalcPreVerificationGas(this EntryPointContract.UserOperation userOp, GasOverheads gasOverheads = null)
69-
{
70-
GasOverheads ov = gasOverheads ?? new GasOverheads();
71-
72-
userOp.Signature = new byte[ov.sigSize];
73-
userOp.PreVerificationGas = 21000;
74-
75-
byte[] packed = userOp.PackUserOp(false);
76-
int lengthInWord = (packed.Length + 31) / 32;
77-
78-
int callDataCost = 0;
79-
foreach (byte b in packed)
80-
{
81-
callDataCost += (b == 0) ? ov.zeroByte : ov.nonZeroByte;
82-
}
83-
84-
int totalGas = callDataCost + ov.fixedGas / ov.bundleSize + ov.perUserOp + ov.perUserOpWord * lengthInWord;
85-
86-
return totalGas;
87-
}
88-
89-
public static byte[] PackUserOp(this EntryPointContract.UserOperation op, bool forSignature = true)
90-
{
91-
var abiEncode = new Nethereum.ABI.ABIEncode();
92-
var sha3Keccak = new Nethereum.Util.Sha3Keccack();
93-
94-
if (forSignature)
95-
{
96-
return abiEncode.GetABIEncoded(
97-
op.Sender,
98-
op.Nonce,
99-
sha3Keccak.CalculateHash(op.InitCode),
100-
sha3Keccak.CalculateHash(op.CallData),
101-
op.CallGasLimit,
102-
op.VerificationGasLimit,
103-
op.PreVerificationGas,
104-
op.MaxFeePerGas,
105-
op.MaxPriorityFeePerGas,
106-
sha3Keccak.CalculateHash(op.PaymasterAndData)
107-
);
108-
}
109-
else
110-
{
111-
return abiEncode.GetABIEncodedPacked(
112-
op.Sender,
113-
op.Nonce,
114-
op.InitCode,
115-
op.CallData,
116-
op.CallGasLimit,
117-
op.VerificationGasLimit,
118-
op.PreVerificationGas,
119-
op.MaxFeePerGas,
120-
op.MaxPriorityFeePerGas,
121-
op.PaymasterAndData,
122-
op.Signature
123-
);
124-
}
125-
}
126-
}
127-
128-
public class GasOverheads
129-
{
130-
public int fixedGas = 21000;
131-
public int perUserOp = 18300;
132-
public int perUserOpWord = 4;
133-
public int zeroByte = 4;
134-
public int nonZeroByte = 16;
135-
public int bundleSize = 1;
136-
public int sigSize = Constants.DUMMY_SIG_LENGTH;
13767
}
13868
}

Assets/Thirdweb/Core/Scripts/ThirdwebSession.cs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,24 @@ internal async Task<string> Connect(WalletConnection walletConnection)
6363
ActiveWallet = new ThirdwebMetamask();
6464
break;
6565
case WalletProvider.SmartWallet:
66-
await Connect(
67-
new WalletConnection(
68-
provider: walletConnection.personalWallet,
69-
chainId: walletConnection.chainId,
70-
password: walletConnection.password,
71-
email: walletConnection.email,
72-
authOptions: walletConnection.authOptions
73-
)
74-
);
7566
if (Options.smartWalletConfig == null)
7667
throw new UnityException("Smart wallet config is required for smart wallet connection method!");
68+
if (ActiveWallet?.GetProvider() != walletConnection.personalWallet)
69+
{
70+
await Connect(
71+
new WalletConnection(
72+
provider: walletConnection.personalWallet,
73+
chainId: walletConnection.chainId,
74+
password: walletConnection.password,
75+
email: walletConnection.email,
76+
authOptions: walletConnection.authOptions
77+
)
78+
);
79+
}
80+
else
81+
{
82+
ThirdwebDebug.Log("Already connected to personal wallet, skipping connection.");
83+
}
7784
ActiveWallet = new ThirdwebSmartWallet(ActiveWallet, Options.smartWalletConfig.Value);
7885
break;
7986
case WalletProvider.Hyperplay:

Assets/Thirdweb/Core/Scripts/Types.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,4 +561,35 @@ public override readonly string ToString()
561561
+ $"\n>approvedCallTargets: {approvedCallTargets}";
562562
}
563563
}
564+
565+
[System.Serializable]
566+
public class GasPriceParameters
567+
{
568+
public BigInteger MaxFeePerGas { get; private set; }
569+
public BigInteger MaxPriorityFeePerGas { get; private set; }
570+
571+
public GasPriceParameters(BigInteger maxFeePerGas, BigInteger maxPriorityFeePerGas)
572+
{
573+
MaxFeePerGas = maxFeePerGas;
574+
MaxPriorityFeePerGas = maxPriorityFeePerGas;
575+
}
576+
}
577+
578+
[System.Serializable]
579+
public class PolygonGasStationResult
580+
{
581+
public GasStationResult safeLow;
582+
public GasStationResult standard;
583+
public GasStationResult fast;
584+
public BigInteger estimatedBaseFee;
585+
public int blockTime;
586+
public BigInteger blockNumber;
587+
}
588+
589+
[System.Serializable]
590+
public class GasStationResult
591+
{
592+
public double maxPriorityFee;
593+
public double maxFee;
594+
}
564595
}

Assets/Thirdweb/Core/Scripts/Utils.cs

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
using Nethereum.Web3;
1313
using Newtonsoft.Json.Linq;
1414
using System.Globalization;
15+
using System.Linq;
16+
using System.Net.Http;
1517

1618
namespace Thirdweb
1719
{
@@ -409,9 +411,9 @@ public static string AppendBundleIdQueryParam(this string uri)
409411
return uri;
410412
}
411413

412-
public static Web3 GetWeb3()
414+
public static Web3 GetWeb3(BigInteger? chainId = null)
413415
{
414-
return new Web3(new ThirdwebClient(new Uri(ThirdwebManager.Instance.SDK.session.RPC)));
416+
return new Web3(new ThirdwebClient(new Uri(chainId == null ? ThirdwebManager.Instance.SDK.session.RPC : $"https://{chainId}.rpc.thirdweb.com")));
415417
}
416418

417419
public static string GetNativeTokenWrapper(BigInteger chainId)
@@ -465,5 +467,102 @@ public static string JSDateToUnixTimestamp(string dateString)
465467
long unixTimestamp = (long)(dateTime - unixEpoch).TotalSeconds;
466468
return unixTimestamp.ToString();
467469
}
470+
471+
public async static Task<GasPriceParameters> GetGasPriceAsync(BigInteger chainId)
472+
{
473+
if (chainId == 137 || chainId == 80001)
474+
{
475+
return await GetPolygonGasPriceParameters((int)chainId);
476+
}
477+
478+
var web3 = GetWeb3(chainId);
479+
var gasPrice = (await web3.Eth.GasPrice.SendRequestAsync()).Value;
480+
481+
if (chainId == 42220) // celo mainnet
482+
{
483+
gasPrice = BigInteger.Multiply(gasPrice, 3) / 2;
484+
return new GasPriceParameters(gasPrice, gasPrice);
485+
}
486+
487+
if (
488+
chainId == 1 // mainnet
489+
|| chainId == 11155111 // sepolia
490+
|| chainId == 42161 // arbitrum
491+
|| chainId == 421613 // arbitrum goerli
492+
|| chainId == 534352 // scroll
493+
|| chainId == 534351 // scroll sepolia
494+
|| chainId == 5000 // mantle
495+
|| chainId == 22222 // nautilus
496+
|| chainId == 8453 // base
497+
|| chainId == 53935 // dfk
498+
|| chainId == 44787 // celo alfajores
499+
|| chainId == 43114 // avalanche
500+
|| chainId == 43113 // avalanche fuji
501+
)
502+
{
503+
gasPrice = BigInteger.Multiply(gasPrice, 10) / 9;
504+
return new GasPriceParameters(gasPrice, gasPrice);
505+
}
506+
507+
var maxPriorityFeePerGas = new BigInteger(2000000000) > gasPrice ? gasPrice : new BigInteger(2000000000);
508+
509+
var feeHistory = await web3.Eth.FeeHistory.SendRequestAsync(new Nethereum.Hex.HexTypes.HexBigInteger(20), Nethereum.RPC.Eth.DTOs.BlockParameter.CreateLatest(), new double[] { 20 });
510+
511+
if (feeHistory.Reward == null)
512+
{
513+
gasPrice = BigInteger.Multiply(gasPrice, 3) / 2;
514+
maxPriorityFeePerGas = gasPrice;
515+
}
516+
else
517+
{
518+
var feeAverage = feeHistory.Reward.Select(r => r[0]).Aggregate(BigInteger.Zero, (acc, cur) => cur + acc) / 10;
519+
if (feeAverage > gasPrice)
520+
{
521+
gasPrice = feeAverage;
522+
}
523+
maxPriorityFeePerGas = gasPrice;
524+
}
525+
526+
return new GasPriceParameters(gasPrice, maxPriorityFeePerGas);
527+
}
528+
529+
public async static Task<GasPriceParameters> GetPolygonGasPriceParameters(int chainId)
530+
{
531+
using var httpClient = new HttpClient();
532+
string gasStationUrl;
533+
BigInteger minGasPrice = 1;
534+
switch (chainId)
535+
{
536+
case 137:
537+
gasStationUrl = "https://gasstation.polygon.technology/v2";
538+
minGasPrice = 31;
539+
break;
540+
case 80001:
541+
gasStationUrl = "https://gasstation-testnet.polygon.technology/v2";
542+
minGasPrice = 1;
543+
break;
544+
default:
545+
throw new UnityException("Unsupported chain id");
546+
}
547+
try
548+
{
549+
var response = await httpClient.GetAsync(gasStationUrl);
550+
response.EnsureSuccessStatusCode();
551+
string responseBody = await response.Content.ReadAsStringAsync();
552+
var data = JsonConvert.DeserializeObject<PolygonGasStationResult>(responseBody);
553+
return new GasPriceParameters(GweiToWei(data.fast.maxFee), GweiToWei(data.fast.maxPriorityFee));
554+
}
555+
catch (Exception e)
556+
{
557+
ThirdwebDebug.LogWarning($"Failed to get gas price from Polygon gas station, using default: {e.Message}");
558+
}
559+
var gasPrice = await GetWeb3().Eth.GasPrice.SendRequestAsync();
560+
return new GasPriceParameters(gasPrice, minGasPrice);
561+
}
562+
563+
public static BigInteger GweiToWei(double gweiAmount)
564+
{
565+
return new BigInteger(gweiAmount * 1e9);
566+
}
468567
}
469568
}

0 commit comments

Comments
 (0)