Skip to content

Commit

Permalink
feat: show actual transaction fee in tx activity
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewwahid committed May 26, 2023
1 parent 4d3cd66 commit b5e4356
Show file tree
Hide file tree
Showing 12 changed files with 121 additions and 72 deletions.
22 changes: 14 additions & 8 deletions lib/controller/persistent_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,6 @@ class TransactionActivity {
String action;
String title;
String status;
String? paymasterEventTopic; // used to extract gas cost from paymaster emitted event
String? hash;
String? txHash;
Map<String, String> data;
Expand All @@ -433,7 +432,6 @@ class TransactionActivity {
required this.action,
required this.title,
required this.status,
this.paymasterEventTopic,
this.hash,
this.txHash,
required this.data});
Expand All @@ -444,7 +442,6 @@ class TransactionActivity {
action = json['action'],
title = json['title'],
status = json['status'],
paymasterEventTopic = json['paymasterEventTopic'],
hash = json['hash'],
txHash = json['txHash'],
data = Map<String, String>.from(json['data']),
Expand All @@ -456,7 +453,6 @@ class TransactionActivity {
'action': action,
'title': title,
'status': status,
'paymasterEventTopic': paymasterEventTopic,
'hash': hash,
'txHash': txHash,
'data': data,
Expand All @@ -466,22 +462,32 @@ class TransactionActivity {

class TransactionFeeActivityData {
String paymasterAddress;
String? sponsoredEventTopic; // used to extract gas cost from paymaster emitted event
/// Currency Address
String currencyAddress;
BigInt fee;
BigInt fee; // estimated fee
BigInt? actualFee; // actual fee, fetched from UO's receipt

TransactionFeeActivityData({required this.paymasterAddress,
TransactionFeeActivityData({
required this.paymasterAddress,
this.sponsoredEventTopic,
required this.currencyAddress,
required this.fee});
required this.fee,
this.actualFee,
});

TransactionFeeActivityData.fromJson(Map json)
: paymasterAddress = json['paymasterAddress'],
sponsoredEventTopic = json['sponsoredEventTopic'],
currencyAddress = json['currency'],
fee = BigInt.parse(json['fee']);
fee = BigInt.parse(json['fee']),
actualFee = json['actualFee'] != null ? BigInt.parse(json['actualFee']) : null;

Map<String, dynamic> toJson() => {
'paymasterAddress': paymasterAddress,
'sponsoredEventTopic': sponsoredEventTopic,
'currency': currencyAddress,
'fee': fee.toString(),
'actualFee': actualFee?.toString(),
};
}
3 changes: 2 additions & 1 deletion lib/controller/transaction_confirm_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ class TransactionConfirmController {
transactionActivity.hash = response?.hash;
transactionActivity.status = response?.status ?? "failed-to-submit";
transactionActivity.fee = TransactionFeeActivityData(
paymasterAddress: batch.includesPaymaster ? Constants.addressZeroHex : batch.paymasterResponse.paymasterData.paymaster.hexEip55,
paymasterAddress: batch.includesPaymaster ? batch.paymasterResponse.paymasterData.paymaster.hexEip55 : Constants.addressZeroHex,
sponsoredEventTopic: batch.includesPaymaster ? batch.paymasterResponse.paymasterData.eventTopic : "0x",
currencyAddress: batch.getFeeToken(),
fee: batch.getFee(),
);
Expand Down
10 changes: 1 addition & 9 deletions lib/controller/wallet_connect_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import 'package:candide_mobile_app/controller/persistent_data.dart';
import 'package:candide_mobile_app/controller/token_info_storage.dart';
import 'package:candide_mobile_app/models/batch.dart';
import 'package:candide_mobile_app/models/gnosis_transaction.dart';
import 'package:candide_mobile_app/models/paymaster/paymaster_response.dart';
import 'package:candide_mobile_app/screens/home/activity/components/transaction_activity_details_card.dart';
import 'package:candide_mobile_app/screens/home/components/transaction/transaction_review_sheet.dart';
import 'package:candide_mobile_app/screens/home/send/components/send_review_leading.dart';
Expand All @@ -15,7 +14,6 @@ import 'package:candide_mobile_app/screens/home/wallet_connect/components/wc_rev
import 'package:candide_mobile_app/screens/home/wallet_connect/components/wc_signature_reject_dialog.dart';
import 'package:candide_mobile_app/screens/home/wallet_connect/wc_session_request_sheet.dart';
import 'package:candide_mobile_app/screens/home/wallet_connect/wc_signature_request_sheet.dart';
import 'package:candide_mobile_app/services/paymaster.dart';
import 'package:candide_mobile_app/services/transaction_watchdog.dart';
import 'package:candide_mobile_app/utils/currency.dart';
import 'package:candide_mobile_app/utils/events.dart';
Expand Down Expand Up @@ -335,13 +333,7 @@ class WalletConnectController {
wcBatch.transactions.add(transaction);
}
//
PaymasterResponse? paymasterResponse = await Paymaster.fetchPaymasterFees(PersistentData.selectedAccount.chainId);
if (paymasterResponse == null){
// todo handle network errors
return;
}else{
await wcBatch.setPaymasterResponse(paymasterResponse);
}
await wcBatch.fetchPaymasterResponse();
//
cancelLoad();
TransactionActivity transactionActivity = TransactionActivity(
Expand Down
9 changes: 7 additions & 2 deletions lib/models/batch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ class Batch {
//
PaymasterResponse get paymasterResponse => _paymasterResponse;
FeeToken? get selectedFeeToken => _selectedFeeToken;
bool get includesPaymaster => _selectedFeeToken != null && _selectedFeeToken?.token.symbol != network.nativeCurrency && _selectedFeeToken?.token.address != Constants.addressZeroHex;
bool get includesPaymaster {
if (gasBack?.gasBackApplied ?? false) return true;
return _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;

Expand Down Expand Up @@ -84,6 +87,9 @@ class Batch {
}

BigInt getFee(){
if (gasBack?.gasBackApplied ?? false){
return BigInt.zero;
}
return selectedFeeToken?.fee ?? BigInt.zero;
}

Expand Down Expand Up @@ -184,7 +190,6 @@ class Batch {
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(feeToken)){
userOp.verificationGasLimit += BigInt.from(35000);
}
Expand Down
1 change: 1 addition & 0 deletions lib/models/paymaster/gas_back_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class GasBackData {
GasBackData({required this.paymaster, required this.paymasterAndData, required this.gasBackApplied});

static Future<GasBackData> getGasBackData(Account account, EthereumAddress _paymaster, Network network, BigInt maxETHCost) async {
if (_paymaster == Constants.addressZero) return GasBackData(paymaster: _paymaster, paymasterAndData: "0x", gasBackApplied: false);
PaymasterContract paymasterContract = PaymasterContract(address: _paymaster, client: network.client);
BigInt accountGasBack = await paymasterContract.gasBackBalances(account.address);
bool gasBackApplied = accountGasBack >= maxETHCost;
Expand Down
4 changes: 1 addition & 3 deletions lib/models/paymaster/paymaster_data.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import 'dart:typed_data';

import 'package:web3dart/credentials.dart';

class PaymasterData {
EthereumAddress paymaster;
Uint8List eventTopic;
String eventTopic;

PaymasterData({required this.paymaster, required this.eventTopic});
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:candide_mobile_app/controller/persistent_data.dart';
import 'package:candide_mobile_app/controller/token_info_storage.dart';
import 'package:candide_mobile_app/models/guardian_operation.dart';
import 'package:candide_mobile_app/screens/components/summary_table.dart';
import 'package:candide_mobile_app/screens/home/components/transaction/free_card_indicator.dart';
import 'package:candide_mobile_app/screens/home/guardians/components/guardian_review_leading.dart';
import 'package:candide_mobile_app/screens/home/send/components/send_review_leading.dart';
import 'package:candide_mobile_app/screens/home/swap/components/swap_review_leading.dart';
Expand Down Expand Up @@ -77,13 +78,22 @@ class _TransactionActivityDetailsCardState extends State<TransactionActivityDeta
});
}
entries["Status"] = widget.transaction.status.replaceAll("-", " ").capitalizeFirst;
entries["Transaction fee"] = "< ${CurrencyUtils.formatCurrency(
entries["Estimated fee"] = "< ${CurrencyUtils.formatCurrency(
widget.transaction.fee.fee,
TokenInfoStorage.getTokenByAddress(widget.transaction.fee.currencyAddress)!,
includeSymbol: true,
formatSmallDecimals: true)}";
formatSmallDecimals: true).replaceAll("<", "")}";
if (widget.transaction.status == "failed-to-submit"){
entries["Transaction fee"] = CurrencyUtils.formatCurrency(BigInt.zero, TokenInfoStorage.getTokenByAddress(widget.transaction.fee.currencyAddress)!, includeSymbol: true, formatSmallDecimals: true);
entries.remove("Estimated fee");
}
if (widget.transaction.fee.actualFee != null){
entries["Transaction fee"] = CurrencyUtils.formatCurrency(
widget.transaction.fee.actualFee!,
TokenInfoStorage.getTokenByAddress(widget.transaction.fee.currencyAddress)!,
includeSymbol: true,
formatSmallDecimals: true);
entries.remove("Estimated fee");
}
entries["Network"] = Networks.selected().chainId.toString();
return entries;
Expand All @@ -110,6 +120,12 @@ class _TransactionActivityDetailsCardState extends State<TransactionActivityDeta

@override
Widget build(BuildContext context) {
var tableEntries = getTableEntries();
bool userOperationFullSponsored = widget.transaction.status == "success" && (widget.transaction.fee.fee == BigInt.zero || widget.transaction.fee.actualFee == BigInt.zero);
if (userOperationFullSponsored){
tableEntries["Transaction fee"] = "";
tableEntries.remove("Estimated fee");
}
return LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
Expand All @@ -133,10 +149,11 @@ class _TransactionActivityDetailsCardState extends State<TransactionActivityDeta
margin: EdgeInsets.symmetric(horizontal: Get.width * 0.03),
child: SummaryTable(
entries: [
for (MapEntry entry in getTableEntries().entries)
for (MapEntry entry in tableEntries.entries)
SummaryTableEntry(
title: entry.key,
titleStyle: null,
trailing: entry.key == "Transaction fee" && userOperationFullSponsored ? const FreeCardIndicator() : null,
value: entry.key == "Network" ? Networks.getByChainId(int.parse(entry.value))!.name : entry.value,
valueStyle: entry.key == "Network" || entry.key == "Status" ? TextStyle(fontFamily: AppThemes.fonts.gilroyBold, color: entry.key == "Status" ? getStatusColor(widget.transaction.status) : Networks.getByChainId(PersistentData.selectedAccount.chainId)!.color) : null,
)
Expand Down
35 changes: 35 additions & 0 deletions lib/screens/home/components/transaction/free_card_indicator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:candide_mobile_app/config/theme.dart';
import 'package:flutter/material.dart';
import 'package:phosphor_flutter/phosphor_flutter.dart';

class FreeCardIndicator extends StatelessWidget {
const FreeCardIndicator({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.zero,
elevation: 3,
shadowColor: Colors.green.withOpacity(0.2),
color: Colors.green.withOpacity(0.2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
side: const BorderSide(
color: Colors.green,
width: 1.2
)
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(PhosphorIcons.lightningBold, size: 15,),
const SizedBox(width: 5,),
Text("FREE", textAlign: TextAlign.center, style: TextStyle(fontFamily: AppThemes.fonts.gilroyBold),),
],
),
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:candide_mobile_app/controller/persistent_data.dart';
import 'package:candide_mobile_app/models/batch.dart';
import 'package:candide_mobile_app/models/paymaster/fee_token.dart';
import 'package:candide_mobile_app/screens/home/components/transaction/fee_currency_selection_sheet.dart';
import 'package:candide_mobile_app/screens/home/components/transaction/free_card_indicator.dart';
import 'package:candide_mobile_app/screens/home/components/transaction/gas_back_sheet.dart';
import 'package:candide_mobile_app/utils/currency.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -92,7 +93,7 @@ class _TokenFeeSelectorState extends State<TokenFeeSelector> {
],
),
const Spacer(),
!widget.batch.gasBack!.gasBackApplied ? _TokenFeeDisplay(batch: widget.batch,) : const _FreeCardIndicator(),
!widget.batch.gasBack!.gasBackApplied ? _TokenFeeDisplay(batch: widget.batch,) : const FreeCardIndicator(),
const SizedBox(width: 5,),
const Icon(PhosphorIcons.caretRightBold, size: 15, color: Colors.white,),
const SizedBox(width: 5,),
Expand All @@ -118,34 +119,3 @@ class _TokenFeeDisplay extends StatelessWidget {
);
}
}

class _FreeCardIndicator extends StatelessWidget {
const _FreeCardIndicator({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Card(
elevation: 3,
shadowColor: Colors.green.withOpacity(0.2),
color: Colors.green.withOpacity(0.2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
side: const BorderSide(
color: Colors.green,
width: 1.2
)
),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(PhosphorIcons.lightningBold, size: 15,),
const SizedBox(width: 5,),
Text("FREE", textAlign: TextAlign.center, style: TextStyle(fontFamily: AppThemes.fonts.gilroyBold),),
],
),
),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:candide_mobile_app/controller/transaction_confirm_controller.dar
import 'package:candide_mobile_app/models/batch.dart';
import 'package:candide_mobile_app/models/paymaster/fee_token.dart';
import 'package:candide_mobile_app/screens/components/summary_table.dart';
import 'package:candide_mobile_app/screens/components/token_fee_selector.dart';
import 'package:candide_mobile_app/screens/home/components/transaction/token_fee_selector.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:info_popup/info_popup.dart';
Expand Down
8 changes: 6 additions & 2 deletions lib/services/paymaster.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import 'package:candide_mobile_app/models/paymaster/paymaster_response.dart';
import 'package:candide_mobile_app/models/paymaster/sponsor_data.dart';
import 'package:candide_mobile_app/utils/constants.dart';
import 'package:dio/dio.dart';
import 'package:eth_sig_util/util/utils.dart';
import 'package:wallet_dart/wallet/user_operation.dart';
import 'package:web3dart/web3dart.dart';

Expand All @@ -30,7 +29,7 @@ class Paymaster {
tokens: result,
paymasterData: PaymasterData(
paymaster: Constants.addressZero,
eventTopic: hexToBytes("0x"),
eventTopic: "0x",
),
sponsorData: SponsorData(
sponsored: false,
Expand All @@ -48,8 +47,12 @@ class Paymaster {
);
//
EthereumAddress paymasterAddress = Constants.addressZero;
String eventTopic = "0x";
for (Map tokenData in response.data['result']){
paymasterAddress = EthereumAddress.fromHex(tokenData["paymaster"]);
if (tokenData.containsKey("sponsoredEventTopic")){
eventTopic = tokenData["sponsoredEventTopic"];
}
TokenInfo? _token = TokenInfoStorage.getTokenByAddress(tokenData["address"]);
if (_token == null) continue;
result.add(
Expand All @@ -63,6 +66,7 @@ class Paymaster {
}
paymasterResponse.tokens = result;
paymasterResponse.paymasterData.paymaster = paymasterAddress;
paymasterResponse.paymasterData.eventTopic = eventTopic;
return paymasterResponse;
} on DioError catch(e){
print("Error occurred ${e.type.toString()}");
Expand Down
Loading

0 comments on commit b5e4356

Please sign in to comment.