v6.0.2 Release candidate (#3080)

* v6.0.2 Release candidate

* v6.0.2 Release candidate

* move btc ln up in the currency picker

* disable bsc for swapsxyz

* minor [skip ci]

* hide dex/rate selection on swap screen

* temp fix for evm tokens swaps icons until hive migration is done

* minor [skip ci]

* disable force dex providers for existing users and let them pick it from settings preference

* Show bitcoin address types in swap picker (#3096)

* Show bitcoin address types in swap picker

* remove wrong conversion to bolt11 [skip ci]

* feat: add Lightning invoice resolution and address normalization

* automatically select contact wallet address based on the selected swap currency (whether it's bitcoin or lightning)

* minor [skip ci]

* default to bolt11 invoice for lightning swaps

---------

Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>

---------

Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>
This commit is contained in:
Omar Hatem
2026-03-16 14:21:16 +01:00
committed by GitHub
co-authored by Konstantin Ullrich
parent 82982ae5af
commit db00a0214a
52 changed files with 269 additions and 156 deletions
+58
View File
@@ -0,0 +1,58 @@
import 'dart:convert';
import 'package:cw_core/utils/proxy_wrapper.dart';
Future<String?> getBolt11FromLightingAddress(String lightningAddress, {int amount = 0}) async {
try {
final uri = getURlOfLightningAddress(lightningAddress);
var response = await ProxyWrapper().get(clearnetUri: uri);
final responseBody =
_LNURLPResponseDTO.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
response =
await ProxyWrapper().get(clearnetUri: Uri.parse("${responseBody.callback}?amount=$amount"));
final callbackResponseBody =
_LNURLPCallbackResponseDTO.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
return callbackResponseBody.pr;
} catch (e) {
return null;
}
}
Uri getURlOfLightningAddress(String lightningAddress) {
final parts = lightningAddress.split("@");
final name = parts.first;
final domain = parts.last;
return Uri.parse("https://$domain/.well-known/lnurlp/$name");
}
class _LNURLPResponseDTO {
final String callback;
final int maxSendable;
final int minSendable;
final String tag;
final String metadata;
const _LNURLPResponseDTO(
this.callback, this.maxSendable, this.minSendable, this.tag, this.metadata);
static _LNURLPResponseDTO fromJson(Map<String, dynamic> map) => _LNURLPResponseDTO(
map["callback"] as String,
map["maxSendable"] as int,
map["minSendable"] as int,
map["tag"] as String,
map["metadata"] as String);
}
class _LNURLPCallbackResponseDTO {
final String pr;
const _LNURLPCallbackResponseDTO(this.pr);
static _LNURLPCallbackResponseDTO fromJson(Map<String, dynamic> map) =>
_LNURLPCallbackResponseDTO(map["pr"] as String);
}
@@ -595,6 +595,9 @@ Future<void> defaultSettingsMigration(
case 60: // BalanceCardStyleSettings.cardOrder no-op (handled in sqlite.dart)
// Do not migrate SQLite here, do that in sqlite.dart in order to prevent runtime
// errors, missing row and missing tables.
case 61:
// reset force dex option only 1 time and let users pick it from the swap settings preference
await sharedPreferences.setBool(PreferencesKey.forceDecentralizedExchanges, false);
break;
default:
break;
@@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/core/lightning_invoice_service.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
@@ -196,8 +197,8 @@ class ExolixExchangeProvider extends ExchangeProvider {
'coinTo': _normalizeCurrency(_overrideToCryptoCurrency(request.toCurrency, request.toAddress)),
'networkFrom': _networkFor(request.fromCurrency),
'networkTo': _networkFor(request.toCurrency),
'withdrawalAddress': _normalizeAddress(request.toAddress),
'refundAddress': _normalizeAddress(request.refundAddress),
'withdrawalAddress': await _normalizeAddress(request.toAddress),
'refundAddress': await _normalizeAddress(request.refundAddress),
'rateType': _getRateType(isFixedRateMode),
'apiToken': apiKey,
};
@@ -213,7 +214,6 @@ class ExolixExchangeProvider extends ExchangeProvider {
headers: headers,
body: json.encode(body),
);
if (response.statusCode == 400) {
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
@@ -446,8 +446,17 @@ class ExolixExchangeProvider extends ExchangeProvider {
}
}
String _normalizeAddress(String address) =>
address.startsWith('bitcoincash:') ? address.replaceFirst('bitcoincash:', '') : address;
Future<String> _normalizeAddress(String address) async {
if (address.startsWith('bitcoincash:'))
return address.replaceFirst('bitcoincash:', '');
// Lightning addresses
if(address.contains("@"))
return await getBolt11FromLightingAddress(address) ?? address;
return address;
}
static double? _toDouble(dynamic value) {
if (value is int) {
@@ -8,7 +8,6 @@ import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/exchange/trade_not_created_exception.dart';
import 'package:cake_wallet/exchange/trade_request.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
import 'package:cw_core/amount_converter.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
@@ -20,7 +19,7 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
static final List<CryptoCurrency> _notSupportedAsSourceToken = [
CryptoCurrency.sol,
...CryptoCurrency.all.where(
(c) => (c.tag ?? '').toUpperCase() == 'SOL',
(c) => (c.tag ?? '').toUpperCase() == 'SOL' || c.tag == CryptoCurrency.bnb.tag,
),
];
@@ -209,7 +208,7 @@ class SwapsXyzExchangeProvider extends ExchangeProvider {
required bool isReceiveAmount}) async {
try {
if(_notSupportedAsSourceToken.contains(from)) {
if(_notSupportedAsSourceToken.contains(from) || _notSupportedAsSourceToken.contains(to)) {
printV(
'fetchRate: source token ${from.title} is not supported as source token');
return 0.0;
@@ -7,7 +7,6 @@ import 'package:cake_wallet/exchange/provider/exchange_provider.dart';
import 'package:cake_wallet/exchange/trade.dart';
import 'package:cake_wallet/exchange/trade_request.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/exchange/utils/currency_pairs_utils.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/crypto_currency.dart';
+1 -1
View File
@@ -324,7 +324,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
payjoinSessionSource: payjoinSessionSource,
anonpayInvoiceInfo: anonpayInvoiceInfo,
havenSeedStore: havenSeedStore,
initialMigrationVersion: 60,
initialMigrationVersion: 61,
);
}
+19 -20
View File
@@ -19,7 +19,6 @@ import 'package:cake_wallet/new-ui/widgets/swap_page/swap_address_selection_moda
import 'package:cake_wallet/new-ui/widgets/swap_page/swap_confirm_sheet.dart';
import 'package:cake_wallet/new-ui/widgets/swap_page/swap_limit_popup.dart';
import 'package:cake_wallet/new-ui/widgets/swap_page/swap_options_page.dart';
import 'package:cake_wallet/new-ui/widgets/swap_page/swap_provider_initial_preference_modal.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
@@ -45,7 +44,6 @@ import 'package:cw_core/wallet_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter_svg/svg.dart';
import 'package:mobx/mobx.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
@@ -98,20 +96,20 @@ class _NewSwapPageState extends State<NewSwapPage> {
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!widget.exchangeViewModel.decentralizedExchangesPromptDismissed) {
showMaterialModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isDismissible: false,
builder: (context) {
return SwapProviderInitialPreferenceModal();
}).then((val) {
widget.exchangeViewModel.dismissDecentralizedExchangesPrompt();
if (val is bool && val == true && !widget.exchangeViewModel.forceDecentralizedExchanges) {
widget.exchangeViewModel.toggleForceDecentralizedExchanges();
}
});
}
// if (!widget.exchangeViewModel.decentralizedExchangesPromptDismissed) {
// showMaterialModalBottomSheet(
// context: context,
// backgroundColor: Colors.transparent,
// isDismissible: false,
// builder: (context) {
// return SwapProviderInitialPreferenceModal();
// }).then((val) {
// widget.exchangeViewModel.dismissDecentralizedExchangesPrompt();
// if (val is bool && val == true && !widget.exchangeViewModel.forceDecentralizedExchanges) {
// widget.exchangeViewModel.toggleForceDecentralizedExchanges();
// }
// });
// }
final depositAddressController = depositKey.currentState!.addressController;
final depositAmountController = depositKey.currentState!.amountController;
@@ -1182,13 +1180,14 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
});
if (res != null && res is SwapAddressSelectionResult) {
if (widget.isReceiverCard) {
widget.exchangeViewModel.selectedAddressBookWallet = res.walletInfo;
widget.exchangeViewModel.receiveAddress = res.address!;
if (res.walletName != null) {
if (res.walletInfo?.name != null) {
if (res.accountName != null) {
widget.exchangeViewModel.receiveAddressDisplayName =
"${res.walletName} → ${res.accountName}";
"${res.walletInfo!.name} → ${res.accountName}";
} else {
widget.exchangeViewModel.receiveAddressDisplayName = res.walletName!;
widget.exchangeViewModel.receiveAddressDisplayName = res.walletInfo!.name;
}
}
} else if (res.address == null || res.address!.isEmpty) {
@@ -1196,7 +1195,7 @@ class SwapAmountBoxState extends State<SwapAmountBox> {
askForRefundAddress();
} else {
widget.exchangeViewModel.isSendFromExternal = false;
switchToDepositWallet(res.walletName!);
switchToDepositWallet(res.walletInfo!.name);
}
}
}
@@ -6,7 +6,6 @@ import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
class AssetsTopBar extends StatelessWidget {
const AssetsTopBar({
@@ -98,5 +97,6 @@ class AssetsTopBar extends StatelessWidget {
if (tabs[selectedTab] == S.current.history) {
return S.current.filters;
}
return null;
}
}
@@ -70,23 +70,25 @@ class HistoryTradeTile extends StatelessWidget {
}
String _getIconPath(CryptoCurrency currency) {
if (currency.iconPath != null) {
return currency.iconPath!;
}
if (currency.name.isNotEmpty) {
final currencyFromName = CryptoCurrency.fromString(currency.name);
if (currencyFromName.iconPath != null) {
return currencyFromName.iconPath!;
try { // temporarily until we migrate from hive to sqlite and store the full currency object
if (currency.iconPath != null) {
return currency.iconPath!;
}
}
if (currency.title.isNotEmpty) {
final currencyFromTitle = CryptoCurrency.fromString(currency.title);
if (currencyFromTitle.iconPath != null) {
return currencyFromTitle.iconPath!;
if (currency.name.isNotEmpty) {
final currencyFromName = CryptoCurrency.fromString(currency.name);
if (currencyFromName.iconPath != null) {
return currencyFromName.iconPath!;
}
}
}
if (currency.title.isNotEmpty) {
final currencyFromTitle = CryptoCurrency.fromString(currency.title);
if (currencyFromTitle.iconPath != null) {
return currencyFromTitle.iconPath!;
}
}
} catch (_) {}
//TODO approporiate fallback
return "";
@@ -6,16 +6,16 @@ import 'package:cake_wallet/src/widgets/new_list_row/new_simple_checkbox.dart';
import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart' show WalletType;
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
class SwapAddressSelectionResult {
String? address;
String? walletName;
WalletInfo? walletInfo;
String? accountName;
SwapAddressSelectionResult({this.address, this.walletName, this.accountName});
SwapAddressSelectionResult({this.address, this.walletInfo, this.accountName});
}
class SwapAddressSelectionModal extends StatefulWidget {
@@ -52,8 +52,8 @@ class _SwapAddressSelectionModalState extends State<SwapAddressSelectionModal> {
? await widget.exchangeViewModel.receiveWallets
: await widget.exchangeViewModel.depositWallets;
for (final item in items) {
if (item.type.toString() == "WalletType.monero") {
accounts[item.id] = await widget.exchangeViewModel.addressesForWallet(item);
if (item.type == WalletType.monero) {
accounts[item.id] = await widget.exchangeViewModel.addressesForAccountsWallet(item);
}
}
setState(() {
@@ -107,10 +107,10 @@ class _SwapAddressSelectionModalState extends State<SwapAddressSelectionModal> {
final String currencyIconPath = walletTypeToCryptoCurrency(item.type).iconPath ?? "";
final bool hasAccounts =
item.type.toString() == "WalletType.monero" && widget.isSelectingReceiver;
final bool hasAccounts =
item.type == WalletType.monero && widget.isSelectingReceiver;
List<WalletInfoAddressInfo>? accounts =
List<WalletInfoAddressInfo>? accounts =
hasAccounts ? this.accounts[item.id] : null;
return Padding(
@@ -123,7 +123,7 @@ class _SwapAddressSelectionModalState extends State<SwapAddressSelectionModal> {
onAddressChosen: (address, accountName) {
Navigator.of(context).pop(SwapAddressSelectionResult(
address: address,
walletName: item.name,
walletInfo: item,
accountName: accountName));
},
),
+1 -1
View File
@@ -716,7 +716,7 @@ abstract class SettingsStoreBase with Store {
reaction(
(_) => decentralizedExchangesPromptDismissed,
(bool forceDecentralizedExchanges) => _sharedPreferences.setBool(
(bool decentralizedExchangesPromptDismissed) => _sharedPreferences.setBool(
PreferencesKey.decentralizedExchangesPromptDismissed,
decentralizedExchangesPromptDismissed));
@@ -9,6 +9,8 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/amount_parsing_proxy.dart';
import 'package:cake_wallet/core/create_trade_result.dart';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
import 'package:cake_wallet/core/lightning_invoice_service.dart';
import 'package:cake_wallet/core/utilities.dart';
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
@@ -420,7 +422,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
return await WalletInfo.selectList("type = ?", [type!.index]);
}
Future<List<WalletInfoAddressInfo>> addressesForWallet(WalletInfo wallet) async {
Future<List<WalletInfoAddressInfo>> addressesForAccountsWallet(WalletInfo wallet) async {
final List<WalletInfoAddressInfo> ret = [];
final addresses = await wallet.getAddressInfos();
for (var list in addresses.values) {
@@ -554,6 +556,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
calculateForcedProviderRate();
}
WalletInfo? selectedAddressBookWallet;
@observable
double forcedProviderRate = 0.0;
@@ -964,22 +968,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
}
if (depositCurrency == CryptoCurrency.btcln &&
depositAddress == wallet.walletAddresses.addressForExchange) {
final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero);
if (invoice != null) {
depositAddress = invoice;
}
}
if (receiveCurrency == CryptoCurrency.btcln &&
receiveAddress == wallet.walletAddresses.addressForExchange) {
final invoice = await bitcoin!.getLightningInvoice(wallet, BigInt.zero);
if (invoice != null) {
receiveAddress = invoice;
}
}
late final Map<double, ExchangeProvider> providers;
if (forcedProvider != null) {
providers = {forcedProviderRate: forcedProvider!};
@@ -1000,6 +988,32 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
}
try {
if (selectedAddressBookWallet?.type == WalletType.bitcoin) {
final _walletAddresses = await selectedAddressBookWallet!.getAddresses();
// if receive currency is lightning pick the lightning address
// if normal bitcoin, then pick the segwit address
if (receiveCurrency == CryptoCurrency.btcln) {
final lightningAddressOfWallet =
_walletAddresses.entries.firstWhereOrNull((e) => e.value.contains("LN"))?.key;
if (lightningAddressOfWallet != null) {
receiveAddress = lightningAddressOfWallet;
}
}
if (receiveCurrency == CryptoCurrency.btc) {
final segwitAddressOfWallet =
_walletAddresses.entries.firstWhereOrNull((e) => e.value.contains("P2WPKH"))?.key;
if (segwitAddressOfWallet != null) {
receiveAddress = segwitAddressOfWallet;
}
}
}
// parse Lightning address to bolt11 invoice
if (receiveAddress.contains("@")) {
receiveAddress = await getBolt11FromLightingAddress(receiveAddress) ?? receiveAddress;
}
// snapshot of providers to avoid concurrent modification issues
final providersSnapshot = providers.values.toList();
final ratesSnapshot = providers.keys.toList();
+1 -1
View File
@@ -109,7 +109,7 @@ abstract class OutputBase with Store {
if (fiatAmount.split(".").last.length <= digits) {
return fiatAmount;
}
return double.parse(fiatAmount.replaceAll(",", "")).toStringAsPrecision(digits);
return double.tryParse(fiatAmount.replaceAll(",", ""))?.toStringAsPrecision(digits) ?? '0.0';
}
@observable