What changed, and why it matters
This commit makes the displayed cryptocurrency amount for a buy/sell quote nullable and adds a safer parsing helper. Previously, if a quote's rate produced an unparsable amount, the app could crash when formatting the value on several buy/sell screens. The change lets the app show an empty string instead of crashing. It is a robustness fix for a UI/data-handling edge case, not an obvious security vulnerability.
Treat as a routine stability/UX fix. Review whether `amountForQuote` returning null should block proceeding to payment or show a user-facing error rather than silently rendering an empty amount. No urgent security response is indicated by the diff alone.
Security signals we found
Change from throwing parser to nullable parser reduces crash surface
UI call sites now null-safe against malformed quote rates
No input validation, crypto, or authorization logic changed
No network, storage, or secret-handling code changed
Evidence from the diff
The patch changes BuySellViewModelBase.amountForQuote from returning non-null Money (via Money.safeParse) to nullable Money? (via new Money.trySafeParse). Call sites in confirmation, provider, and redirecting pages now use ?.toStringWithSymbol(...) ?? "" so null amounts render as empty text rather than throwing. A new trySafeParse static method in cw_core/lib/amount/money.dart uses BigInt.tryParse / tryParseFixed and returns null on failure instead of throwing. The provider page also wraps its list in SingleChildScrollView with a ModalScrollController.
Changed components
cw_core/lib/amount/money.dartlib/view_model/buy/buy_sell_view_model.dartlib/new-ui/pages/buy_sell/buy_sell_confirmation_page.dartlib/new-ui/pages/buy_sell/buy_sell_provider_page.dartlib/new-ui/pages/buy_sell/buy_sell_redirecting_page.dartInspect captured patch +54 / −41
diff --git a/cw_core/lib/amount/money.dart b/cw_core/lib/amount/money.dart
index 9ac551da..8ec0793f 100644
--- a/cw_core/lib/amount/money.dart
+++ b/cw_core/lib/amount/money.dart
@@ -45,6 +45,14 @@ class Money implements Comparable<Money> {
return amount != null ? Money(amount, currency) : null;
}
+ static Money? trySafeParse(source, Currency currency, {bool isBaseUnit = false}) {
+ final amount = isBaseUnit
+ ? BigInt.tryParse(source.toString())
+ : tryParseFixed(source.toString().withDecimals(currency.decimals), currency.decimals);
+
+ return amount != null ? Money(amount, currency) : null;
+ }
+
final BigInt amount;
final Currency currency;
diff --git a/lib/new-ui/pages/buy_sell/buy_sell_confirmation_page.dart b/lib/new-ui/pages/buy_sell/buy_sell_confirmation_page.dart
index 851c3c26..7d6c3c7d 100644
--- a/lib/new-ui/pages/buy_sell/buy_sell_confirmation_page.dart
+++ b/lib/new-ui/pages/buy_sell/buy_sell_confirmation_page.dart
@@ -52,7 +52,7 @@ class BuySellConfirmationPage extends StatelessWidget {
style: TextStyle(fontSize: 32),
),
Text(
- "≈ ${buySellViewModel.amountForQuote(buySellViewModel.selectedQuote!).toStringWithSymbol(fractionalDigits: 8)}",
+ "≈ ${buySellViewModel.amountForQuote(buySellViewModel.selectedQuote!)?.toStringWithSymbol(fractionalDigits: 8)}",
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500),
diff --git a/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart b/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart
index 64d9f95a..4a563e3b 100644
--- a/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart
+++ b/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart
@@ -13,6 +13,7 @@ import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
+import "package:modal_bottom_sheet/modal_bottom_sheet.dart";
class BuySellProviderPage extends StatefulWidget {
const BuySellProviderPage({super.key, required this.buySellViewModel});
@@ -88,40 +89,43 @@ class _BuySellProviderPageState extends State<BuySellProviderPage> {
);
}
- return Padding(
- padding: EdgeInsets.symmetric(horizontal: 18),
- child: NewListSections(showHeader: true, sections: {
- "": [
- ListItemRegularRow(
- keyValue: "payment method",
- label: S.of(context).payment_method,
- showArrow: true,
- onTap: () {
- final page =
- BuySellPaymentMethodPage(buySellViewModel: widget.buySellViewModel);
- Navigator.of(context).push(CupertinoPageRoute(
- builder: (context) => Material(
- color: Colors.transparent,
- child: page,
- )));
- },
- trailingText: widget.buySellViewModel.selectedPaymentMethod?.title)
- ],
- S.of(context).available_providers: [
- ...widget.buySellViewModel.sortedRecommendedQuotes.map(quoteListItem),
- if (widget.buySellViewModel.sortedQuotes.isNotEmpty)
- ListItemDropdown(
- keyValue: "more options",
- label: S.of(context).more_options,
+ return SingleChildScrollView(
+ controller: ModalScrollController.of(context),
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 18),
+ child: NewListSections(showHeader: true, sections: {
+ "": [
+ ListItemRegularRow(
+ keyValue: "payment method",
+ label: S.of(context).payment_method,
+ showArrow: true,
onTap: () {
- setState(() {
- _allProvidersExpanded = !_allProvidersExpanded;
- });
- }),
- if (_allProvidersExpanded)
- ...widget.buySellViewModel.sortedQuotes.map(quoteListItem)
- ]
- }),
+ final page =
+ BuySellPaymentMethodPage(buySellViewModel: widget.buySellViewModel);
+ Navigator.of(context).push(CupertinoPageRoute(
+ builder: (context) => Material(
+ color: Colors.transparent,
+ child: page,
+ )));
+ },
+ trailingText: widget.buySellViewModel.selectedPaymentMethod?.title)
+ ],
+ S.of(context).available_providers: [
+ ...widget.buySellViewModel.sortedRecommendedQuotes.map(quoteListItem),
+ if (widget.buySellViewModel.sortedQuotes.isNotEmpty)
+ ListItemDropdown(
+ keyValue: "more options",
+ label: S.of(context).more_options,
+ onTap: () {
+ setState(() {
+ _allProvidersExpanded = !_allProvidersExpanded;
+ });
+ }),
+ if (_allProvidersExpanded)
+ ...widget.buySellViewModel.sortedQuotes.map(quoteListItem)
+ ]
+ }),
+ ),
);
},
))
@@ -156,11 +160,12 @@ class _BuySellProviderPageState extends State<BuySellProviderPage> {
mainAxisAlignment: MainAxisAlignment.center,
spacing: 4,
children: [
- Text(widget.buySellViewModel
- .amountForQuote(quote)
- .toStringWithSymbol(fractionalDigits: 8)),
- if(fiatAmount != null)
- Text(
+ Text(widget.buySellViewModel
+ .amountForQuote(quote)
+ ?.toStringWithSymbol(fractionalDigits: 8) ??
+ ""),
+ if (fiatAmount != null)
+ Text(
"= ${fiatAmount.toStringWithSymbol(fractionalDigits: 2, trimZeros: false)}",
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant))
],
diff --git a/lib/new-ui/pages/buy_sell/buy_sell_redirecting_page.dart b/lib/new-ui/pages/buy_sell/buy_sell_redirecting_page.dart
index c4b614de..bf7d716f 100644
--- a/lib/new-ui/pages/buy_sell/buy_sell_redirecting_page.dart
+++ b/lib/new-ui/pages/buy_sell/buy_sell_redirecting_page.dart
@@ -98,7 +98,7 @@ class _BuySellRedirectingPageState extends State<BuySellRedirectingPage> {
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
Text(
- "${widget.buySellViewModel.fiatAmount} ${widget.buySellViewModel.fiatCurrency} → ${widget.buySellViewModel.amountForQuote(widget.buySellViewModel.selectedQuote!).toStringWithSymbol(fractionalDigits: 8)}")
+ "${widget.buySellViewModel.fiatAmount} ${widget.buySellViewModel.fiatCurrency} → ${widget.buySellViewModel.amountForQuote(widget.buySellViewModel.selectedQuote!)?.toStringWithSymbol(fractionalDigits: 8)}")
],
)
],
diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart
index e9f7a342..66e5c431 100644
--- a/lib/view_model/buy/buy_sell_view_model.dart
+++ b/lib/view_model/buy/buy_sell_view_model.dart
@@ -171,7 +171,7 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S
return maxAmount.toStringAsFixed(2);
}
- Money amountForQuote(Quote quote) => Money.safeParse(
+ Money? amountForQuote(Quote quote) => Money.trySafeParse(
(double.parse(fiatAmount) / quote.rate).toStringAsFixed(min(20, cryptoCurrency.decimals)),
cryptoCurrency);
Why this scored 24/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.