feat: Add memo support for swap (#3229)

* feat: Add memo support for swap

* fix: Error on swap page select receiver bottomsheet when picking receiveing currency that's not a wallet type

* feat: exclude providers that do not support memo when receive currency needs it, also show passed memo in confirmation and trade history sheets

* fix: overflow for destination tag on swap confirmation

* Update lib/view_model/exchange/exchange_view_model.dart [skip ci]

* Update lib/exchange/provider/trocador_exchange_provider.dart [skip ci]

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
David Adegoke
2026-05-20 20:09:34 +02:00
committed by GitHub
co-authored by Omar Hatem
parent d10f2f2a60
commit 1d652c76aa
56 changed files with 330 additions and 55 deletions
@@ -57,6 +57,7 @@ import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart';
import 'package:cw_core/crypto_amount_format.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currencies_with_memo.dart';
import 'package:cw_core/erc20_token.dart';
import 'package:cw_core/spl_token.dart';
import 'package:cw_core/sync_status.dart';
@@ -124,12 +125,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
_useTorOnly = _settingsStore.exchangeStatus == ExchangeApiMode.torOnly;
_setProviders();
const excludeDepositCurrencies = [CryptoCurrency.btt];
const excludeReceiveCurrencies = [
CryptoCurrency.xlm,
CryptoCurrency.xrp,
CryptoCurrency.bnb,
CryptoCurrency.btt
];
const excludeReceiveCurrencies = [CryptoCurrency.btt];
_initialPairBasedOnWallet();
unspentCoinsListViewModel.initialSetup().then((_) {
@@ -391,6 +387,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@observable
String receiveAddress;
@observable
String receiveAddressExtraId = '';
@observable
String? receiveAddressDisplayName;
@@ -436,13 +435,18 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
@computed
Future<List<WalletInfo>> get receiveWallets async {
WalletType? type;
type = cryptoCurrencyOrTokenToWalletType(receiveCurrency);
if (type == null) {
type = cryptoCurrencyOrTokenToWalletType(CryptoCurrency.fromString(receiveCurrency.tag ?? ""));
}
try {
WalletType? type;
type = cryptoCurrencyOrTokenToWalletType(receiveCurrency);
if (type == null) {
type =
cryptoCurrencyOrTokenToWalletType(CryptoCurrency.fromString(receiveCurrency.tag ?? ""));
}
return await WalletInfo.selectList("type = ?", [type!.index]);
return await WalletInfo.selectList("type = ?", [type!.index]);
} catch (e) {
return [];
}
}
Future<List<WalletInfoAddressInfo>> addressesForAccountsWallet(WalletInfo wallet) async {
@@ -761,6 +765,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
currency == receiveCurrency)) {
receiveAddress = "";
}
if (currency != receiveCurrency) {
receiveAddressExtraId = "";
}
receiveCurrency = currency;
isFixedRateMode = false;
@@ -884,6 +891,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
(provider.description == ExchangeProviderDescription.swapsXyz ||
provider.description == ExchangeProviderDescription.nearIntents);
bool _excludeProviderForReceiveExtraId(ExchangeProvider provider) =>
memoLabelTypeFor(receiveCurrency) != null && !provider.supportsMemoOrDestinationTag;
Future<void> calculateBestRate() async {
if (depositCurrency == receiveCurrency) {
bestRate = 0.0;
@@ -895,6 +905,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
final validProvidersForAmount = _tradeAvailableProviders.where((provider) {
if (_excludeProviderForSwapAll(provider)) return false;
if (_excludeProviderForReceiveExtraId(provider)) return false;
final limits = _providerLimits[provider];
@@ -972,6 +983,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
try {
final futures = selectedProviders
.where((provider) => providerList.contains(provider))
.where((provider) => !_excludeProviderForReceiveExtraId(provider))
.map((provider) async {
final limits = await provider
.fetchLimits(
@@ -1050,6 +1062,13 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
}
if (forcedProvider != null && _excludeProviderForReceiveExtraId(forcedProvider!)) {
tradeState = TradeIsCreatedFailure(
title: S.current.trade_not_created,
error: S.current.none_of_selected_providers_can_exchange);
return;
}
Map<double, ExchangeProvider> providers;
if (forcedProvider != null) {
providers = {forcedProviderRate: forcedProvider!};
@@ -1138,6 +1157,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
if (isFixedRateMode && provider.supportsFixedRate == false) {
continue;
}
if (_excludeProviderForReceiveExtraId(provider)) {
continue;
}
// Skip Swaps.xyz when sending from external
if (isSendFromExternal &&
@@ -1160,6 +1183,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
toAmount: _receiveAmount.replaceAll(',', '.'),
refundAddress: depositAddress,
toAddress: receiveAddress,
toAddressExtraId: receiveAddressExtraId.trim(),
isFixedRate: isFixedRateMode,
);
@@ -1260,6 +1284,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
depositCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
receiveAddress =
receiveCurrency == wallet.currency ? wallet.walletAddresses.addressForExchange : '';
receiveAddressExtraId = '';
isDepositAddressEnabled = !(depositCurrency == wallet.currency);
isFixedRateMode = false;
_onPairChange();
@@ -26,6 +26,7 @@ import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.d
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/utils/date_formatter.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cw_core/currencies_with_memo.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/services.dart';
import 'package:mobx/mobx.dart';
@@ -182,6 +183,18 @@ abstract class TradeDetailsViewModelBase with Store {
));
}
final destinationMemo = trade.toAddressExtraId;
final destinationCurrency = trade.to;
if (destinationMemo != null &&
destinationMemo.isNotEmpty &&
destinationCurrency != null) {
final isDestinationTag =
memoLabelTypeFor(destinationCurrency) == MemoLabelType.destinationTag;
items.add(StandartListItem(
title: isDestinationTag ? S.current.destination_tag : S.current.memo,
value: destinationMemo));
}
items.add(StandartListItem(
title: S.current.trade_details_provider, value: trade.provider.toString()));