Skip to content

Commit

Permalink
refactor: only estimate gas once for single uo
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewwahid authored and Sednaoui committed May 15, 2023
1 parent 93f278b commit 238a1d2
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 87 deletions.
81 changes: 38 additions & 43 deletions lib/models/batch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,24 @@ class Batch {
Account account;
Network network;
//
FeeToken? _feeToken;
GasEstimate? gasEstimate;
FeeToken? _selectedFeeToken;
late PaymasterResponse _paymasterResponse;
List<GnosisTransaction> transactions = [];

bool get includesPaymaster => _feeToken != null && _feeToken?.token.symbol != network.nativeCurrency && _feeToken?.token.address != Constants.addressZeroHex;
FeeToken? get feeCurrency => _feeToken;
PaymasterResponse get paymasterResponse => _paymasterResponse;
//
Batch({required this.account, required this.network});
//
PaymasterResponse get paymasterResponse => _paymasterResponse;
FeeToken? get selectedFeeToken => _selectedFeeToken;
bool get includesPaymaster => _selectedFeeToken != null && _selectedFeeToken?.token.symbol != network.nativeCurrency && _selectedFeeToken?.token.address != Constants.addressZeroHex;

bool _includesPaymaster(FeeToken? feeToken) => feeToken != null && feeToken.token.symbol != network.nativeCurrency && feeToken.token.address != Constants.addressZeroHex;

GnosisTransaction? getById(String id){
return transactions.firstWhereOrNull((e) => e.id == id);
}

void setSelectedFeeToken(FeeToken? feeCurrency) => _feeToken = feeCurrency;
void setSelectedFeeToken(FeeToken? feeToken) => _selectedFeeToken = feeToken;

Future<bool> fetchPaymasterResponse() async {
PaymasterResponse? paymasterResponse = await Paymaster.fetchPaymasterFees(network.chainId.toInt());
Expand All @@ -58,40 +61,32 @@ class Batch {


Future<void> _adjustFeeCurrencyCosts() async{
List<Future<UserOperation>> _userOpsTemp = [];
Map<FeeToken, UserOperation> _userOps = {};
for (FeeToken feeCurrency in _paymasterResponse.tokens){
bool isEther = feeCurrency.token.symbol == network.nativeCurrency && feeCurrency.token.address == Constants.addressZeroHex;
_userOpsTemp.add(
toUserOperation(
BigInt.from(PersistentData.accountStatus.nonce),
proxyDeployed: PersistentData.accountStatus.proxyDeployed,
skipPaymasterData: true,
overrideFee: isEther ? null : feeCurrency,
).then((op){
_userOps[feeCurrency] = op;
BigInt maxCost = FeeCurrencyUtils.calculateFee(_userOps[feeCurrency]!, feeCurrency.exchangeRate, isEther);
if (!isEther){
maxCost = maxCost.scale(1.05); // todo check
}
feeCurrency.fee = maxCost;
return op;
})
for (FeeToken feeToken in _paymasterResponse.tokens){
bool isEther = feeToken.token.symbol == network.nativeCurrency && feeToken.token.address == Constants.addressZeroHex;
UserOperation op = await toUserOperation(
BigInt.from(PersistentData.accountStatus.nonce),
proxyDeployed: PersistentData.accountStatus.proxyDeployed,
skipPaymasterData: true,
feeToken: feeToken,
);
BigInt maxCost = FeeCurrencyUtils.calculateFee(op, feeToken.exchangeRate, isEther);
if (!isEther){
maxCost = maxCost.scale(1.05); // todo check
}
feeToken.fee = maxCost;
}
await Future.wait(_userOpsTemp);
}

String getFeeToken(){
return feeCurrency?.token.symbol ?? "ETH";
return selectedFeeToken?.token.symbol ?? "ETH";
}

BigInt getFee(){
return feeCurrency?.fee ?? BigInt.zero;
return selectedFeeToken?.fee ?? BigInt.zero;
}

Future<void> _addPaymasterToUserOp(UserOperation userOp, int chainId) async {
String? paymasterData = await Paymaster.getPaymasterData(userOp, feeCurrency!.token.address, chainId);
String? paymasterData = await Paymaster.getPaymasterData(userOp, selectedFeeToken!.token.address, chainId);
if (paymasterData == null){ // todo network: handle fetching errors
userOp.paymasterAndData = "0x";
}else{
Expand All @@ -100,9 +95,7 @@ class Batch {
}
}

void _addPaymasterToTransaction(GnosisTransaction transaction, [FeeToken? feeToken]){
feeToken ??= feeCurrency;
if (feeToken == null) return;
void _addPaymasterToTransaction(GnosisTransaction transaction, FeeToken feeToken){
transaction.paymaster = _paymasterResponse.paymasterData.paymaster;
transaction.approveToken = EthereumAddress.fromHex(feeToken.token.address);
transaction.approveAmount = feeToken.fee;
Expand Down Expand Up @@ -139,7 +132,8 @@ class Batch {
return transaction;
}

Future<UserOperation> toUserOperation(BigInt nonce, {bool proxyDeployed=true, bool skipPaymasterData=false, FeeToken? overrideFee}) async {
Future<UserOperation> toUserOperation(BigInt nonce, {bool proxyDeployed=true, bool skipPaymasterData=false, FeeToken? feeToken}) async {
feeToken ??= _selectedFeeToken;
//
String initCode = "0x";
if (!proxyDeployed){
Expand All @@ -155,8 +149,8 @@ class Batch {
GnosisTransaction? multiSendTransaction = _getMultiSendTransaction();
String callData = "0x";
if (multiSendTransaction != null){
if (includesPaymaster || overrideFee != null){
_addPaymasterToTransaction(multiSendTransaction, overrideFee);
if (_includesPaymaster(feeToken)){
_addPaymasterToTransaction(multiSendTransaction, feeToken!);
}
callData = multiSendTransaction.toCallData();
}
Expand All @@ -167,22 +161,23 @@ class Batch {
callData: callData,
);
//
GasEstimate? gasEstimates = await network.gasEstimator.getGasEstimates(userOp, includesPaymaster: includesPaymaster || overrideFee != null); // todo enable, // todo handle null gas estimates (calldata error, or network error)
GasEstimate? _gasEstimate = await network.gasEstimator.getGasEstimates(userOp, prevEstimate: gasEstimate, includesPaymaster: _includesPaymaster(feeToken)); // todo enable, // todo handle null gas estimates (calldata error, or network error)
gasEstimate ??= _gasEstimate;
//
userOp.callGasLimit = gasEstimates!.callGasLimit.scale(1.25);
userOp.preVerificationGas = gasEstimates.preVerificationGas;
userOp.verificationGasLimit = gasEstimates.verificationGasLimit.scale(2);
userOp.maxFeePerGas = gasEstimates.maxFeePerGas;
userOp.maxPriorityFeePerGas = gasEstimates.maxPriorityFeePerGas;
userOp.callGasLimit = _gasEstimate!.callGasLimit.scale(1.25);
userOp.preVerificationGas = _gasEstimate.preVerificationGas;
userOp.verificationGasLimit = _gasEstimate.verificationGasLimit.scale(2);
userOp.maxFeePerGas = _gasEstimate.maxFeePerGas;
userOp.maxPriorityFeePerGas = _gasEstimate.maxPriorityFeePerGas;
if (userOp.initCode != "0x"){
userOp.verificationGasLimit += BigInt.from(350000); // higher than normal for deployment
userOp.callGasLimit += multiSendTransaction?.suggestedGasLimit ?? userOp.callGasLimit; // todo remove when first simulateHandleOp is implemented
}
// userOp.callGasLimit += multiSendTransaction?.suggestedGasLimit.toInt() ?? 0; // todo check
if (includesPaymaster || overrideFee != null){
if (_includesPaymaster(feeToken)){
userOp.verificationGasLimit += BigInt.from(35000);
}
if (includesPaymaster && !skipPaymasterData){
if (_includesPaymaster(feeToken) && !skipPaymasterData){
await _addPaymasterToUserOp(userOp, account.chainId);
}
//
Expand Down
11 changes: 9 additions & 2 deletions lib/models/gas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@ class GasEstimate {
BigInt verificationGasLimit;
BigInt maxPriorityFeePerGas;
BigInt maxFeePerGas;
late BigInt l1GasUsed; //specific for L2s
late BigInt l1BaseFee; //specific for L2s

GasEstimate(
{required this.callGasLimit,
required this.preVerificationGas,
required this.verificationGasLimit,
required this.maxPriorityFeePerGas,
required this.maxFeePerGas});
}
required this.maxFeePerGas,
BigInt? l1GasUsed,
BigInt? l1BaseFee}) {
this.l1GasUsed = l1GasUsed ?? BigInt.zero;
this.l1BaseFee = l1BaseFee ?? BigInt.zero;
}
}
2 changes: 1 addition & 1 deletion lib/models/gas_estimators/gas_estimator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ abstract class GasEstimator {
GasEstimator({required this.chainId});

Future<List<int>?> getNetworkGasFees();
Future<GasEstimate?> getGasEstimates(UserOperation userOp, {bool includesPaymaster = false});
Future<GasEstimate?> getGasEstimates(UserOperation userOp, {GasEstimate? prevEstimate, bool includesPaymaster = false});
}
22 changes: 13 additions & 9 deletions lib/models/gas_estimators/l1_gas_estimator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,27 +31,31 @@ class L1GasEstimator extends GasEstimator {
}

@override
Future<GasEstimate?> getGasEstimates(UserOperation userOp, {bool includesPaymaster = false}) async {
List<int> networkFees = await getNetworkGasFees() ?? [0, 0];
Future<GasEstimate?> getGasEstimates(UserOperation userOp, {GasEstimate? prevEstimate, bool includesPaymaster = false}) async {
GasEstimate? gasEstimate;
UserOperation dummyOp = UserOperation.fromJson(userOp.toJson()); // copy userOp to a dummy one for any modifications related to estimates
/*if (paymasterAddress != null){
dummyOp.paymasterAndData = bytesToHex(paymasterAddress.addressBytes + Uint8List.fromList(List<int>.filled(340, 1)), include0x: true);
}*/
//
dummyOp.callGasLimit = BigInt.parse("ffffffffffffff", radix: 16);
dummyOp.preVerificationGas = BigInt.parse("0", radix: 16);
dummyOp.verificationGasLimit = BigInt.parse("ffffffffffff", radix: 16);
dummyOp.maxFeePerGas = BigInt.zero;
dummyOp.maxPriorityFeePerGas = BigInt.zero;
dummyOp.signature = bytesToHex(Uint8List.fromList(List<int>.filled(65, 1)), include0x: true);
GasEstimate? gasEstimate = await Bundler.getUserOperationGasEstimates(dummyOp, chainId);
if (gasEstimate == null) return null;
//
if (prevEstimate == null){
List<int> networkFees = await getNetworkGasFees() ?? [0, 0];
gasEstimate = await Bundler.getUserOperationGasEstimates(dummyOp, chainId);
if (gasEstimate == null) return null;
gasEstimate.maxFeePerGas = BigInt.from(networkFees[0]);
gasEstimate.maxPriorityFeePerGas = BigInt.from(networkFees[1]);
}else{
gasEstimate = prevEstimate;
}
if (includesPaymaster){
gasEstimate.preVerificationGas += BigInt.from(84); // To accommodate for GnosisTransaction.approveAmount which would be 0 before estimation
gasEstimate.preVerificationGas += BigInt.from(2496); // to accommodate for paymasterAndData (156 bytes * 16)
}
gasEstimate.preVerificationGas += BigInt.from(1260);
gasEstimate.maxFeePerGas = BigInt.from(networkFees[0]);
gasEstimate.maxPriorityFeePerGas = BigInt.from(networkFees[1]);
return gasEstimate;
}
}
59 changes: 33 additions & 26 deletions lib/models/gas_estimators/l2_gas_estimator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,48 +22,55 @@ class L2GasEstimator extends GasEstimator {
}

@override
Future<GasEstimate?> getGasEstimates(UserOperation userOp, {bool includesPaymaster = false}) async {
Future<GasEstimate?> getGasEstimates(UserOperation userOp, {GasEstimate? prevEstimate, bool includesPaymaster = false}) async {
GasEstimate? gasEstimate;
UserOperation dummyOp = UserOperation.fromJson(userOp.toJson()); // copy userOp to a dummy one for any modifications related to estimates
//
/*if (paymasterAddress != null){
dummyOp.paymasterAndData = bytesToHex(paymasterAddress.addressBytes + Uint8List.fromList(List<int>.filled(156, 1)), include0x: true);
}*/
dummyOp.callGasLimit = BigInt.parse("fffffff", radix: 16);
dummyOp.preVerificationGas = BigInt.parse("0", radix: 16);
dummyOp.verificationGasLimit = BigInt.parse("fffffff", radix: 16);
dummyOp.maxFeePerGas = BigInt.zero;
dummyOp.maxPriorityFeePerGas = BigInt.zero;
dummyOp.signature = bytesToHex(Uint8List.fromList(List<int>.filled(65, 1)), include0x: true);
//
List<int> networkFees = await getNetworkGasFees() ?? [0, 0];
GasEstimate? gasEstimate = await Bundler.getUserOperationGasEstimates(dummyOp, chainId);
if (gasEstimate == null) return null;
if (prevEstimate == null){
List<int> networkFees = await getNetworkGasFees() ?? [0, 0];
gasEstimate = await Bundler.getUserOperationGasEstimates(dummyOp, chainId);
if (gasEstimate == null) return null;
gasEstimate.maxFeePerGas = BigInt.from(networkFees[0]);
gasEstimate.maxPriorityFeePerGas = BigInt.from(networkFees[1]);
}else{
gasEstimate = prevEstimate;
}
//
Web3Client client = Networks.getByChainId(chainId)!.client;
OVMGasOracle gasOracle = OVMGasOracle(address: ovmGasOracle, client: client);
late BigInt l1GasUsed;
late BigInt l1BaseFee;
EntryPoint entryPoint = EntryPoint(address: Constants.addressZero, client: client);
var callData = entryPoint.self.function("handleOps").encodeCall([
[dummyOp.toList()],
dummyOp.sender
]);
await Future.wait([
gasOracle.getL1GasUsed(callData).then((value) => l1GasUsed = value),
gasOracle.l1BaseFee().then((value) => l1BaseFee = value),
]);
if (gasEstimate.l1GasUsed == BigInt.zero){
Web3Client client = Networks.getByChainId(chainId)!.client;
OVMGasOracle gasOracle = OVMGasOracle(address: ovmGasOracle, client: client);
late BigInt l1GasUsed;
late BigInt l1BaseFee;
EntryPoint entryPoint = EntryPoint(address: Constants.addressZero, client: client);
var callData = entryPoint.self.function("handleOps").encodeCall([
[dummyOp.toList()],
dummyOp.sender
]);
await Future.wait([
gasOracle.getL1GasUsed(callData).then((value) => l1GasUsed = value),
gasOracle.l1BaseFee().then((value) => l1BaseFee = value),
]);
gasEstimate.l1BaseFee = l1BaseFee;
gasEstimate.l1GasUsed = l1GasUsed;
}

if (includesPaymaster){
l1GasUsed += BigInt.from(84); // To accommodate for GnosisTransaction.approveAmount which would be 0 before estimation
l1GasUsed += BigInt.from(2496); // to accommodate for paymasterAndData (156 bytes * 16)
gasEstimate.l1GasUsed += BigInt.from(84); // To accommodate for GnosisTransaction.approveAmount which would be 0 before estimation
gasEstimate.l1GasUsed += BigInt.from(2496); // to accommodate for paymasterAndData (156 bytes * 16)
}
BigInt scale = l1BaseFee ~/ BigInt.from(networkFees[0]);
BigInt scale = gasEstimate.l1BaseFee ~/ gasEstimate.maxFeePerGas;
if (scale == BigInt.zero){
scale = BigInt.one;
}
gasEstimate.preVerificationGas += l1GasUsed * (scale);
gasEstimate.preVerificationGas += gasEstimate.l1GasUsed * (scale);
//
gasEstimate.maxFeePerGas = BigInt.from(networkFees[0]);
gasEstimate.maxPriorityFeePerGas = BigInt.from(networkFees[1]);
return gasEstimate;
}
}
6 changes: 3 additions & 3 deletions lib/screens/components/token_fee_selector.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class _TokenFeeSelectorState extends State<TokenFeeSelector> {
controller: ModalScrollController.of(context),
child: FeeCurrenciesSelectionSheet(
currencies: widget.batch.paymasterResponse.tokens,
initialSelection: widget.batch.feeCurrency?.token,
initialSelection: widget.batch.selectedFeeToken?.token,
onSelected: (feeCurrency){
setState(() {
widget.batch.setSelectedFeeToken(feeCurrency);
Expand Down Expand Up @@ -78,8 +78,8 @@ class _TokenFeeSelectorState extends State<TokenFeeSelector> {
const Spacer(),
Column(
children: [
Text(widget.batch.feeCurrency != null ? CurrencyUtils.formatCurrency(widget.batch.feeCurrency!.fee, widget.batch.feeCurrency!.token, formatSmallDecimals: true) : "-", style: TextStyle(fontFamily: AppThemes.fonts.gilroyBold, color: Colors.white)),
Text(widget.batch.feeCurrency != null ? "\$${CurrencyUtils.convertToQuote(widget.batch.feeCurrency!.token.address.toLowerCase(), PersistentData.accountBalance.quoteCurrency, widget.batch.feeCurrency!.fee).toPrecision(3)}" : "-", style: TextStyle(fontFamily: AppThemes.fonts.gilroyBold, color: Colors.grey, fontSize: 12)),
Text(widget.batch.selectedFeeToken != null ? CurrencyUtils.formatCurrency(widget.batch.selectedFeeToken!.fee, widget.batch.selectedFeeToken!.token, formatSmallDecimals: true) : "-", style: TextStyle(fontFamily: AppThemes.fonts.gilroyBold, color: Colors.white)),
Text(widget.batch.selectedFeeToken != null ? "\$${CurrencyUtils.convertToQuote(widget.batch.selectedFeeToken!.token.address.toLowerCase(), PersistentData.accountBalance.quoteCurrency, widget.batch.selectedFeeToken!.fee).toPrecision(3)}" : "-", style: TextStyle(fontFamily: AppThemes.fonts.gilroyBold, color: Colors.grey, fontSize: 12)),
],
),
const SizedBox(width: 5,),
Expand Down
6 changes: 3 additions & 3 deletions lib/screens/home/components/transaction_review_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,21 +70,21 @@ class _TransactionReviewSheetState extends State<TransactionReviewSheet> {
errorMessage = "";
BigInt fee = widget.batch.getFee();
if (widget.currency != null){
if (widget.currency?.address.toLowerCase() == widget.batch.feeCurrency!.token.address.toLowerCase()){
if (widget.currency?.address.toLowerCase() == widget.batch.selectedFeeToken!.token.address.toLowerCase()){
if ((widget.value ?? BigInt.zero) + fee > PersistentData.getCurrencyBalance(widget.currency!.address.toLowerCase())){
errorMessage = _errors["fee"]!;
}
}else{
if ((widget.value ?? BigInt.zero) > PersistentData.getCurrencyBalance(widget.currency!.address.toLowerCase())){
errorMessage = _errors["balance"]!;
}else{
if (PersistentData.getCurrencyBalance(widget.batch.feeCurrency!.token.address.toLowerCase()) < fee){
if (PersistentData.getCurrencyBalance(widget.batch.selectedFeeToken!.token.address.toLowerCase()) < fee){
errorMessage = _errors["fee"]!;
}
}
}
}else{
if (PersistentData.getCurrencyBalance(widget.batch.feeCurrency!.token.address.toLowerCase()) < fee){
if (PersistentData.getCurrencyBalance(widget.batch.selectedFeeToken!.token.address.toLowerCase()) < fee){
errorMessage = _errors["fee"]!;
}
}
Expand Down

0 comments on commit 238a1d2

Please sign in to comment.