From 682db4b3cd825f09e708203352aad480219387d3 Mon Sep 17 00:00:00 2001 From: hmou5008 Date: Tue, 28 Oct 2025 16:16:18 +0800 Subject: [PATCH 1/2] add express fee --- lib/common/transaction.dart | 18 +- .../desktop_transaction_detail_page.dart | 4 +- lib/desktop/main_page.dart | 190 +++++++++++++++++- lib/l10n/app_de.arb | 6 +- lib/l10n/app_en.arb | 6 +- lib/l10n/app_es.arb | 6 +- lib/l10n/app_fr.arb | 6 +- lib/l10n/app_it.arb | 6 +- lib/l10n/app_ja.arb | 6 +- lib/l10n/app_ru.arb | 6 +- lib/l10n/app_zh.arb | 7 +- lib/page/detail/send_page.dart | 186 ++++++++++++++++- lib/page/detail/transaction_page.dart | 8 +- 13 files changed, 427 insertions(+), 28 deletions(-) diff --git a/lib/common/transaction.dart b/lib/common/transaction.dart index 2ccf6f3..ee0bd3a 100644 --- a/lib/common/transaction.dart +++ b/lib/common/transaction.dart @@ -41,7 +41,7 @@ class TransactionHelper { //无 nonce、有 remark(成块补零) static const int padZerosNoNonceHasRemark = 18; - static String getTransaction(String fromAddress, String toAddress, String remark, double value, bip32.BIP32 wallet, String nonce) { + static String getTransaction(String fromAddress, String toAddress, String remark, double value, bip32.BIP32 wallet, String nonce, double fee) { // print('getTransaction: $fromAddress, $toAddress, $remark, $value, $nonce'); bool isPubKeyEven = wallet.publicKey[0] % 2 == 0; String from = checkBase58Address(fromAddress); @@ -71,7 +71,8 @@ class TransactionHelper { } sb += HEX.encode(timeBytes.buffer.asUint8List()); - sb += "0000000000000000"; + // 添加可变手续费 + sb += fee2Bytes(fee); // print('header: $sb'); // nonce:前面补 48 个 0 // 由于rpc查询出来的nonce(rpc查出来的到的结果是String类型),会放在该32字节的后八个字段,然后前面24个字节的零,这后八个字节存放nonce的方式是小端序存放。 @@ -146,6 +147,19 @@ class TransactionHelper { return res + amount; } + static String fee2Bytes(double fee) { + final amountValue = (fee * 1000000000).round(); + + final valBytes = Uint8List(8); + ByteData.view(valBytes.buffer).setUint64(0, amountValue, Endian.little); + + final hexString = valBytes.map((byte) { + return byte.toRadixString(16).padLeft(2, '0'); + }).join(); + + return hexString; + } + static Int64 getCurrentTimestamp() { var t0 = DateTime.now().toUtc().millisecondsSinceEpoch * 1000000; Int64 t = Int64(t0); diff --git a/lib/desktop/desktop_transaction_detail_page.dart b/lib/desktop/desktop_transaction_detail_page.dart index da75602..f509e70 100644 --- a/lib/desktop/desktop_transaction_detail_page.dart +++ b/lib/desktop/desktop_transaction_detail_page.dart @@ -162,7 +162,7 @@ class DesktopTransactionDetailPageWidgetState extends State { final TextEditingController controller0 = TextEditingController(); final TextEditingController controller1 = TextEditingController(); final TextEditingController controller2 = TextEditingController(); + final TextEditingController controller3 = TextEditingController(); + String address = ""; String amount = ""; String remark = ""; + String fee = ""; + String averageFee = "0.1 XDAG"; // 平均手续费 + bool load = false; + bool showFeeBox = false; // 控制是否显示自定义手续费输入框 + bool isLoadingAverageFee = false; // 控制平均手续费加载状态 Isolate? isolate; final dio = Dio(); @@ -522,6 +529,7 @@ class _SendCardState extends State { @override void initState() { super.initState(); + fetchAverageFee(); // 调用 RPC 方法(页面初始化时触发) } @override @@ -532,6 +540,45 @@ class _SendCardState extends State { super.dispose(); } + Future fetchAverageFee() async { + setState(() { + isLoadingAverageFee = true; + }); + try { + ConfigModal config = Provider.of(context, listen: false); + String rpcURL = config.getCurrentRpc(); + Response response = await dio.post( + rpcURL, + cancelToken: cancelToken, + data: { + "jsonrpc": "2.0", + "method": "xdag_getAverageFee", + "params": [], + "id": 1 + }, + ); + if (response.data != null && response.data['result'] != null) { + double feeValue = double.tryParse(response.data['result'].toString()) ?? 0.0; + setState(() { + averageFee = "${feeValue.toStringAsFixed(2)} XDAG"; + }); + } else { + setState(() { + averageFee = "0.10 XDAG"; + }); + } + } catch (e) { + debugPrint("获取平均手续费失败:$e"); + setState(() { + averageFee = "0.10 XDAG"; + }); + } finally { + setState(() { + isLoadingAverageFee = false; + }); + } + } + static void isolateFunction(SendPort sendPort) async { final receivePort = ReceivePort(); sendPort.send(receivePort.sendPort); @@ -543,9 +590,11 @@ class _SendCardState extends State { String fromAddress = data[3] as String; String remark = data[4] as String; String nonce = data[5] as String; + String fee = data[6] as String; + bool isPrivateKey = res.trim().split(' ').length == 1; bip32.BIP32 wallet = Helper.createWallet(isPrivate: isPrivateKey, content: res); - String result = TransactionHelper.getTransaction(fromAddress, toAddress, remark, double.parse(amount), wallet, nonce); + String result = TransactionHelper.getTransaction(fromAddress, toAddress, remark, double.parse(amount), wallet, nonce, double.parse(fee)); sendPort.send(['success', result]); }); } @@ -569,9 +618,11 @@ class _SendCardState extends State { receivePort.listen((data) async { var sendAmount = amount; var sendRemark = remark; + var sendFee = fee; + if (data is SendPort) { var subSendPort = data; - subSendPort.send([res, toAddress, amount, fromAddress, remark, nonce]); + subSendPort.send([res, toAddress, amount, fromAddress, remark, nonce, fee]); } else if (data is List) { String result = data[1]; try { @@ -587,15 +638,18 @@ class _SendCardState extends State { var res = response.data['result'] as String; // print(res); if (res.length == 32 && res.trim().split(' ').length == 1) { - var transactionItem = Transaction(time: '', amount: Helper.removeTrailingZeros(sendAmount.toString()), address: fromAddress, status: 'pending', from: fromAddress, to: toAddress, type: 0, hash: '', fee: 0, blockAddress: res, remark: sendRemark); + var transactionItem = Transaction(time: '', amount: Helper.removeTrailingZeros(sendAmount.toString()), address: fromAddress, status: 'pending', from: fromAddress, to: toAddress, type: 0, hash: '', fee: double.parse(sendFee), blockAddress: res, remark: sendRemark); + controller0.clear(); controller1.clear(); controller2.clear(); + controller3.clear(); setState(() { load = false; amount = ''; remark = ''; address = ''; + fee = ''; }); showDialog(context: context, builder: (BuildContext context) => DesktopTransactionDetailPageWidget(transaction: transactionItem, address: fromAddress)); @@ -610,11 +664,13 @@ class _SendCardState extends State { controller0.clear(); controller1.clear(); controller2.clear(); + controller3.clear(); setState(() { load = false; amount = ''; remark = ''; address = ''; + fee = ''; }); } } @@ -656,10 +712,12 @@ class _SendCardState extends State { ), Expanded( flex: 1, - child: Padding( + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.fromLTRB(15, 0, 15, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 10), Text(AppLocalizations.of(context)!.to, style: Helper.fitChineseFont(context, const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w500))), @@ -723,7 +781,7 @@ class _SendCardState extends State { ], decoration: InputDecoration( filled: true, - contentPadding: const EdgeInsets.fromLTRB(15, 40, 15, 40), + contentPadding: const EdgeInsets.fromLTRB(15, 30, 15, 30), fillColor: DarkColors.bgColor, hintText: 'XDAG', hintStyle: Helper.fitChineseFont(context, const TextStyle(decoration: TextDecoration.none, fontSize: 32, fontWeight: FontWeight.w500, color: Colors.white54)), @@ -731,6 +789,105 @@ class _SendCardState extends State { focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: DarkColors.mainColor, width: 1), borderRadius: BorderRadius.all(Radius.circular(10))), ), ), + // 控制手续费框显示 + const SizedBox(height: 15), + Row( + children: [ + Checkbox( + value: showFeeBox, + onChanged: (value) { + setState(() { + showFeeBox = value ?? false; + if (!showFeeBox) { + fee = ""; + controller3.clear(); + } + }); + }, + activeColor: DarkColors.mainColor, + checkColor: Colors.white, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context)!.express_fee, + style: Helper.fitChineseFont(context, const TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w500, + )), + ), + const SizedBox(height: 4), + Text( + "${AppLocalizations.of(context)!.average_fee}: $averageFee", + style: Helper.fitChineseFont(context, const TextStyle( + fontSize: 14, + color: Colors.white54, + )), + ), + ], + ), + ], + ), + // 手续费框 + if (showFeeBox) ...[ + const SizedBox(height: 10), + Container( + constraints: const BoxConstraints(maxHeight: 150), + decoration: BoxDecoration( + color: DarkColors.bgColor, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: DarkColors.mainColor, width: 1), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + AutoSizeTextField( + controller: controller3, + onChanged: (value) { + setState(() => fee = value); + }, + minFontSize: 16, + maxLines: null, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + keyboardAppearance: Brightness.dark, + style: Helper.fitChineseFont(context, const TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w500, + )), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')), + ], + decoration: InputDecoration( + filled: true, + fillColor: DarkColors.blockColor, + hintText: AppLocalizations.of(context)!.fee, + hintStyle: const TextStyle(color: Colors.white54), + border: const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + const SizedBox(height: 10), + Text( + "${AppLocalizations.of(context)!.fee_explanation}", + style: Helper.fitChineseFont(context, const TextStyle( + fontSize: 14, + color: Colors.white70, + height: 1.5, + )), + ), + ], + ), + ), + ), + ], const SizedBox(height: 15), Text(AppLocalizations.of(context)!.remark, style: Helper.fitChineseFont(context, const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w500))), const SizedBox(height: 10), @@ -767,7 +924,7 @@ class _SendCardState extends State { focusedBorder: const OutlineInputBorder(borderSide: BorderSide(color: DarkColors.mainColor, width: 1), borderRadius: BorderRadius.all(Radius.circular(10))), ), ), - const Spacer(), + const SizedBox(height: 30), Row( children: [ const Spacer(), @@ -783,9 +940,26 @@ class _SendCardState extends State { showDialog(context: context, builder: (context) => DesktopAlertModal(title: AppLocalizations.of(context)!.error, content: AppLocalizations.of(context)!.walletAddressError)); return; } + if (showFeeBox) { + // 必须输入有效数字 + if (fee == '' || fee.isEmpty || double.tryParse(fee) == null) { + if (context.mounted) { + showDialog( + context: context, + builder: (context) => DesktopAlertModal( + title: AppLocalizations.of(context)!.error, + content: AppLocalizations.of(context)!.invalid_fee, + ), + ); + } + return; + } + } else { + fee = '0'; + } WalletModal walletModal = Provider.of(context, listen: false); Wallet wallet = walletModal.getWallet(); - var transactionItem = Transaction(time: '', amount: Helper.removeTrailingZeros(amount.toString()), address: wallet.address, status: 'pending', from: wallet.address, to: address, type: 0, hash: '', fee: 0, blockAddress: "", remark: remark); + var transactionItem = Transaction(time: '', amount: Helper.removeTrailingZeros(amount.toString()), address: wallet.address, status: 'pending', from: wallet.address, to: address, type: 0, hash: '', fee: double.parse(fee), blockAddress: "", remark: remark); var f = await showDialog( context: context, builder: (context) => DesktopTransactionDetail(transaction: transactionItem), @@ -829,4 +1003,4 @@ class _SendCardState extends State { ], )); } -} +} \ No newline at end of file diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 9e83daa..0296a21 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -150,5 +150,9 @@ "transactions": "Transaktionen", "forget_the_password": "Passwort vergessen", "time_zone": "Zeitzone", - "transactions_in_progress": "Transaktionen in Arbeit" + "transactions_in_progress": "Transaktionen in Arbeit", + "express_fee": "Variable Gebühr", + "invalid_fee": "Bitte geben Sie eine gültige Gebühr ein", + "average_fee": "Durchschnittliche Gebühr", + "fee_explanation": "Gebührenerklärung:\n1. Zur Grundgebühr von 0,1 XDAG müssen Eingabegebühren hinzugerechnet werden\n2. Je höher die Gebühr, desto schneller die Transaktionsbestätigung" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c6af5d4..d8121f0 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -150,5 +150,9 @@ "transactions": "Transactions", "forget_the_password": "forget_the_password", "time_zone": "Time Zone", - "transactions_in_progress": "Transactions in progress" + "transactions_in_progress": "Transactions in progress", + "express_fee": "Expression Fee", + "invalid_fee": "Please enter a invalid fee", + "average_fee": "Average Fee", + "fee_explanation": "Fee Explanation:\n1. Input fees must be added to the base fee of 0.1 XDAG\n2. The higher the fee, the faster the transaction confirmation speed" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 23d7812..eadf1da 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -150,5 +150,9 @@ "transactions": "Transacciones", "forget_the_password": "Olvidé la contraseña", "time_zone": "Zona horaria", - "transactions_in_progress": "Transacciones en progreso" + "transactions_in_progress": "Transacciones en progreso", + "express_fee": "Tarifa exprés", + "invalid_fee": "Por favor, introduzca una tarifa válida", + "average_fee": "Tarifa promedio", + "fee_explanation": "Explicación de la tarifa:\n1. Las tarifas de entrada deben añadirse a la tarifa base de 0,1 XDAG.\n2. Cuanto mayor sea la tarifa, más rápida será la confirmación de la transacción." } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 2701fb5..22fcc3d 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -150,5 +150,9 @@ "transactions": "Transactions", "forget_the_password": "Mot de passe oublié", "time_zone": "Fuseau horaire", - "transactions_in_progress": "Transactions en cours" + "transactions_in_progress": "Transactions en cours", + "express_fee": "Frais d'expression", + "invalid_fee": "Veuillez saisir des frais valides", + "average_fee": "Frais moyens", + "fee_explanation": "Explication des frais :\n1. Les frais de saisie doivent être ajoutés aux frais de base de 0,1 XDAG.\n2. Plus les frais sont élevés, plus la confirmation de la transaction est rapide." } \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index d25d861..d46ddfc 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -150,5 +150,9 @@ "transactions": "Transazioni", "forget_the_password": "Dimentica la password", "time_zone": "Fuso orario", - "transactions_in_progress": "Transazioni in corso" + "transactions_in_progress": "Transazioni in corso", + "express_fee": "Tassa di espressione", + "invalid_fee": "Inserisci una tariffa valida", + "average_fee": "Commissione media", + "fee_explanation": "Spiegazione delle commissioni:\n1. Le commissioni di input devono essere aggiunte alla commissione base di 0,1 XDAG.\n2. Maggiore è la commissione, maggiore è la velocità di conferma della transazione." } \ No newline at end of file diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 4208a08..55fd997 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -150,5 +150,9 @@ "transactions": "取引", "forget_the_password": "パスワードを忘れた", "time_zone": "タイムゾーン", - "transactions_in_progress": "取引中" + "transactions_in_progress": "取引中", + "express_fee": "可変手数料", + "invalid_fee": "有効な手数料を入力してください", + "average_fee": "平均手数料", + "fee_explanation": "手数料の説明:\n1. 入力手数料は0.1 XDAGの基本手数料に加算されます\n2. 手数料が高いほど、取引の確認速度が速くなります" } \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 10aea9a..3c23355 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -150,5 +150,9 @@ "transactions": "Транзакции", "forget_the_password": "Забыли пароль", "time_zone": "Часовой пояс", - "transactions_in_progress": "Транзакции в процессе" + "transactions_in_progress": "Транзакции в процессе", + "express_fee": "Вариабельная комиссия", + "invalid_fee": "Пожалуйста, введите действительную сумму сбора", + "average_fee": "Средняя комиссия", + "fee_explanation": "Пояснение к комиссии:\n1. Комиссия за ввод данных должна быть добавлена ​​к базовой комиссии в размере 0,1 XDAG\n2. Чем выше комиссия, тем выше скорость подтверждения транзакции." } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 8545df9..b6ae0c4 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -150,5 +150,10 @@ "transactions": "交易", "forget_the_password": "忘记密码", "time_zone": "时区", - "transactions_in_progress": "交易进行中" + "transactions_in_progress": "交易进行中", + "express_fee": "可变手续费", + "invalid_fee": "请输入有效的手续费", + "average_fee": "平均手续费", + "fee_explanation": "手续费说明:\n1. 输入费用需加上基础费用 0.1 XDAG\n2. 手续费越高,交易确认速度越快" + } \ No newline at end of file diff --git a/lib/page/detail/send_page.dart b/lib/page/detail/send_page.dart index 7490c66..cb7abce 100644 --- a/lib/page/detail/send_page.dart +++ b/lib/page/detail/send_page.dart @@ -43,6 +43,12 @@ class SendPage extends StatefulWidget { class _SendPageState extends State { late TextEditingController controller; late TextEditingController controller2; + late TextEditingController controller3; + + String fee = ""; + bool showFeeBox = false; + String averageFee = "0.10 XDAG"; + bool isLoadingAverageFee = false; String amount = ""; String remark = ""; String error = ""; @@ -56,6 +62,7 @@ class _SendPageState extends State { super.initState(); controller = TextEditingController(); controller2 = TextEditingController(); + controller3 = TextEditingController(); WidgetsBinding.instance.addPostFrameCallback((_) async { SendPageRouteParams args = SendPageRouteParams(address: ''); @@ -68,6 +75,7 @@ class _SendPageState extends State { amount = args.amount; remark = args.remark; }); + fetchAverageFee(); }); } @@ -79,6 +87,46 @@ class _SendPageState extends State { super.dispose(); } + Future fetchAverageFee() async { + setState(() { + isLoadingAverageFee = true; + }); + try { + ConfigModal config = Provider.of(context, listen: false); + String rpcURL = config.getCurrentRpc(); + + Response response = await dio.post( + rpcURL, + cancelToken: cancelToken, + data: { + "jsonrpc": "2.0", + "method": "xdag_getAverageFee", + "params": [], + "id": 1 + }, + ); + if (response.data != null && response.data['result'] != null) { + double feeValue = double.tryParse(response.data['result'].toString()) ?? 0.0; + setState(() { + averageFee = "${feeValue.toStringAsFixed(2)} XDAG"; + }); + } else { + setState(() { + averageFee = "0.10 XDAG"; + }); + } + } catch (e) { + debugPrint("获取平均手续费失败:$e"); + setState(() { + averageFee = "0.10 XDAG"; // 异常时用默认值 + }); + } finally { + setState(() { + isLoadingAverageFee = false; + }); + } + } + static void isolateFunction(SendPort sendPort) async { final receivePort = ReceivePort(); sendPort.send(receivePort.sendPort); @@ -90,9 +138,13 @@ class _SendPageState extends State { String fromAddress = data[3] as String; String remark = data[4] as String; String nonce = data[5] as String; + String fee = data[6] as String; + bool isPrivateKey = res.trim().split(' ').length == 1; bip32.BIP32 wallet = Helper.createWallet(isPrivate: isPrivateKey, content: res); - String result = TransactionHelper.getTransaction(fromAddress, toAddress, remark, double.parse(amount), wallet, nonce); + + String result = TransactionHelper.getTransaction(fromAddress, toAddress, remark, double.parse(amount), wallet, nonce, double.parse(fee)); + sendPort.send(['success', result]); }); } @@ -121,9 +173,11 @@ class _SendPageState extends State { receivePort.listen((data) async { var sendAmount = amount; var sendRemark = remark; + var sendFee = fee; + if (data is SendPort) { var subSendPort = data; - subSendPort.send([res, toAddress, amount, fromAddress, remark, nonce]); + subSendPort.send([res, toAddress, amount, fromAddress, remark, nonce, fee]); } else if (data is List) { String result = data[1]; // print('result: $result'); @@ -143,15 +197,17 @@ class _SendPageState extends State { var res = response.data['result'] as String; // 把内容存在 localstorage,返回列表的时候,从 localstorage 中获取 if (res.length == 32 && res.trim().split(' ').length == 1) { - var transactionItem = Transaction(time: DateTime.now().toIso8601String(), amount: Helper.removeTrailingZeros(sendAmount.toString()), address: fromAddress, status: 'pending', from: fromAddress, to: toAddress, type: 0, hash: '', fee: 0.1, blockAddress: res, remark: sendRemark); + var transactionItem = Transaction(time: DateTime.now().toIso8601String(), amount: Helper.removeTrailingZeros(sendAmount.toString()), address: fromAddress, status: 'pending', from: fromAddress, to: toAddress, type: 0, hash: '', fee: double.parse(sendFee), blockAddress: res, remark: sendRemark); TransactionModal transactionModal = Provider.of(context, listen: false); transactionModal.addTransaction(transactionItem, fromAddress); controller.clear(); controller2.clear(); + controller3.clear(); setState(() { isLoad = false; amount = ''; remark = ''; + fee = ''; }); Helper.changeAndroidStatusBar(true); @@ -191,9 +247,11 @@ class _SendPageState extends State { setState(() { error = res; isLoad = false; + showFeeBox = false; }); controller.clear(); controller2.clear(); + controller3.clear(); } } } on DioException catch (e) { @@ -364,6 +422,114 @@ class _SendPageState extends State { ], ), ), + const SizedBox(height: 15), + Row( + children: [ + Checkbox( + value: showFeeBox, + onChanged: args.isFromScanQR // 扫码场景禁用复选框 + ? null + : (value) { + setState(() { + showFeeBox = value ?? false; + if (!showFeeBox) { + fee = ""; + controller3.clear(); + } + }); + }, + activeColor: DarkColors.mainColor, + checkColor: Colors.white, + ), + Text( + AppLocalizations.of(context)!.express_fee, + style: Helper.fitChineseFont( + context, + const TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + const Spacer(), + Text( + "${AppLocalizations.of(context)!.average_fee}: $averageFee", + style: Helper.fitChineseFont( + context, + const TextStyle( + fontSize: 14, + color: Colors.white54, + ), + ), + ), + ], + ), + if (showFeeBox && !args.isFromScanQR) ...[ + const SizedBox(height: 10), + Container( + constraints: const BoxConstraints(maxHeight: 150), + decoration: BoxDecoration( + color: DarkColors.bgColor, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: DarkColors.mainColor, width: 1), + ), + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + AutoSizeTextField( + controller: controller3, + onChanged: (value) { + setState(() => fee = value); + }, + minFontSize: 16, + maxLines: null, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + keyboardAppearance: Brightness.dark, + enabled: !args.isFromScanQR, + style: Helper.fitChineseFont( + context, + const TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')), + ], + decoration: InputDecoration( + filled: true, + fillColor: DarkColors.blockColor, + hintText: AppLocalizations.of(context)!.fee, + hintStyle: const TextStyle(color: Colors.white54), + border: const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(8)), + ), + ), + ), + const SizedBox(height: 10), + Text( + AppLocalizations.of(context)!.fee_explanation, + style: Helper.fitChineseFont( + context, + const TextStyle( + fontSize: 14, + color: Colors.white70, + height: 1.5, + ), + ), + ), + ], + ), + ), + ), + ], Text(AppLocalizations.of(context)!.remark, style: Helper.fitChineseFont(context, const TextStyle(fontSize: 16, color: Colors.white, fontWeight: FontWeight.w500))), const SizedBox(height: 15), AutoSizeTextField( @@ -440,6 +606,18 @@ class _SendPageState extends State { }); return; } + if (showFeeBox) { + // 勾选了自定义手续费:必须输入有效数字 + if (fee == '' || fee.isEmpty || double.tryParse(fee) == null) { + setState(() { + error = 'Fee must be greater than 0'; + }); + return; + } + } else { + // 未勾选:fee为0 + fee = '0'; + } if (isLoad) return; setState(() { error = ''; @@ -452,7 +630,7 @@ class _SendPageState extends State { if (context.mounted) { Helper.changeAndroidStatusBar(true); - var transactionItem = Transaction(time: '', amount: Helper.removeTrailingZeros(amount.toString()), address: wallet.address, status: 'pending', from: wallet.address, to: args.address, type: 0, hash: '', fee: 0.1, blockAddress: "", remark: remark); + var transactionItem = Transaction(time: '', amount: Helper.removeTrailingZeros(amount.toString()), address: wallet.address, status: 'pending', from: wallet.address, to: args.address, type: 0, hash: '', fee: double.parse(fee), blockAddress: "", remark: remark); bool? flag = await Helper.showBottomSheet( context, TransactionShowDetail(transaction: transactionItem), diff --git a/lib/page/detail/transaction_page.dart b/lib/page/detail/transaction_page.dart index 4975ea0..1bc2300 100644 --- a/lib/page/detail/transaction_page.dart +++ b/lib/page/detail/transaction_page.dart @@ -71,8 +71,8 @@ class _TransactionPageState extends State { for (var i = 0; i < response.data["block_as_transaction"].length; i++) { var item = response.data["block_as_transaction"][i]; if (item['direction'] == "fee") { - //newFee = item['amount']; - newFee = '0.1'; + newFee = item['amount']; + // newFee = '0.1'; } else { if (isSend) { if (item['direction'] == "output") { @@ -218,7 +218,7 @@ class _TransactionPageState extends State { Helper.showToast(context, AppLocalizations.of(context)!.copied_to_clipboard); }), const SizedBox(height: 1), - TransactionButton(showCopy: false, title: AppLocalizations.of(context)!.fee, value: '$fee XDAG', borderRadius: transaction.remark.isNotEmpty ? BorderRadius.zero : const BorderRadius.only(bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8))), + TransactionButton(showCopy: false, title: AppLocalizations.of(context)!.fee, value: '${transaction.fee + 0.1} XDAG', borderRadius: transaction.remark.isNotEmpty ? BorderRadius.zero : const BorderRadius.only(bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8))), const SizedBox(height: 1), if (transaction.remark.isNotEmpty) TransactionButton( @@ -314,7 +314,7 @@ class TransactionShowDetail extends StatelessWidget { value: transaction.from, ), const SizedBox(height: 1), - TransactionButton(showCopy: false, title: AppLocalizations.of(context)!.fee, value: '${transaction.fee} XDAG', borderRadius: transaction.remark.isNotEmpty ? BorderRadius.zero : const BorderRadius.only(bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8))), + TransactionButton(showCopy: false, title: AppLocalizations.of(context)!.fee, value: '${transaction.fee + 0.1} XDAG', borderRadius: transaction.remark.isNotEmpty ? BorderRadius.zero : const BorderRadius.only(bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8))), const SizedBox(height: 1), if (transaction.remark.isNotEmpty) TransactionButton( From 1bb2525554f2514d0e6c5a957edbf7106f4faa1e Mon Sep 17 00:00:00 2001 From: hmou5008 Date: Thu, 4 Jun 2026 10:24:53 +0800 Subject: [PATCH 2/2] Bug fixes --- .../desktop_transaction_detail_page.dart | 3 +- lib/desktop/main_page.dart | 112 +++++++++++------ lib/page/detail/send_page.dart | 119 +++++++++++------- lib/page/detail/transaction_page.dart | 3 +- lib/page/detail/wallet_list_page.dart | 2 +- 5 files changed, 159 insertions(+), 80 deletions(-) diff --git a/lib/desktop/desktop_transaction_detail_page.dart b/lib/desktop/desktop_transaction_detail_page.dart index f509e70..169a6be 100644 --- a/lib/desktop/desktop_transaction_detail_page.dart +++ b/lib/desktop/desktop_transaction_detail_page.dart @@ -94,6 +94,7 @@ class DesktopTransactionDetailPageWidgetState extends State(context); ContactsItem otherContact = contacts.contactsList.firstWhere((element) => element.address == otherAddress, orElse: () => ContactsItem("", otherAddress)); return DesktopModalFrame( @@ -162,7 +163,7 @@ class DesktopTransactionDetailPageWidgetState extends State { sendPort.send(receivePort.sendPort); receivePort.listen((data) async { - String res = data[0] as String; - String toAddress = data[1] as String; - String amount = data[2] as String; - String fromAddress = data[3] as String; - String remark = data[4] as String; - String nonce = data[5] as String; - String fee = data[6] as String; - - bool isPrivateKey = res.trim().split(' ').length == 1; - bip32.BIP32 wallet = Helper.createWallet(isPrivate: isPrivateKey, content: res); - String result = TransactionHelper.getTransaction(fromAddress, toAddress, remark, double.parse(amount), wallet, nonce, double.parse(fee)); - sendPort.send(['success', result]); + try { + String res = data[0] as String; + String toAddress = data[1] as String; + String amount = data[2] as String; + String fromAddress = data[3] as String; + String remark = data[4] as String; + String nonce = data[5] as String; + String fee = data[6] as String; + + bool isPrivateKey = res.trim().split(' ').length == 1; + bip32.BIP32 wallet = Helper.createWallet(isPrivate: isPrivateKey, content: res); + String result = TransactionHelper.getTransaction(fromAddress, toAddress, remark, double.parse(amount), wallet, nonce, double.parse(fee)); + sendPort.send(['success', result]); + } catch (e) { + sendPort.send(['error', e.toString()]); + } finally { + receivePort.close(); + } }); } + void _handleSendFailure(Object error, String rpcURL) { + debugPrint('Transaction send failed via $rpcURL: $error'); + if (!mounted) return; + setState(() { + load = false; + }); + showDialog( + context: context, + builder: (BuildContext context) => DesktopAlertModal( + title: AppLocalizations.of(context)!.error, + content: error.toString().replaceFirst('Exception: ', ''), + ), + ); + } + void send(String res, String toAddress, String fromAddress) async { setState(() { load = true; @@ -607,14 +629,31 @@ class _SendCardState extends State { ConfigModal config = Provider.of(context, listen: false); String rpcURL = config.getCurrentRpc(); String nonce = ''; - Response response = await dio.post(rpcURL, cancelToken: cancelToken, data: { - "jsonrpc": "2.0", - "method": "xdag_getTransactionNonce", - "params": [fromAddress], - "id": 1 + try { + Response response = await dio + .post(rpcURL, cancelToken: cancelToken, data: { + "jsonrpc": "2.0", + "method": "xdag_getTransactionNonce", + "params": [fromAddress], + "id": 1 + }) + .timeout(const Duration(seconds: 15)); + var nonceResult = response.data?['result']; + if (nonceResult is! String || nonceResult.isEmpty) { + throw Exception(response.data?['error'] ?? 'Invalid nonce response'); + } + nonce = nonceResult; + isolate = await Isolate.spawn(isolateFunction, receivePort.sendPort); + } catch (e) { + receivePort.close(); + _handleSendFailure(e, rpcURL); + return; + } + final sendTimeout = Timer(const Duration(seconds: 45), () { + isolate?.kill(priority: Isolate.immediate); + receivePort.close(); + _handleSendFailure('Transaction signing timed out', rpcURL); }); - nonce = response.data['result'] as String; - isolate = await Isolate.spawn(isolateFunction, receivePort.sendPort); receivePort.listen((data) async { var sendAmount = amount; var sendRemark = remark; @@ -624,14 +663,23 @@ class _SendCardState extends State { var subSendPort = data; subSendPort.send([res, toAddress, amount, fromAddress, remark, nonce, fee]); } else if (data is List) { + sendTimeout.cancel(); + if (data.length < 2 || data[0] != 'success') { + _handleSendFailure(data.length > 1 ? data[1] : 'Transaction signing failed', rpcURL); + isolate?.kill(priority: Isolate.immediate); + receivePort.close(); + return; + } String result = data[1]; try { - Response response = await dio.post(rpcURL, cancelToken: cancelToken, data: { - "jsonrpc": "2.0", - "method": "xdag_sendRawTransaction", - "params": [result], - "id": 1 - }); + Response response = await dio + .post(rpcURL, cancelToken: cancelToken, data: { + "jsonrpc": "2.0", + "method": "xdag_sendRawTransaction", + "params": [result], + "id": 1 + }) + .timeout(const Duration(seconds: 30)); // 502 处理 if (context.mounted) { @@ -674,19 +722,13 @@ class _SendCardState extends State { }); } } - } on DioException catch (e) { + } catch (e) { // 502 处理 - if (e.response?.statusCode == 502) { - showDialog(context: context, builder: (BuildContext context) => DesktopAlertModal(title: AppLocalizations.of(context)!.error, content: AppLocalizations.of(context)!.error)); - setState(() { - load = false; - // error = AppLocalizations.of(context)!.error; - }); - return; - } + _handleSendFailure(e, rpcURL); } isolate?.kill(priority: Isolate.immediate); + receivePort.close(); } }); } @@ -1003,4 +1045,4 @@ class _SendCardState extends State { ], )); } -} \ No newline at end of file +} diff --git a/lib/page/detail/send_page.dart b/lib/page/detail/send_page.dart index cb7abce..b66aefb 100644 --- a/lib/page/detail/send_page.dart +++ b/lib/page/detail/send_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:isolate'; import 'package:auto_size_text_field/auto_size_text_field.dart'; @@ -132,20 +133,33 @@ class _SendPageState extends State { sendPort.send(receivePort.sendPort); receivePort.listen((data) async { - String res = data[0] as String; - String toAddress = data[1] as String; - String amount = data[2] as String; - String fromAddress = data[3] as String; - String remark = data[4] as String; - String nonce = data[5] as String; - String fee = data[6] as String; + try { + String res = data[0] as String; + String toAddress = data[1] as String; + String amount = data[2] as String; + String fromAddress = data[3] as String; + String remark = data[4] as String; + String nonce = data[5] as String; + String fee = data[6] as String; - bool isPrivateKey = res.trim().split(' ').length == 1; - bip32.BIP32 wallet = Helper.createWallet(isPrivate: isPrivateKey, content: res); - - String result = TransactionHelper.getTransaction(fromAddress, toAddress, remark, double.parse(amount), wallet, nonce, double.parse(fee)); + bool isPrivateKey = res.trim().split(' ').length == 1; + bip32.BIP32 wallet = Helper.createWallet(isPrivate: isPrivateKey, content: res); + String result = TransactionHelper.getTransaction(fromAddress, toAddress, remark, double.parse(amount), wallet, nonce, double.parse(fee)); + sendPort.send(['success', result]); + } catch (e) { + sendPort.send(['error', e.toString()]); + } finally { + receivePort.close(); + } + }); + } - sendPort.send(['success', result]); + void _handleSendFailure(Object exception, String rpcURL) { + debugPrint('Transaction send failed via $rpcURL: $exception'); + if (!mounted) return; + setState(() { + isLoad = false; + error = exception.toString().replaceFirst('Exception: ', ''); }); } @@ -158,18 +172,35 @@ class _SendPageState extends State { ConfigModal config = Provider.of(context, listen: false); String rpcURL = config.getCurrentRpc(); String nonce = ''; - Response response = await dio.post( - rpcURL, - cancelToken: cancelToken, - data: { - "jsonrpc": "2.0", - "method": "xdag_getTransactionNonce", - "params": [fromAddress], - "id": 1 - }, - ).timeout(const Duration(seconds: 10)); - nonce = response.data['result'] as String; - isolate = await Isolate.spawn(isolateFunction, receivePort.sendPort); + try { + Response response = await dio + .post( + rpcURL, + cancelToken: cancelToken, + data: { + "jsonrpc": "2.0", + "method": "xdag_getTransactionNonce", + "params": [fromAddress], + "id": 1 + }, + ) + .timeout(const Duration(seconds: 15)); + var nonceResult = response.data?['result']; + if (nonceResult is! String || nonceResult.isEmpty) { + throw Exception(response.data?['error'] ?? 'Invalid nonce response'); + } + nonce = nonceResult; + isolate = await Isolate.spawn(isolateFunction, receivePort.sendPort); + } catch (e) { + receivePort.close(); + _handleSendFailure(e, rpcURL); + return; + } + final sendTimeout = Timer(const Duration(seconds: 45), () { + isolate?.kill(priority: Isolate.immediate); + receivePort.close(); + _handleSendFailure('Transaction signing timed out', rpcURL); + }); receivePort.listen((data) async { var sendAmount = amount; var sendRemark = remark; @@ -179,19 +210,28 @@ class _SendPageState extends State { var subSendPort = data; subSendPort.send([res, toAddress, amount, fromAddress, remark, nonce, fee]); } else if (data is List) { + sendTimeout.cancel(); + if (data.length < 2 || data[0] != 'success') { + _handleSendFailure(data.length > 1 ? data[1] : 'Transaction signing failed', rpcURL); + isolate?.kill(priority: Isolate.immediate); + receivePort.close(); + return; + } String result = data[1]; // print('result: $result'); try { - Response response = await dio.post( - rpcURL, - cancelToken: cancelToken, - data: { - "jsonrpc": "2.0", - "method": "xdag_sendRawTransaction", - "params": [result], - "id": 1 - }, - ); + Response response = await dio + .post( + rpcURL, + cancelToken: cancelToken, + data: { + "jsonrpc": "2.0", + "method": "xdag_sendRawTransaction", + "params": [result], + "id": 1 + }, + ) + .timeout(const Duration(seconds: 30)); print('response: ${response.data}'); if (context.mounted) { var res = response.data['result'] as String; @@ -254,18 +294,13 @@ class _SendPageState extends State { controller3.clear(); } } - } on DioException catch (e) { + } catch (e) { // 502 处理 - if (e.response?.statusCode == 502) { - setState(() { - isLoad = false; - error = AppLocalizations.of(context)!.error; - }); - return; - } + _handleSendFailure(e, rpcURL); } isolate?.kill(priority: Isolate.immediate); + receivePort.close(); } }); } diff --git a/lib/page/detail/transaction_page.dart b/lib/page/detail/transaction_page.dart index 1bc2300..bc461e6 100644 --- a/lib/page/detail/transaction_page.dart +++ b/lib/page/detail/transaction_page.dart @@ -116,6 +116,7 @@ class _TransactionPageState extends State { Transaction transaction = widget.transaction; String address = widget.address; bool isSend = transaction.from == address; + String displayFee = fee.isNotEmpty ? fee : Helper.removeTrailingZeros((transaction.fee + 0.1).toString()); // String otherAddress = isSend ? transaction.to : transaction.from; ContactsModal contacts = Provider.of(context); // 查询 otherAddress 是否在 contacts.contactsList 中 @@ -218,7 +219,7 @@ class _TransactionPageState extends State { Helper.showToast(context, AppLocalizations.of(context)!.copied_to_clipboard); }), const SizedBox(height: 1), - TransactionButton(showCopy: false, title: AppLocalizations.of(context)!.fee, value: '${transaction.fee + 0.1} XDAG', borderRadius: transaction.remark.isNotEmpty ? BorderRadius.zero : const BorderRadius.only(bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8))), + TransactionButton(showCopy: false, title: AppLocalizations.of(context)!.fee, value: '$displayFee XDAG', borderRadius: transaction.remark.isNotEmpty ? BorderRadius.zero : const BorderRadius.only(bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8))), const SizedBox(height: 1), if (transaction.remark.isNotEmpty) TransactionButton( diff --git a/lib/page/detail/wallet_list_page.dart b/lib/page/detail/wallet_list_page.dart index 62df4b8..af0a87c 100644 --- a/lib/page/detail/wallet_list_page.dart +++ b/lib/page/detail/wallet_list_page.dart @@ -347,7 +347,7 @@ class Item extends StatelessWidget { borderRadius: BorderRadius.circular(10), ), child: Padding( - padding: const EdgeInsets.all(15), + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12), child: Row( children: [ Expanded(