From ab910983a212a07c0d909e9c270966f071ae7f33 Mon Sep 17 00:00:00 2001 From: smk762 Date: Thu, 30 Oct 2025 18:50:04 +0800 Subject: [PATCH 1/4] show transaction details after broadcast --- .../withdraw_form/withdraw_form.dart | 60 ++++++++++--------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart index 2999029a39..77168ba8a2 100644 --- a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart +++ b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart @@ -24,6 +24,7 @@ import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_for import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/fill_form/fields/fill_form_memo.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/trezor_withdraw_progress_dialog.dart'; import 'package:web_dex/views/wallet/coin_details/withdraw_form/widgets/withdraw_form_header.dart'; +import 'package:web_dex/views/wallet/coin_details/transactions/transaction_details.dart'; bool _isMemoSupportedProtocol(Asset asset) { final protocol = asset.protocol; @@ -77,7 +78,7 @@ class _WithdrawFormState extends State { BlocListener( listenWhen: (prev, curr) => prev.step != curr.step && curr.step == WithdrawFormStep.success, - listener: (context, state) { + listener: (context, state) async { final authBloc = context.read(); final walletType = authBloc.state.currentUser?.type ?? ''; context.read().logEvent( @@ -88,7 +89,6 @@ class _WithdrawFormState extends State { hdType: walletType, ), ); - widget.onSuccess(); }, ), BlocListener( @@ -138,6 +138,7 @@ class _WithdrawFormState extends State { ], child: WithdrawFormContent( onBackButtonPressed: widget.onBackButtonPressed, + onSuccess: widget.onSuccess, ), ), ); @@ -146,8 +147,9 @@ class _WithdrawFormState extends State { class WithdrawFormContent extends StatelessWidget { final VoidCallback? onBackButtonPressed; + final VoidCallback onSuccess; - const WithdrawFormContent({this.onBackButtonPressed, super.key}); + const WithdrawFormContent({required this.onSuccess, this.onBackButtonPressed, super.key}); @override Widget build(BuildContext context) { @@ -186,7 +188,7 @@ class WithdrawFormContent extends StatelessWidget { case WithdrawFormStep.confirm: return const WithdrawFormConfirmSection(); case WithdrawFormStep.success: - return const WithdrawFormSuccessSection(); + return WithdrawFormSuccessSection(onDone: onSuccess); case WithdrawFormStep.failed: return const WithdrawFormFailedSection(); } @@ -709,34 +711,38 @@ class WithdrawFormConfirmSection extends StatelessWidget { } class WithdrawFormSuccessSection extends StatelessWidget { - const WithdrawFormSuccessSection({super.key}); + final VoidCallback onDone; + + const WithdrawFormSuccessSection({required this.onDone, super.key}); @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Icon( - Icons.check_circle_outline, - size: 64, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(height: 24), - Text( - LocaleKeys.transactionSuccessful.tr(), - style: Theme.of(context).textTheme.headlineMedium, - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - WithdrawResultDetails(result: state.result!), - const SizedBox(height: 24), - FilledButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(LocaleKeys.done.tr()), - ), - ], + // Build a temporary Transaction model matching history view expectations + final result = state.result!; + final tx = Transaction( + id: result.txHash, + internalId: result.txHash, + assetId: state.asset.id, + balanceChanges: result.balanceChanges, + // Show as unconfirmed initially + timestamp: DateTime.fromMillisecondsSinceEpoch(0), + confirmations: 0, + blockHeight: 0, + from: state.selectedSourceAddress != null + ? [state.selectedSourceAddress!.address] + : [], + to: [result.toAddress], + txHash: result.txHash, + fee: result.fee, + memo: state.memo, + ); + + return TransactionDetails( + transaction: tx, + coin: state.asset.toCoin(), + onClose: onDone, ); }, ); From b27167093dc40ae752e9c5d6d760871ee72c4e7d Mon Sep 17 00:00:00 2001 From: smk762 Date: Thu, 30 Oct 2025 20:16:03 +0800 Subject: [PATCH 2/4] use withdraw preview signed hex for broadcast --- .../withdraw_form/withdraw_form_bloc.dart | 61 ++++++++++++------- .../withdraw_form/withdraw_form.dart | 3 + 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/lib/bloc/withdraw_form/withdraw_form_bloc.dart b/lib/bloc/withdraw_form/withdraw_form_bloc.dart index df30709197..d9c3c4002c 100644 --- a/lib/bloc/withdraw_form/withdraw_form_bloc.dart +++ b/lib/bloc/withdraw_form/withdraw_form_bloc.dart @@ -5,6 +5,8 @@ import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; +import 'package:web_dex/mm2/mm2_api/rpc/send_raw_transaction/send_raw_transaction_request.dart'; import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/services/fd_monitor_service.dart'; @@ -21,12 +23,15 @@ import 'package:decimal/decimal.dart'; class WithdrawFormBloc extends Bloc { final KomodoDefiSdk _sdk; final WalletType? _walletType; + final Mm2Api _mm2Api; WithdrawFormBloc({ required Asset asset, required KomodoDefiSdk sdk, + required Mm2Api mm2Api, WalletType? walletType, }) : _sdk = sdk, + _mm2Api = mm2Api, _walletType = walletType, super( WithdrawFormState( @@ -471,34 +476,48 @@ class WithdrawFormBloc extends Bloc { state.copyWith( isSending: true, transactionError: () => null, + // No second device interaction is needed on confirm isAwaitingTrezorConfirmation: false, ), ); - - // Show Trezor progress message for hardware wallets - if (_walletType == WalletType.trezor) { - emit(state.copyWith(isAwaitingTrezorConfirmation: true)); + final preview = state.preview; + if (preview == null) { + throw Exception('Missing withdrawal preview'); } - await for (final progress in _sdk.withdrawals.withdraw( - state.toWithdrawParameters(), - )) { - if (progress.status == WithdrawalStatus.complete) { - emit( - state.copyWith( - step: WithdrawFormStep.success, - result: () => progress.withdrawalResult, - isSending: false, - isAwaitingTrezorConfirmation: false, - ), - ); - return; - } + final response = await _mm2Api.sendRawTransaction( + SendRawTransactionRequest( + coin: preview.coin, + txHex: preview.txHex, + ), + ); - if (progress.status == WithdrawalStatus.error) { - throw Exception(progress.errorMessage); - } + if (response.txHash == null) { + throw Exception(response.error?.message ?? 'Broadcast failed'); } + + final result = WithdrawalResult( + txHash: response.txHash!, + balanceChanges: preview.balanceChanges, + coin: preview.coin, + toAddress: preview.to.first, + fee: preview.fee, + kmdRewardsEligible: + preview.kmdRewards != null && + Decimal.parse(preview.kmdRewards!.amount) > Decimal.zero, + ); + + emit( + state.copyWith( + step: WithdrawFormStep.success, + result: () => result, + // Clear cached preview after successful broadcast + preview: () => null, + isSending: false, + isAwaitingTrezorConfirmation: false, + ), + ); + return; } catch (e) { // Capture FD snapshot when KDF withdrawal submission fails if (PlatformTuner.isIOS) { diff --git a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart index 2999029a39..ede607261b 100644 --- a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart +++ b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart @@ -14,6 +14,7 @@ import 'package:web_dex/bloc/coins_bloc/asset_coin_extension.dart'; import 'package:web_dex/bloc/withdraw_form/withdraw_form_bloc.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/rpc/base.dart'; +import 'package:web_dex/mm2/mm2_api/mm2_api.dart'; import 'package:web_dex/model/text_error.dart'; import 'package:web_dex/model/wallet.dart'; import 'package:web_dex/shared/utils/extensions/kdf_user_extensions.dart'; @@ -49,6 +50,7 @@ class WithdrawForm extends StatefulWidget { class _WithdrawFormState extends State { late final WithdrawFormBloc _formBloc; late final _sdk = context.read(); + late final _mm2Api = context.read(); @override void initState() { @@ -58,6 +60,7 @@ class _WithdrawFormState extends State { _formBloc = WithdrawFormBloc( asset: widget.asset, sdk: _sdk, + mm2Api: _mm2Api, walletType: walletType, ); } From 1403882f81b4398f4f8ebfd74b2fbfaa7ac03cf3 Mon Sep 17 00:00:00 2001 From: smk762 Date: Thu, 30 Oct 2025 20:33:50 +0800 Subject: [PATCH 3/4] update some wording --- assets/translations/en.json | 2 +- lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/translations/en.json b/assets/translations/en.json index 54764c231b..7c8af814a6 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -63,7 +63,7 @@ "transactionComplete": "Transaction complete!", "transactionDenied": "Denied", "coinDisableSpan1": "You can't disable {} while it has a swap in progress", - "confirmSending": "Confirm sending", + "confirmSending": "Confirm withdrawl", "confirmSend": "Confirm send", "confirm": "Confirm", "confirmed": "Confirmed", diff --git a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart index ede607261b..756f292065 100644 --- a/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart +++ b/lib/views/wallet/coin_details/withdraw_form/withdraw_form.dart @@ -699,7 +699,7 @@ class WithdrawFormConfirmSection extends StatelessWidget { height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) - : Text(LocaleKeys.confirm.tr()), + : Text(LocaleKeys.send.tr()), ), ), ], From d5545c1872a6bb354cb96b093759cc58ab04cc84 Mon Sep 17 00:00:00 2001 From: smk762 Date: Fri, 31 Oct 2025 10:18:44 +0800 Subject: [PATCH 4/4] fix coin variant sum in dropdowns --- .../tables/coins_table/coins_table_item.dart | 7 +- .../orders_table/grouped_list_view.dart | 66 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/lib/views/dex/simple/form/tables/coins_table/coins_table_item.dart b/lib/views/dex/simple/form/tables/coins_table/coins_table_item.dart index 8cefa0c216..4aca3a3cfc 100644 --- a/lib/views/dex/simple/form/tables/coins_table/coins_table_item.dart +++ b/lib/views/dex/simple/form/tables/coins_table/coins_table_item.dart @@ -13,6 +13,7 @@ class CoinsTableItem extends StatelessWidget { required this.coin, this.isGroupHeader = false, this.subtitleText, + this.trailing, }); final T? data; @@ -20,6 +21,7 @@ class CoinsTableItem extends StatelessWidget { final Function(T) onSelect; final bool isGroupHeader; final String? subtitleText; + final Widget? trailing; @override Widget build(BuildContext context) { @@ -35,7 +37,10 @@ class CoinsTableItem extends StatelessWidget { showNetworkLogo: !isGroupHeader, ), const SizedBox(width: 8), - if (coin.isActive) CoinBalance(coin: coin, isVertical: true), + if (trailing != null) + trailing! + else if (coin.isActive) + CoinBalance(coin: coin, isVertical: true), ], ), ); diff --git a/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart b/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart index 1958deb110..51447c2e12 100644 --- a/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart +++ b/lib/views/dex/simple/form/tables/orders_table/grouped_list_view.dart @@ -8,6 +8,8 @@ import 'package:web_dex/common/screen.dart'; import 'package:web_dex/generated/codegen_loader.g.dart'; import 'package:web_dex/mm2/mm2_api/rpc/best_orders/best_orders.dart'; import 'package:web_dex/model/coin.dart'; +import 'package:web_dex/shared/utils/formatters.dart'; +import 'package:web_dex/shared/utils/utils.dart'; import 'package:web_dex/views/dex/simple/form/tables/coins_table/coins_table_item.dart'; class GroupedListView extends StatelessWidget { @@ -64,6 +66,11 @@ class GroupedListView extends StatelessWidget { subtitleText: LocaleKeys.nNetworks.tr( args: [group.value.length.toString()], ), + trailing: _GroupedCoinBalance( + coins: group.value + .map((item) => getCoin(context, item)) + .toList(), + ), ), children: group.value .map((item) => buildItem(context, item, onSelect)) @@ -128,3 +135,62 @@ class GroupedListView extends StatelessWidget { } } } + +class _GroupedCoinBalance extends StatelessWidget { + const _GroupedCoinBalance({required this.coins}); + + final List coins; + + @override + Widget build(BuildContext context) { + final baseFont = Theme.of(context).textTheme.bodySmall; + final balanceStyle = baseFont?.copyWith(fontWeight: FontWeight.w500); + + // Sum on-chain spendable balances for all variants + double totalSpendable = 0.0; + for (final coin in coins) { + totalSpendable += + context.sdk.balances.lastKnown(coin.id)?.spendable.toDouble() ?? 0.0; + } + + // Sum USD balances (uses cached lastKnown data) + double totalUsd = 0.0; + for (final coin in coins) { + totalUsd += coin.lastKnownUsdBalance(context.sdk) ?? 0.0; + } + + // Show base ticker without protocol/segwit suffixes in group header + final abbr = abbr2Ticker(coins.first.abbr); + + final children = [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: AutoScrollText( + text: doubleToString(totalSpendable), + style: balanceStyle, + textAlign: TextAlign.right, + ), + ), + Text(' $abbr', style: balanceStyle), + ], + ), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 100), + child: Text( + totalUsd > 0 ? ' (${formatUsdValue(totalUsd)})' : '', + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ]; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ); + } +}