mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-18 05:51:34 +02:00
* feat(zcash): initial commit - initial work on cw_zcash - use single .db for everything zcash to speed up sync and scanning - implement silent autoshielding of non-orchard funds - use FFI for everything to fetch the data - add support for passphrase by adding extra word to seed phrase - enforce lints on CI for cw_zcash directory - use correct zcash name, and call transparent zcash tZEC - minor cleanups outsite of cw_zcash - gitmodules, gitignore - db debug - fix broken addressPageType in WalletInfo - enable debug options in kDebugMode and kProfileMode * feat: T address rotation (and cache) wip: minor fixes * wip: shield tx list * fix fiat amounts fix derrivation of rotation account addresses screen for zcash block height fix T address shields improvements Amount in disposable T shields UI improvements show pending outgoing tx in tx history * fix null check on passphrase page fix tx showing as pending rename auto shield txs fixed send all to include fee use rotating address in addressForExchange wrap all calls of WarpApi.getLatestHeight in try catch to prevent leaking errors other minor fixes from notion * wallet group debug cache sent tx address wallet T address rotation fixes automatically pick the fee for auto-shield * fix T address cache deserialization fix autoshield icon fix autoshield text getCachedDestinationAddress fix send address cache * address comments from review * wip: iOS fix * fix swap * Allow cakepay payment from zcash wallet * fix: ios (I'm stupid) fix: reorder addresses to make orchard default fix: send all balance * 5.6.6 bump fix: ios apple review add transport error to ignored exceptions * fix: leaving dust behind when sweeping all * Change wallets order * fix: reflect balance of pending out txs fix: restore height support * linux support (disabled) * fix: date picker for zcash on restore screen fix: build script for linux match WarpApi.warpSync args with upstream * fix: linter script * ensure all exchange providers support ZEC (#2802) * ensure all exchange providers support ZEC * remove zaddr and tzec currencies and refactor providers * Update lib/exchange/provider/near_Intents_exchange_provider.dart * add back old zcash currencies for backward compatibility, thanks Czarek * minor fix [skip ci] --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com> * Update android/app/src/main/AndroidManifestBase.xml [skip ci] * Update ios/Runner/InfoBase.plist [skip ci] * fix: remove unused code fix: macos fix: disable on linux and macos * update build numbers [skip ci] * Add zcash to birdpay --------- Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net> Co-authored-by: Serhii <borodenko.sv@gmail.com>
1176 lines
40 KiB
Dart
1176 lines
40 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
|
import 'package:cake_wallet/core/address_validator.dart';
|
|
import 'package:cake_wallet/core/amount_validator.dart';
|
|
import 'package:cake_wallet/core/execution_state.dart';
|
|
import 'package:cake_wallet/core/open_crypto_pay/models.dart';
|
|
import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart';
|
|
import 'package:cake_wallet/core/validator.dart';
|
|
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
|
|
import 'package:cake_wallet/decred/decred.dart';
|
|
import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
|
|
import 'package:cake_wallet/entities/contact.dart';
|
|
import 'package:cake_wallet/entities/contact_record.dart';
|
|
import 'package:cake_wallet/entities/evm_transaction_error_fees_handler.dart';
|
|
import 'package:cake_wallet/entities/fiat_currency.dart';
|
|
import 'package:cake_wallet/entities/parsed_address.dart';
|
|
import 'package:cake_wallet/entities/preferences_key.dart';
|
|
import 'package:cake_wallet/entities/template.dart';
|
|
import 'package:cake_wallet/entities/transaction_description.dart';
|
|
import 'package:cake_wallet/entities/wallet_contact.dart';
|
|
import 'package:cake_wallet/evm/evm.dart';
|
|
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
|
|
import 'package:cake_wallet/exchange/provider/swapsxyz_exchange_provider.dart';
|
|
import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart';
|
|
import 'package:cake_wallet/exchange/trade.dart';
|
|
import 'package:cake_wallet/generated/i18n.dart';
|
|
import 'package:cake_wallet/monero/monero.dart';
|
|
import 'package:cake_wallet/nano/nano.dart';
|
|
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
|
import 'package:cake_wallet/routes.dart';
|
|
import 'package:cake_wallet/solana/solana.dart';
|
|
import 'package:cake_wallet/store/app_store.dart';
|
|
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
|
|
import 'package:cake_wallet/store/settings_store.dart';
|
|
import 'package:cake_wallet/tron/tron.dart';
|
|
import 'package:cake_wallet/utils/payment_request.dart';
|
|
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
|
|
import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
|
|
import 'package:cake_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
|
|
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
|
|
import 'package:cake_wallet/view_model/send/output.dart';
|
|
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
|
|
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
|
|
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
|
|
import 'package:cake_wallet/wownero/wownero.dart';
|
|
import 'package:cake_wallet/zano/zano.dart';
|
|
import 'package:cake_wallet/zcash/zcash.dart';
|
|
import 'package:cw_core/crypto_currency.dart';
|
|
import 'package:cw_core/erc20_token.dart';
|
|
import 'package:cw_core/exceptions.dart';
|
|
import 'package:cw_core/pending_transaction.dart';
|
|
import 'package:cw_core/sync_status.dart';
|
|
import 'package:cw_core/transaction_info.dart';
|
|
import 'package:cw_core/unspent_coin_type.dart';
|
|
import 'package:cw_core/utils/print_verbose.dart';
|
|
import 'package:cw_core/wallet_type.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:hive/hive.dart';
|
|
import 'package:mobx/mobx.dart';
|
|
import 'package:cake_wallet/utils/token_utilities.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
part 'send_view_model.g.dart';
|
|
|
|
class SendViewModel = SendViewModelBase with _$SendViewModel;
|
|
|
|
abstract class SendViewModelBase extends WalletChangeListenerViewModel with Store {
|
|
@override
|
|
void onWalletChange(wallet) {
|
|
currencies = wallet.balance.keys.toList();
|
|
selectedCryptoCurrency = wallet.currency;
|
|
hasMultipleTokens = isEVMWallet ||
|
|
wallet.type == WalletType.solana ||
|
|
wallet.type == WalletType.tron ||
|
|
wallet.type == WalletType.zano;
|
|
|
|
for (final output in outputs) {
|
|
output.updateWallet(wallet);
|
|
}
|
|
|
|
// Update unspent coins list view model with the new wallet reference
|
|
unspentCoinsListViewModel.updateWallet(wallet);
|
|
|
|
// Update sending balance to reflect the new wallet's balance
|
|
updateSendingBalance();
|
|
}
|
|
|
|
UnspentCoinsListViewModel unspentCoinsListViewModel;
|
|
|
|
SendViewModelBase(
|
|
AppStore appStore,
|
|
this.sendTemplateViewModel,
|
|
this._fiatConversationStore,
|
|
this.balanceViewModel,
|
|
this.contactListViewModel,
|
|
this.transactionDescriptionBox,
|
|
this.hardwareWalletViewModel,
|
|
this.unspentCoinsListViewModel,
|
|
this.feesViewModel, {
|
|
this.coinTypeToSpendFrom = UnspentCoinType.nonMweb,
|
|
}) : state = InitialExecutionState(),
|
|
currencies = appStore.wallet!.balance.keys.toList(),
|
|
selectedCryptoCurrency = appStore.wallet!.currency,
|
|
hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type) ||
|
|
appStore.wallet!.type == WalletType.solana ||
|
|
appStore.wallet!.type == WalletType.tron ||
|
|
appStore.wallet!.type == WalletType.zano,
|
|
selectedChainId = appStore.wallet!.chainId,
|
|
outputs = ObservableList<Output>(),
|
|
_settingsStore = appStore.settingsStore,
|
|
fiatFromSettings = appStore.settingsStore.fiatCurrency,
|
|
super(appStore: appStore) {
|
|
outputs
|
|
.add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency));
|
|
|
|
unspentCoinsListViewModel.initialSetup().then((_) {
|
|
unspentCoinsListViewModel.resetUnspentCoinsInfoSelections();
|
|
});
|
|
|
|
reaction((_) {
|
|
if (isEVMCompatibleChain(wallet.type)) {
|
|
// Access currency which depends on selectedChainId, so MobX tracks the change
|
|
return wallet.currency;
|
|
}
|
|
return null;
|
|
}, (_) async {
|
|
// When chain changes, update currencies and selected currency
|
|
await Future.delayed(const Duration(milliseconds: 100));
|
|
currencies = wallet.balance.keys.toList();
|
|
selectedCryptoCurrency = wallet.currency;
|
|
updateSendingBalance();
|
|
});
|
|
}
|
|
|
|
PendingTransaction? _pendingApprovalTx;
|
|
bool _isSwapsXYZCallDataTx = false;
|
|
|
|
@observable
|
|
ExecutionState state;
|
|
|
|
ObservableList<Output> outputs;
|
|
|
|
@observable
|
|
UnspentCoinType coinTypeToSpendFrom;
|
|
|
|
bool get showAddressBookPopup => _settingsStore.showAddressBookPopupEnabled;
|
|
|
|
bool get isMwebEnabled => balanceViewModel.mwebEnabled;
|
|
|
|
bool get isEVMWallet => isEVMCompatibleChain(walletType);
|
|
|
|
@action
|
|
void setShowAddressBookPopup(bool value) {
|
|
_settingsStore.showAddressBookPopupEnabled = value;
|
|
}
|
|
|
|
@action
|
|
void addOutput() {
|
|
outputs
|
|
.add(Output(wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency));
|
|
}
|
|
|
|
@action
|
|
void removeOutput(Output output) {
|
|
if (isBatchSending) {
|
|
outputs.remove(output);
|
|
}
|
|
}
|
|
|
|
@action
|
|
void clearOutputs() {
|
|
outputs.clear();
|
|
addOutput();
|
|
}
|
|
|
|
@action
|
|
void setAllowMwebCoins(bool allow) {
|
|
if (wallet.type == WalletType.litecoin) {
|
|
coinTypeToSpendFrom = allow ? UnspentCoinType.any : UnspentCoinType.nonMweb;
|
|
}
|
|
}
|
|
|
|
@computed
|
|
bool get isBatchSending => outputs.length > 1;
|
|
|
|
bool get shouldDisplaySendALL {
|
|
if (walletType == WalletType.solana) return false;
|
|
|
|
// if (walletType == WalletType.ethereum && selectedCryptoCurrency == CryptoCurrency.eth)
|
|
// return false;
|
|
|
|
// if (walletType == WalletType.polygon && selectedCryptoCurrency == CryptoCurrency.maticpoly)
|
|
// return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
@computed
|
|
String get pendingTransactionFiatAmount {
|
|
if (pendingTransaction == null) {
|
|
return '0.00';
|
|
}
|
|
|
|
try {
|
|
final fiat = calculateFiatAmount(
|
|
price: _fiatConversationStore.prices[_fiatConversationStore.prices.keys
|
|
.firstWhere((k) => k.titleAndTagEqual(selectedCryptoCurrency))],
|
|
cryptoAmount: pendingTransaction!.amountFormatted);
|
|
return fiat;
|
|
} catch (_) {
|
|
return '0.00';
|
|
}
|
|
}
|
|
|
|
@computed
|
|
String get pendingTransactionFeeFiatAmount {
|
|
try {
|
|
if (pendingTransaction != null) {
|
|
final currency = pendingTransactionFeeCurrency(walletType);
|
|
final fiat = calculateFiatAmount(
|
|
price: _fiatConversationStore.prices[currency]!,
|
|
cryptoAmount: pendingTransaction!.feeFormattedValue,
|
|
);
|
|
return fiat;
|
|
} else {
|
|
return '0.00';
|
|
}
|
|
} catch (_) {
|
|
return '0.00';
|
|
}
|
|
}
|
|
|
|
CryptoCurrency pendingTransactionFeeCurrency(WalletType type) {
|
|
switch (type) {
|
|
case WalletType.ethereum:
|
|
case WalletType.polygon:
|
|
case WalletType.base:
|
|
case WalletType.arbitrum:
|
|
case WalletType.tron:
|
|
case WalletType.solana:
|
|
return wallet.currency;
|
|
default:
|
|
return selectedCryptoCurrency;
|
|
}
|
|
}
|
|
|
|
FiatCurrency get fiat => _settingsStore.fiatCurrency;
|
|
|
|
CryptoCurrency get currency => wallet.currency;
|
|
|
|
Validator<String> amountValidator(Output output) => AmountValidator(
|
|
currency: wallet.currency,
|
|
minValue: isSendToSilentPayments(output)
|
|
?
|
|
// TODO: get from server
|
|
// bitcoin!.silentPaymentsMinAmount
|
|
'0.00001'
|
|
: null,
|
|
);
|
|
|
|
Validator<String> get allAmountValidator => AllAmountValidator();
|
|
|
|
Validator<String> get addressValidator =>
|
|
AddressValidator(type: selectedCryptoCurrency, isTestnet: wallet.isTestnet);
|
|
|
|
Validator<String> get textValidator => TextValidator();
|
|
|
|
final FiatCurrency fiatFromSettings;
|
|
|
|
@observable
|
|
PendingTransaction? pendingTransaction;
|
|
|
|
@computed
|
|
String get balance {
|
|
if (walletType == WalletType.litecoin && coinTypeToSpendFrom == UnspentCoinType.mweb) {
|
|
return balanceViewModel.balances.values.first.secondAvailableBalance;
|
|
} else if (walletType == WalletType.litecoin &&
|
|
coinTypeToSpendFrom == UnspentCoinType.nonMweb) {
|
|
return balanceViewModel.balances.values.first.availableBalance;
|
|
}
|
|
// Handle case where balance might not be available yet (e.g., during chain switch)
|
|
final balanceForCurrency = wallet.balance[selectedCryptoCurrency];
|
|
if (balanceForCurrency == null) {
|
|
return wallet.formatCryptoAmount('0');
|
|
}
|
|
return balanceForCurrency.formattedFullAvailableBalance;
|
|
}
|
|
|
|
@action
|
|
Future<void> updateSendingBalance() async {
|
|
// force the sendingBalance to recompute since unspent coins aren't observable
|
|
// or at least mobx can't detect the changes
|
|
|
|
final currentType = coinTypeToSpendFrom;
|
|
|
|
if (currentType == UnspentCoinType.any) {
|
|
coinTypeToSpendFrom = UnspentCoinType.nonMweb;
|
|
} else if (currentType == UnspentCoinType.nonMweb) {
|
|
coinTypeToSpendFrom = UnspentCoinType.any;
|
|
} else if (currentType == UnspentCoinType.mweb) {
|
|
coinTypeToSpendFrom = UnspentCoinType.nonMweb;
|
|
}
|
|
|
|
// set it back to the original value:
|
|
coinTypeToSpendFrom = currentType;
|
|
}
|
|
|
|
@computed
|
|
Future<String> get sendingBalance async {
|
|
// only for electrum, monero, wownero, decred wallets atm:
|
|
switch (wallet.type) {
|
|
case WalletType.bitcoin:
|
|
case WalletType.litecoin:
|
|
case WalletType.bitcoinCash:
|
|
case WalletType.dogecoin:
|
|
case WalletType.monero:
|
|
case WalletType.wownero:
|
|
case WalletType.decred:
|
|
return wallet.formatCryptoAmount(
|
|
(await unspentCoinsListViewModel.getSendingBalance(coinTypeToSpendFrom)).toString());
|
|
default:
|
|
return balance;
|
|
}
|
|
}
|
|
|
|
@computed
|
|
bool get isFiatDisabled => balanceViewModel.isFiatDisabled;
|
|
|
|
@computed
|
|
String get pendingTransactionFiatAmountFormatted =>
|
|
isFiatDisabled ? '' : pendingTransactionFiatAmount + ' ' + fiat.title;
|
|
|
|
@computed
|
|
String get pendingTransactionFeeFiatAmountFormatted =>
|
|
isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + ' ' + fiat.title;
|
|
|
|
@computed
|
|
bool get isReadyForSend =>
|
|
wallet.syncStatus is SyncedSyncStatus ||
|
|
// If silent payments scanning, can still send payments
|
|
(wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus);
|
|
|
|
bool isSendToSilentPayments(Output output) =>
|
|
wallet.type == WalletType.bitcoin &&
|
|
(RegExp(AddressValidator.silentPaymentAddressPatternMainnet).hasMatch(output.address) ||
|
|
RegExp(AddressValidator.silentPaymentAddressPatternMainnet)
|
|
.hasMatch(output.extractedAddress) ||
|
|
(output.parsedAddress.addresses.isNotEmpty &&
|
|
RegExp(AddressValidator.silentPaymentAddressPatternMainnet)
|
|
.hasMatch(output.parsedAddress.addresses[0])));
|
|
|
|
@computed
|
|
List<Template> get templates => sendTemplateViewModel.templates
|
|
.where((template) => _isEqualCurrency(template.cryptoCurrency))
|
|
.toList();
|
|
|
|
@computed
|
|
bool get hasCoinControl => [
|
|
WalletType.bitcoin,
|
|
WalletType.litecoin,
|
|
WalletType.monero,
|
|
WalletType.wownero,
|
|
WalletType.decred,
|
|
WalletType.bitcoinCash,
|
|
WalletType.dogecoin
|
|
].contains(wallet.type);
|
|
|
|
@computed
|
|
bool get isElectrumWallet => [
|
|
WalletType.bitcoin,
|
|
WalletType.litecoin,
|
|
WalletType.bitcoinCash,
|
|
WalletType.dogecoin
|
|
].contains(wallet.type);
|
|
|
|
@observable
|
|
CryptoCurrency selectedCryptoCurrency;
|
|
|
|
List<CryptoCurrency> currencies;
|
|
|
|
bool get hasYat => outputs
|
|
.any((out) => out.isParsedAddress && out.parsedAddress.parseFrom == ParseFrom.yatRecord);
|
|
|
|
WalletType get walletType => wallet.type;
|
|
|
|
String? get walletCurrencyName => wallet.currency.fullName?.toLowerCase() ?? wallet.currency.name;
|
|
|
|
@computed
|
|
FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency;
|
|
|
|
final SettingsStore _settingsStore;
|
|
final SendTemplateViewModel sendTemplateViewModel;
|
|
final BalanceViewModel balanceViewModel;
|
|
final ContactListViewModel contactListViewModel;
|
|
final HardwareWalletViewModel? hardwareWalletViewModel;
|
|
final FeesViewModel feesViewModel;
|
|
final FiatConversionStore _fiatConversationStore;
|
|
final Box<TransactionDescription> transactionDescriptionBox;
|
|
|
|
@observable
|
|
bool hasMultipleTokens;
|
|
|
|
@observable
|
|
int? selectedChainId;
|
|
|
|
@computed
|
|
List<ContactRecord> get contactsToShow => contactListViewModel.contacts
|
|
.where((element) => element.type == selectedCryptoCurrency)
|
|
.toList();
|
|
|
|
@computed
|
|
List<WalletContact> get walletContactsToShow => contactListViewModel.walletContacts
|
|
.where((element) => element.type == selectedCryptoCurrency)
|
|
.toList();
|
|
|
|
@action
|
|
bool checkIfAddressIsAContact(String address) {
|
|
final contactList = contactsToShow.where((element) => element.address == address).toList();
|
|
|
|
return contactList.isNotEmpty;
|
|
}
|
|
|
|
@action
|
|
bool checkIfWalletIsAnInternalWallet(String address) {
|
|
final walletContactList =
|
|
walletContactsToShow.where((element) => element.address == address).toList();
|
|
|
|
return walletContactList.isNotEmpty;
|
|
}
|
|
|
|
@computed
|
|
bool get shouldDisplayTOTP2FAForContact => _settingsStore.shouldRequireTOTP2FAForSendsToContact;
|
|
|
|
@computed
|
|
bool get shouldDisplayTOTP2FAForNonContact =>
|
|
_settingsStore.shouldRequireTOTP2FAForSendsToNonContact;
|
|
|
|
@computed
|
|
bool get shouldDisplayTOTP2FAForSendsToInternalWallet =>
|
|
_settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets;
|
|
|
|
//* Still open to further optimize these checks
|
|
//* It works but can be made better
|
|
@action
|
|
bool checkThroughChecksToDisplayTOTP(String address) {
|
|
final isContact = checkIfAddressIsAContact(address);
|
|
final isInternalWallet = checkIfWalletIsAnInternalWallet(address);
|
|
|
|
if (isContact) {
|
|
return shouldDisplayTOTP2FAForContact;
|
|
} else if (isInternalWallet) {
|
|
return shouldDisplayTOTP2FAForSendsToInternalWallet;
|
|
} else {
|
|
return shouldDisplayTOTP2FAForNonContact;
|
|
}
|
|
}
|
|
|
|
bool shouldDisplayTotp() {
|
|
List<bool> conditionsList = [];
|
|
|
|
for (var output in outputs) {
|
|
final show = checkThroughChecksToDisplayTOTP(output.extractedAddress);
|
|
conditionsList.add(show);
|
|
}
|
|
|
|
return conditionsList.contains(true);
|
|
}
|
|
|
|
final _ocpService = OpenCryptoPayService();
|
|
|
|
@observable
|
|
OpenCryptoPayRequest? ocpRequest;
|
|
|
|
@action
|
|
Future<void> dismissTransaction() async {
|
|
state = InitialExecutionState();
|
|
if (ocpRequest != null) {
|
|
clearOutputs();
|
|
_ocpService.cancelOpenCryptoPayRequest(ocpRequest!);
|
|
ocpRequest = null;
|
|
}
|
|
}
|
|
|
|
@action
|
|
Future<PendingTransaction?> createOpenCryptoPayTransaction(String uri) async {
|
|
state = IsExecutingState();
|
|
|
|
try {
|
|
final originalOCPRequest = await _ocpService.getOpenCryptoPayInvoice(uri.toString());
|
|
final paymentUri = await _ocpService.getOpenCryptoPayAddress(
|
|
originalOCPRequest,
|
|
selectedCryptoCurrency,
|
|
);
|
|
|
|
ocpRequest = originalOCPRequest;
|
|
|
|
final paymentRequest = PaymentRequest.fromUri(paymentUri);
|
|
clearOutputs();
|
|
|
|
outputs.first.address = paymentRequest.address;
|
|
outputs.first.parsedAddress =
|
|
ParsedAddress(addresses: [paymentRequest.address], name: ocpRequest!.receiverName);
|
|
outputs.first.setCryptoAmount(paymentRequest.amount);
|
|
outputs.first.note = ocpRequest!.receiverName;
|
|
|
|
return createTransaction();
|
|
} catch (e) {
|
|
printV(e);
|
|
state = FailureState(translateErrorMessage(e, walletType, currency));
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Timer? _ledgerTxStateTimer;
|
|
|
|
@action
|
|
Future<PendingTransaction?> createTransaction({ExchangeProvider? provider, Trade? trade}) async {
|
|
try {
|
|
if (!(state is IsExecutingState)) state = IsExecutingState();
|
|
|
|
if (wallet.isHardwareWallet) {
|
|
state = IsAwaitingDeviceResponseState();
|
|
if (walletType == WalletType.monero)
|
|
_ledgerTxStateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
|
|
if (monero!.getLastLedgerCommand() == "INS_CLSAG") {
|
|
timer.cancel();
|
|
state = IsDeviceSigningResponseState();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Swaps.xyz (EVM) path
|
|
|
|
if (isEVMWallet && trade != null && provider is SwapsXyzExchangeProvider) {
|
|
final routerTo = trade.inputAddress;
|
|
final routerData = trade.routerData;
|
|
final routerValueWei =
|
|
BigInt.tryParse((trade.routerValue ?? '0').toString()) ?? BigInt.zero;
|
|
|
|
if (routerTo?.isNotEmpty == true && routerData?.isNotEmpty == true) {
|
|
// detect prepared ERC-20 transfer(...) (alt-vm deposit pattern)
|
|
String _selector(String s) =>
|
|
(s.startsWith('0x') && s.length >= 10) ? s.substring(0, 10) : '';
|
|
const _transferSig = '0xa9059cbb';
|
|
final _sel = _selector(routerData!);
|
|
final _isPreparedTransfer = _sel == _transferSig &&
|
|
(trade.sourceTokenAddress ?? '').toLowerCase() == (routerTo ?? '').toLowerCase();
|
|
|
|
_pendingApprovalTx = null;
|
|
|
|
// Optionally prebuild approval (SKIP for prepared transfer)
|
|
final tokenContract = trade.sourceTokenAddress ?? '';
|
|
final requiredAmount = BigInt.tryParse(
|
|
(trade.sourceTokenAmountRaw ?? '0').replaceAll('n', ''),
|
|
) ??
|
|
BigInt.zero;
|
|
|
|
// Only do approval when NOT a prepared transfer, and only if the API hinted we might need it
|
|
final requiresTokenApproval =
|
|
(trade.requiresTokenApproval ?? false) && !_isPreparedTransfer;
|
|
|
|
if (requiresTokenApproval && tokenContract.isNotEmpty && requiredAmount > BigInt.zero) {
|
|
if (isEVMWallet) {
|
|
final priority = _settingsStore.getPriority(walletType, chainId: selectedChainId);
|
|
_pendingApprovalTx = await buildApprovalIfNeeded(
|
|
spender: routerTo!,
|
|
tokenContract: tokenContract,
|
|
requiredAmount: requiredAmount,
|
|
sourceTokenDecimals: trade.sourceTokenDecimals,
|
|
);
|
|
|
|
// Build the callData tx
|
|
pendingTransaction = await evm!.createRawCallDataTransaction(
|
|
wallet,
|
|
routerTo,
|
|
routerData,
|
|
routerValueWei,
|
|
priority,
|
|
useBlinkProtection: canSupportBlinkProtection(selectedChainId)
|
|
? _settingsStore.useBlinkProtection
|
|
: false,
|
|
);
|
|
|
|
_isSwapsXYZCallDataTx = true;
|
|
state = ExecutedSuccessfullyState();
|
|
return pendingTransaction; // do NOT fall back to regular flow
|
|
}
|
|
}
|
|
|
|
// No approval needed (or prepared transfer): send exactly what backend prepared
|
|
if (isEVMWallet) {
|
|
final priority = _settingsStore.getPriority(walletType, chainId: selectedChainId);
|
|
pendingTransaction = await evm!.createRawCallDataTransaction(
|
|
wallet,
|
|
routerTo!,
|
|
routerData,
|
|
routerValueWei,
|
|
priority,
|
|
useBlinkProtection: canSupportBlinkProtection(selectedChainId)
|
|
? _settingsStore.useBlinkProtection
|
|
: false,
|
|
);
|
|
_isSwapsXYZCallDataTx = true;
|
|
state = ExecutedSuccessfullyState();
|
|
return pendingTransaction;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Regular flow
|
|
|
|
pendingTransaction = await wallet.createTransaction(_credentials(provider));
|
|
|
|
if (provider is ThorChainExchangeProvider) {
|
|
final outputCount = pendingTransaction?.outputCount ?? 0;
|
|
if (outputCount > 10) {
|
|
throw Exception("THORChain does not support more than 10 outputs");
|
|
}
|
|
|
|
if (_hasTaprootInput(pendingTransaction)) {
|
|
throw Exception("THORChain does not support Taproot addresses");
|
|
}
|
|
}
|
|
|
|
if (wallet.type == WalletType.bitcoin) {
|
|
final updatedOutputs = bitcoin!.updateOutputs(pendingTransaction!, outputs);
|
|
|
|
if (outputs.length == updatedOutputs.length) {
|
|
outputs.replaceRange(0, outputs.length, updatedOutputs);
|
|
}
|
|
}
|
|
|
|
state = ExecutedSuccessfullyState();
|
|
return pendingTransaction;
|
|
} catch (e) {
|
|
_ledgerTxStateTimer?.cancel();
|
|
// if (e is LedgerException) {
|
|
// final errorCode = e.errorCode.toRadixString(16);
|
|
// final fallbackMsg =
|
|
// e.message.isNotEmpty ? e.message : "Unexpected Ledger Error Code: $errorCode";
|
|
// final errorMsg = ledgerViewModel!.interpretErrorCode(errorCode) ?? fallbackMsg;
|
|
//
|
|
// state = FailureState(errorMsg);
|
|
// } else {
|
|
state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency));
|
|
// }
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@action
|
|
Future<void> replaceByFee(TransactionInfo tx, String newFee) async {
|
|
state = IsExecutingState();
|
|
|
|
try {
|
|
final isSufficient = await bitcoin!.isChangeSufficientForFee(wallet, tx.id, newFee);
|
|
|
|
if (!isSufficient) {
|
|
state = AwaitingConfirmationState(
|
|
title: S.current.confirm_fee_deduction,
|
|
message: S.current.confirm_fee_deduction_content,
|
|
onConfirm: () async => await _executeReplaceByFee(tx, newFee),
|
|
onCancel: () => state = FailureState('Insufficient change for fee'));
|
|
} else {
|
|
await _executeReplaceByFee(tx, newFee);
|
|
}
|
|
} catch (e) {
|
|
state = FailureState(e.toString());
|
|
}
|
|
}
|
|
|
|
Future<void> _executeReplaceByFee(TransactionInfo tx, String newFee) async {
|
|
clearOutputs();
|
|
final output = outputs.first;
|
|
output.address = tx.outputAddresses?.first ?? '';
|
|
|
|
try {
|
|
pendingTransaction = await bitcoin!.replaceByFee(wallet, tx.id, newFee);
|
|
state = ExecutedSuccessfullyState();
|
|
} catch (e) {
|
|
state = FailureState(e.toString());
|
|
}
|
|
}
|
|
|
|
Future<void> _handleOcpRequest() async {
|
|
if (OpenCryptoPayService.requiresClientCommit(selectedCryptoCurrency)) {
|
|
await pendingTransaction!.commit();
|
|
}
|
|
|
|
await _ocpService.commitOpenCryptoPayRequest(
|
|
pendingTransaction!.hex,
|
|
txId: pendingTransaction!.id,
|
|
request: ocpRequest!,
|
|
asset: selectedCryptoCurrency,
|
|
);
|
|
}
|
|
|
|
Future<void> _commitApprovalTransaction() async {
|
|
if (_pendingApprovalTx != null) {
|
|
await _pendingApprovalTx!.commit();
|
|
_pendingApprovalTx = null;
|
|
// Small pause to ensure allowance is indexed
|
|
await Future.delayed(const Duration(milliseconds: 300));
|
|
}
|
|
|
|
await pendingTransaction!.commit();
|
|
_isSwapsXYZCallDataTx = false;
|
|
}
|
|
|
|
Future<void> _commitUR(BuildContext context) async {
|
|
final urstr = await pendingTransaction!.commitUR();
|
|
final result = await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: urstr);
|
|
if (result == null) {
|
|
throw "Canceled by user";
|
|
}
|
|
}
|
|
|
|
@action
|
|
Future<void> commitTransaction(BuildContext context) async {
|
|
if (pendingTransaction == null) {
|
|
throw Exception("Pending transaction doesn't exist. It should not be happened.");
|
|
}
|
|
|
|
try {
|
|
state = TransactionCommitting();
|
|
|
|
if (ocpRequest != null) {
|
|
await _handleOcpRequest();
|
|
} else if (_isSwapsXYZCallDataTx) {
|
|
// Swaps.xyz approval (if any), then commit the prebuilt router tx
|
|
await _commitApprovalTransaction();
|
|
} else if (pendingTransaction!.shouldCommitUR()) {
|
|
await _commitUR(context);
|
|
} else {
|
|
await pendingTransaction!.commit();
|
|
}
|
|
|
|
state = TransactionCommitted();
|
|
|
|
// Immediate transaction update for EVM chains, Solana, Tron, and Nano
|
|
if (isEVMWallet ||
|
|
[WalletType.solana, WalletType.tron, WalletType.nano].contains(walletType)) {
|
|
Future.delayed(Duration(seconds: 4), () async {
|
|
try {
|
|
await wallet.updateTransactionsHistory();
|
|
} catch (e) {
|
|
printV('Failed to update transactions after send: $e');
|
|
}
|
|
});
|
|
}
|
|
|
|
if (pendingTransaction!.id.isNotEmpty) {
|
|
_addTransactionDescription();
|
|
}
|
|
final sharedPreferences = await SharedPreferences.getInstance();
|
|
await sharedPreferences.setString(PreferencesKey.backgroundSyncLastTrigger(wallet.name),
|
|
DateTime.now().add(Duration(minutes: 1)).toIso8601String());
|
|
} catch (e) {
|
|
state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency));
|
|
}
|
|
}
|
|
|
|
Future<void> _addTransactionDescription() async {
|
|
String address = outputs.fold('', (acc, value) {
|
|
return value.isParsedAddress
|
|
? '$acc${value.address}\n${value.extractedAddress}\n\n'
|
|
: '$acc${value.address}\n\n';
|
|
});
|
|
|
|
address = address.trim();
|
|
|
|
String note = outputs.fold('', (acc, value) => '$acc${value.note}\n');
|
|
|
|
note = note.trim();
|
|
|
|
TransactionInfo? tx;
|
|
if (walletType == WalletType.monero) {
|
|
await Future.delayed(Duration(milliseconds: 450));
|
|
await wallet.fetchTransactions();
|
|
final txhistory = monero!.getTransactionHistory(wallet);
|
|
tx = txhistory.transactions.values.last;
|
|
}
|
|
final descriptionKey = '${pendingTransaction!.id}_${wallet.walletAddresses.primaryAddress}';
|
|
_settingsStore.shouldSaveRecipientAddress
|
|
? await transactionDescriptionBox.add(TransactionDescription(
|
|
id: descriptionKey,
|
|
recipientAddress: address,
|
|
transactionNote: note,
|
|
transactionKey: tx?.additionalInfo["key"] as String?,
|
|
))
|
|
: await transactionDescriptionBox.add(TransactionDescription(
|
|
id: descriptionKey,
|
|
transactionNote: note,
|
|
transactionKey: tx?.additionalInfo["key"] as String?,
|
|
));
|
|
}
|
|
|
|
Object _credentials([ExchangeProvider? provider]) {
|
|
final priority = _settingsStore.getPriority(wallet.type, chainId: wallet.chainId);
|
|
|
|
if (priority == null &&
|
|
wallet.type != WalletType.nano &&
|
|
wallet.type != WalletType.banano &&
|
|
wallet.type != WalletType.solana &&
|
|
wallet.type != WalletType.tron &&
|
|
wallet.type != WalletType.arbitrum &&
|
|
wallet.type != WalletType.zcash) {
|
|
throw Exception('Priority is null for wallet type: ${wallet.type}');
|
|
}
|
|
|
|
switch (wallet.type) {
|
|
case WalletType.bitcoin:
|
|
case WalletType.bitcoinCash:
|
|
case WalletType.dogecoin:
|
|
return bitcoin!.createBitcoinTransactionCredentials(
|
|
outputs,
|
|
priority: priority!,
|
|
feeRate: feesViewModel.customBitcoinFeeRate,
|
|
coinTypeToSpendFrom: coinTypeToSpendFrom,
|
|
payjoinUri: _settingsStore.usePayjoin ? payjoinUri : null,
|
|
);
|
|
case WalletType.litecoin:
|
|
return bitcoin!.createBitcoinTransactionCredentials(
|
|
outputs,
|
|
priority: priority!,
|
|
feeRate: feesViewModel.customBitcoinFeeRate,
|
|
// if it's an exchange flow then disable sending from mweb coins
|
|
coinTypeToSpendFrom: provider != null ? UnspentCoinType.nonMweb : coinTypeToSpendFrom,
|
|
);
|
|
|
|
case WalletType.monero:
|
|
return monero!
|
|
.createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority!);
|
|
|
|
case WalletType.wownero:
|
|
return wownero!
|
|
.createWowneroTransactionCreationCredentials(outputs: outputs, priority: priority!);
|
|
|
|
case WalletType.ethereum:
|
|
case WalletType.polygon:
|
|
case WalletType.base:
|
|
case WalletType.arbitrum:
|
|
return evm!.createEVMTransactionCredentials(
|
|
outputs,
|
|
priority: priority,
|
|
currency: selectedCryptoCurrency,
|
|
useBlinkProtection: canSupportBlinkProtection(selectedChainId)
|
|
? _settingsStore.useBlinkProtection
|
|
: false,
|
|
);
|
|
case WalletType.nano:
|
|
return nano!.createNanoTransactionCredentials(outputs);
|
|
case WalletType.solana:
|
|
return solana!
|
|
.createSolanaTransactionCredentials(outputs, currency: selectedCryptoCurrency);
|
|
case WalletType.tron:
|
|
return tron!.createTronTransactionCredentials(outputs, currency: selectedCryptoCurrency);
|
|
case WalletType.zano:
|
|
return zano!.createZanoTransactionCredentials(
|
|
outputs: outputs, priority: priority!, currency: selectedCryptoCurrency);
|
|
case WalletType.decred:
|
|
this.coinTypeToSpendFrom = UnspentCoinType.any;
|
|
return decred!.createDecredTransactionCredentials(outputs, priority!);
|
|
case WalletType.zcash:
|
|
return zcash!.createZcashTransactionCredentials(
|
|
outputs,
|
|
currency: selectedCryptoCurrency,
|
|
// priority: priority,
|
|
);
|
|
default:
|
|
throw Exception('Unexpected wallet type: ${wallet.type}');
|
|
}
|
|
}
|
|
|
|
bool _isEqualCurrency(String currency) =>
|
|
wallet.balance.keys.any((e) => currency.toLowerCase() == e.title.toLowerCase());
|
|
|
|
void onClose() => _settingsStore.fiatCurrency = fiatFromSettings;
|
|
|
|
@action
|
|
void setFiatCurrency(FiatCurrency fiat) => _settingsStore.fiatCurrency = fiat;
|
|
|
|
@action
|
|
void setSelectedCryptoCurrency(String cryptoCurrency) {
|
|
try {
|
|
selectedCryptoCurrency = wallet.balance.keys
|
|
.firstWhere((e) => cryptoCurrency.toLowerCase() == e.title.toLowerCase());
|
|
} catch (e) {
|
|
selectedCryptoCurrency = wallet.currency;
|
|
}
|
|
}
|
|
|
|
ContactRecord? newContactAddress() {
|
|
final Set<String> contactAddresses =
|
|
Set.from(contactListViewModel.contacts.map((contact) => contact.address))
|
|
..addAll(contactListViewModel.walletContacts.map((contact) => contact.address));
|
|
|
|
for (var output in outputs) {
|
|
String address;
|
|
if (output.isParsedAddress) {
|
|
address = output.parsedAddress.addresses.first;
|
|
} else {
|
|
address = output.address;
|
|
}
|
|
|
|
if (address.isNotEmpty &&
|
|
!contactAddresses.contains(address) &&
|
|
selectedCryptoCurrency.raw != -1) {
|
|
return ContactRecord(
|
|
contactListViewModel.contactSource,
|
|
Contact(
|
|
name: '',
|
|
address: address,
|
|
type: selectedCryptoCurrency,
|
|
));
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
String translateErrorMessage(
|
|
Object error,
|
|
WalletType walletType,
|
|
CryptoCurrency currency,
|
|
) {
|
|
String errorMessage = error.toString();
|
|
|
|
if (walletType == WalletType.solana) {
|
|
if (errorMessage.contains('insufficient lamports')) {
|
|
double solValueNeeded = 0.0;
|
|
|
|
// Regular expression to match the number after "need". This shows the exact lamports the user needs to perform the transaction.
|
|
RegExp regExp = RegExp(r'need (\d+)');
|
|
|
|
// Find the match
|
|
Match? match = regExp.firstMatch(errorMessage);
|
|
|
|
if (match != null) {
|
|
String neededAmount = match.group(1)!;
|
|
final lamportsNeeded = int.tryParse(neededAmount);
|
|
|
|
// 5000 lamport used here is the constant for sending a transaction on solana
|
|
int lamportsPerSol = 1000000000;
|
|
|
|
solValueNeeded =
|
|
lamportsNeeded != null ? ((lamportsNeeded + 5000) / lamportsPerSol) : 0.0;
|
|
return S.current.insufficient_lamports(solValueNeeded.toString());
|
|
} else {
|
|
return S.current.insufficient_lamport_for_tx;
|
|
}
|
|
}
|
|
|
|
if (error is SignNativeTokenTransactionRentException) {
|
|
return S.current.solana_sign_native_transaction_rent_exception;
|
|
}
|
|
|
|
if (error is CreateAssociatedTokenAccountException) {
|
|
return "${S.current.solana_create_associated_token_account_exception} ${S.current.added_message_for_ata_error}";
|
|
}
|
|
|
|
if (error is SignSPLTokenTransactionRentException) {
|
|
return S.current.solana_sign_spl_token_transaction_rent_exception;
|
|
}
|
|
|
|
if (error is NoAssociatedTokenAccountException) {
|
|
return S.current.solana_no_associated_token_account_exception;
|
|
}
|
|
|
|
if (errorMessage.contains('found no record of a prior credit')) {
|
|
return S.current.insufficient_funds_for_tx;
|
|
}
|
|
|
|
if (errorMessage.contains('insufficient funds for rent') &&
|
|
errorMessage.contains('Transaction simulation failed') &&
|
|
errorMessage.contains('account_index')) {
|
|
final accountIndexMatch = RegExp(r'account_index: (\d+)').firstMatch(errorMessage);
|
|
if (accountIndexMatch != null) {
|
|
return int.parse(accountIndexMatch.group(1)!) == 0
|
|
? S.current.insufficientFundsForRentError
|
|
: S.current.insufficientFundsForRentErrorReceiver;
|
|
}
|
|
}
|
|
|
|
if (errorMessage.contains('invalid account data')) {
|
|
return S.current.solana_invalid_data_message;
|
|
}
|
|
|
|
if (errorMessage.contains('Blockhash not found') ||
|
|
errorMessage.contains('BlockhashNotFound') ||
|
|
errorMessage.contains('BlockhashMNotFound')) {
|
|
return 'Transaction failed because its recent blockhash expired. '
|
|
'Please retry your send; if this keeps happening, try again in '
|
|
'a few seconds or switch to a different Solana node.';
|
|
}
|
|
|
|
return errorMessage;
|
|
}
|
|
if (isEVMWallet || walletType == WalletType.haven) {
|
|
if (errorMessage.contains('gas required exceeds allowance')) {
|
|
return S.current.gas_exceeds_allowance;
|
|
}
|
|
|
|
if (errorMessage.contains('insufficient funds')) {
|
|
final parsedErrorMessageResult =
|
|
EVMTransactionErrorFeesHandler.parseEthereumFeesErrorMessage(
|
|
errorMessage,
|
|
_fiatConversationStore.prices[currency] ?? 0.0,
|
|
);
|
|
|
|
// Handle generic insufficient funds error (no specific values available)
|
|
if (parsedErrorMessageResult.error == 'generic_insufficient_funds') {
|
|
return S.current.insufficient_funds_for_tx;
|
|
}
|
|
|
|
// Handle parsing errors (couldn't parse the error message)
|
|
if (parsedErrorMessageResult.error != null) {
|
|
return S.current.insufficient_funds_for_tx;
|
|
}
|
|
|
|
// Handle successfully parsed errors with specific values
|
|
return '''${S.current.insufficient_funds_for_tx} \n\n'''
|
|
'''${S.current.balance}: ${parsedErrorMessageResult.balanceEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.balanceUsd} ${fiatFromSettings.name})\n\n'''
|
|
'''${S.current.transaction_cost}: ${parsedErrorMessageResult.txCostEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.txCostUsd} ${fiatFromSettings.name})\n\n'''
|
|
'''${S.current.overshot}: ${parsedErrorMessageResult.overshotEth} ${walletType == WalletType.polygon ? "POL" : "ETH"} (${parsedErrorMessageResult.overshotUsd} ${fiatFromSettings.name})''';
|
|
}
|
|
|
|
if (errorMessage.contains('max fee per gas less than block base fee')) {
|
|
return S.current.tx_retry_message;
|
|
}
|
|
|
|
return errorMessage;
|
|
}
|
|
|
|
if (walletType == WalletType.tron) {
|
|
if (errorMessage.contains('balance is not sufficient')) {
|
|
return S.current.do_not_have_enough_gas_asset(currency.toString());
|
|
}
|
|
|
|
if (errorMessage.contains('Transaction expired')) {
|
|
return S.current.tx_retry_message;
|
|
}
|
|
}
|
|
|
|
if (error is TransactionWrongBalanceException) {
|
|
if (error.amount != null)
|
|
return S.current
|
|
.tx_wrong_balance_with_amount_exception(currency.toString(), error.amount.toString());
|
|
|
|
return S.current.tx_wrong_balance_exception(currency.toString());
|
|
}
|
|
if (error is TransactionNoInputsException) {
|
|
return S.current.tx_not_enough_inputs_exception;
|
|
}
|
|
if (error is TransactionNoFeeException) {
|
|
return S.current.tx_zero_fee_exception;
|
|
}
|
|
if (error is TransactionNoDustException) {
|
|
return S.current.tx_no_dust_exception;
|
|
}
|
|
if (error is TransactionCommitFailed) {
|
|
if (error.errorMessage != null && error.errorMessage!.contains("no peers replied")) {
|
|
return S.current.tx_commit_failed_no_peers;
|
|
}
|
|
return "${S.current.tx_commit_failed}\nsupport@cakewallet.com${error.errorMessage != null ? "\n\n${error.errorMessage}" : ""}";
|
|
}
|
|
if (error is TransactionCommitFailedDustChange) {
|
|
return S.current.tx_rejected_dust_change;
|
|
}
|
|
if (error is TransactionCommitFailedDustOutput) {
|
|
return S.current.tx_rejected_dust_output;
|
|
}
|
|
if (error is TransactionCommitFailedDustOutputSendAll) {
|
|
return S.current.tx_rejected_dust_output_send_all;
|
|
}
|
|
if (error is TransactionCommitFailedVoutNegative) {
|
|
return S.current.tx_rejected_vout_negative;
|
|
}
|
|
if (error is TransactionCommitFailedBIP68Final) {
|
|
return S.current.tx_rejected_bip68_final;
|
|
}
|
|
if (error is TransactionCommitFailedLessThanMin) {
|
|
return S.current.fee_less_than_min;
|
|
}
|
|
if (error is TransactionNoDustOnChangeException) {
|
|
return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max);
|
|
}
|
|
if (error is TransactionInputNotSupported) {
|
|
return S.current.tx_invalid_input;
|
|
}
|
|
|
|
return errorMessage;
|
|
}
|
|
|
|
bool _hasTaprootInput(PendingTransaction? pendingTransaction) {
|
|
if (walletType == WalletType.bitcoin && pendingTransaction != null) {
|
|
return bitcoin!.hasTaprootInput(pendingTransaction);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
Future<PendingTransaction?> buildApprovalIfNeeded({
|
|
required String spender,
|
|
required String tokenContract,
|
|
required BigInt requiredAmount,
|
|
int? sourceTokenDecimals,
|
|
}) async {
|
|
// Only EVM chains support ERC20 approvals
|
|
if (!isEVMWallet) return null;
|
|
|
|
const zero = '0x0000000000000000000000000000000000000000';
|
|
const evmNative = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
|
|
|
|
final tokenLc = tokenContract.toLowerCase();
|
|
if (tokenLc == zero || tokenLc == evmNative.toLowerCase()) return null;
|
|
if (requiredAmount <= BigInt.zero) return null;
|
|
|
|
bool needsApproval = false;
|
|
if (isEVMWallet) {
|
|
needsApproval = await evm!.isApprovalRequired(
|
|
wallet,
|
|
tokenContract,
|
|
spender,
|
|
requiredAmount,
|
|
);
|
|
}
|
|
|
|
if (!needsApproval) return null;
|
|
|
|
final erc20Token = wallet.balance.keys.whereType<Erc20Token>().firstWhere(
|
|
(t) => t.contractAddress.toLowerCase() == tokenLc,
|
|
orElse: () => Erc20Token(
|
|
name: '',
|
|
symbol: '',
|
|
contractAddress: tokenContract,
|
|
decimal: sourceTokenDecimals ?? 18,
|
|
enabled: true,
|
|
),
|
|
);
|
|
|
|
if (isEVMWallet) {
|
|
final priority = _settingsStore.getPriority(walletType, chainId: selectedChainId);
|
|
return await evm!.createTokenApproval(
|
|
wallet,
|
|
requiredAmount,
|
|
spender,
|
|
erc20Token,
|
|
priority,
|
|
useBlinkProtection:
|
|
canSupportBlinkProtection(selectedChainId) ? _settingsStore.useBlinkProtection : false,
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
@computed
|
|
bool get usePayjoin => _settingsStore.usePayjoin;
|
|
|
|
@observable
|
|
String? payjoinUri;
|
|
|
|
@action
|
|
Future<void> fetchTokenForContractAddress(String contractAddress) async {
|
|
final token = await TokenUtilities.findTokenByAddress(
|
|
walletType: wallet.type,
|
|
address: contractAddress,
|
|
);
|
|
|
|
if (token != null) {
|
|
selectedCryptoCurrency = token;
|
|
}
|
|
}
|
|
}
|