sp improvements (#2418)
* feat: show sp keys * fix: merge fixes * feat: show sp receive address on tx details page * feat(keys): show silent payment keys tab & keys tab * Update lib/src/screens/wallet_keys/wallet_keys_page.dart --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
rafael_xmr committed
Aug 6, 2025 at 15:02 UTC
778ac4783eecd0f7ce8f5368299a60de34017252
5 files changed
+117
-48
lib/bitcoin/cw_bitcoin.dart
+34
-7
@@ -73,6 +73,19 @@ class CWBitcoin extends Bitcoin {
73
return bitcoinWallet.keys.toJson();
74
}
75
76
+ @override
77
+ Map<String, String> getSilentPaymentKeys(Object wallet) {
78
+ final bitcoinWallet = wallet as ElectrumWallet;
79
+ final keysOwner = bitcoinWallet.walletAddresses.silentAddress!;
80
+
81
+ return <String, String>{
82
+ 'privateSpendKey': keysOwner.b_spend.toHex(),
83
+ 'publicSpendKey': keysOwner.B_spend.toHex(),
84
+ 'privateViewKey': keysOwner.b_scan.toHex(),
85
+ 'publicViewKey': keysOwner.B_scan.toHex(),
86
+ };
87
+ }
88
+
89
@override
90
List<TransactionPriority> getTransactionPriorities() => BitcoinTransactionPriority.all;
91
@@ -116,9 +129,7 @@ class CWBitcoin extends Bitcoin {
129
String? payjoinUri,
130
}) {
131
final bitcoinFeeRate =
119
- priority == BitcoinTransactionPriority.custom && feeRate != null
120
- ? feeRate
121
- : null;
132
+ priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null;
133
return BitcoinTransactionCredentials(
134
outputs
135
.map((out) => OutputInfo(
@@ -237,12 +248,12 @@ class CWBitcoin extends Bitcoin {
248
Box<UnspentCoinsInfo> unspentCoinSource,
249
Box<PayjoinSession> payjoinSessionSource,
250
bool isDirect) {
240
- return BitcoinWalletService(walletInfoSource, unspentCoinSource,
241
- payjoinSessionSource, isDirect);
251
+ return BitcoinWalletService(
252
+ walletInfoSource, unspentCoinSource, payjoinSessionSource, isDirect);
253
}
254
244
- WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource,
245
- Box<UnspentCoinsInfo> unspentCoinSource, bool isDirect) {
255
+ WalletService createLitecoinWalletService(
256
+ Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, bool isDirect) {
257
return LitecoinWalletService(walletInfoSource, unspentCoinSource, isDirect);
258
}
259
@@ -775,4 +786,20 @@ class CWBitcoin extends Bitcoin {
786
(_wallet.walletAddresses as BitcoinWalletAddresses).currentPayjoinReceiver = null;
787
(_wallet.walletAddresses as BitcoinWalletAddresses).payjoinEndpoint = null;
788
}
789
+
790
+ @override
791
+ String? getTransactionAddress(Object wallet, TransactionInfo tx) {
792
+ final bitcoinWallet = wallet as BitcoinWallet;
793
+ final bitcoinTx = tx as ElectrumTransactionInfo;
794
+
795
+ if (bitcoinTx.unspents == null || bitcoinTx.unspents!.isEmpty) {
796
+ return null;
797
+ }
798
+
799
+ // final bitcoinSPAddrs =
800
+ // bitcoinTx.unspents!.first.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
801
+ final bitcoinSPAddrs = wallet.walletAddresses as BitcoinWalletAddresses;
802
+ final bitcoinSPAddr = bitcoinSPAddrs.silentAddresses.first;
803
+ return bitcoinSPAddr.address;
804
+ }
805
}
lib/src/screens/wallet_keys/wallet_keys_page.dart
+32
-11
@@ -2,6 +2,7 @@ import 'package:cake_wallet/entities/qr_view_data.dart';
2
import 'package:cake_wallet/generated/i18n.dart';
3
import 'package:cake_wallet/routes.dart';
4
import 'package:cake_wallet/src/screens/base_page.dart';
5
+import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
6
import 'package:cake_wallet/src/widgets/primary_button.dart';
7
import 'package:cake_wallet/src/widgets/seedphrase_grid_widget.dart';
8
import 'package:cake_wallet/src/widgets/text_info_box.dart';
@@ -67,6 +68,7 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
68
with SingleTickerProviderStateMixin {
69
late TabController _tabController;
70
late bool showKeyTab;
71
+ late bool showSilentPaymentsTab;
72
late bool showLegacySeedTab;
73
late bool isLegacySeedOnly;
74
@@ -79,10 +81,15 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
81
super.initState();
82
83
showKeyTab = widget.walletKeysViewModel.items.isNotEmpty;
84
+ showSilentPaymentsTab =
85
+ widget.walletKeysViewModel.isBitcoin && widget.walletKeysViewModel.items.length > 4;
86
showLegacySeedTab = widget.walletKeysViewModel.legacySeedSplit.isNotEmpty;
87
isLegacySeedOnly = widget.walletKeysViewModel.isLegacySeedOnly;
88
85
- final totalTabs = (_hasSeeds ? 1 : 0) + (showKeyTab ? 1 : 0) + (showLegacySeedTab ? 1 : 0);
89
+ final totalTabs = (_hasSeeds ? 1 : 0) +
90
+ (showKeyTab ? 1 : 0) +
91
+ (showLegacySeedTab ? 1 : 0) +
92
+ (showSilentPaymentsTab ? 1 : 0);
93
94
_tabController = TabController(length: totalTabs, vsync: this);
95
}
@@ -108,13 +115,11 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
115
isScrollable: true,
116
labelStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
117
fontSize: 18,
111
-
118
fontWeight: FontWeight.w600,
119
color: Theme.of(context).colorScheme.onSurfaceVariant,
120
),
121
unselectedLabelStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
122
fontSize: 18,
117
-
123
fontWeight: FontWeight.w600,
124
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5),
125
),
@@ -126,9 +131,20 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
131
dividerColor: Colors.transparent,
132
padding: EdgeInsets.zero,
133
tabs: [
129
- if (_hasSeeds) Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')),
130
- if (showKeyTab) Tab(text: S.of(context).keys, key: ValueKey('wallet_keys_page_keys'),),
131
- if (showLegacySeedTab) Tab(text: S.of(context).legacy, key: ValueKey('wallet_keys_page_seed_legacy')),
134
+ if (_hasSeeds)
135
+ Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')),
136
+ if (showKeyTab)
137
+ Tab(
138
+ text: S.of(context).keys,
139
+ key: ValueKey('wallet_keys_page_keys'),
140
+ ),
141
+ if (showSilentPaymentsTab)
142
+ Tab(
143
+ text: S.of(context).silent_payments,
144
+ key: ValueKey('wallet_keys_silent_payments_keys'),
145
+ ),
146
+ if (showLegacySeedTab)
147
+ Tab(text: S.of(context).legacy, key: ValueKey('wallet_keys_page_seed_legacy')),
148
],
149
),
150
),
@@ -145,7 +161,12 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
161
if (showKeyTab)
162
Padding(
163
padding: const EdgeInsets.only(left: 22, right: 22),
148
- child: _buildKeysTab(context),
164
+ child: _buildKeysTab(context, showSilentPaymentsTab ? widget.walletKeysViewModel.items.sublist(0, 4) : widget.walletKeysViewModel.items),
165
+ ),
166
+ if (showSilentPaymentsTab)
167
+ Padding(
168
+ padding: const EdgeInsets.only(left: 22, right: 22),
169
+ child: _buildKeysTab(context, widget.walletKeysViewModel.items.sublist(4)),
170
),
171
if (showLegacySeedTab)
172
Padding(
@@ -163,7 +184,7 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
184
Widget _buildSeedTab(BuildContext context, bool isLegacySeed) {
185
return Column(
186
children: [
166
- if (isLegacySeedOnly || isLegacySeed) ...[
187
+ if (isLegacySeedOnly || isLegacySeed || widget.walletKeysViewModel.isBitcoin) ...[
188
_buildHeightBox(),
189
const SizedBox(height: 20),
190
],
@@ -189,15 +210,15 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
210
);
211
}
212
192
- Widget _buildKeysTab(BuildContext context) {
213
+ Widget _buildKeysTab(BuildContext context, List<StandartListItem> items) {
214
return Column(
215
children: [
216
Expanded(
217
child: ListView.separated(
218
shrinkWrap: true,
198
- itemCount: widget.walletKeysViewModel.items.length,
219
+ itemCount: items.length,
220
itemBuilder: (context, index) {
200
- final item = widget.walletKeysViewModel.items[index];
221
+ final item = items[index];
222
return TextInfoBox(
223
key: item.key,
224
title: item.title,
lib/view_model/transaction_details_view_model.dart
+22
-4
@@ -90,7 +90,7 @@ abstract class TransactionDetailsViewModelBase with Store {
90
break;
91
case WalletType.none:
92
case WalletType.banano:
93
- break;
93
+ break;
94
}
95
96
final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}';
@@ -235,9 +235,8 @@ abstract class TransactionDetailsViewModelBase with Store {
235
void _addMoneroListItems(TransactionInfo tx, DateFormat dateFormat) {
236
final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}';
237
final description = transactionDescriptionBox.values.firstWhere(
238
- (val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
239
- orElse: () => TransactionDescription(id: descriptionKey));
240
-
238
+ (val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
239
+ orElse: () => TransactionDescription(id: descriptionKey));
240
241
final key = tx.additionalInfo['key'] as String? ?? description.transactionKey;
242
final accountIndex = tx.additionalInfo['accountIndex'] as int;
@@ -344,6 +343,25 @@ abstract class TransactionDetailsViewModelBase with Store {
343
),
344
];
345
346
+ if (wallet.type == WalletType.bitcoin && tx.direction == TransactionDirection.incoming) {
347
+ try {
348
+ final address = bitcoin!.getTransactionAddress(wallet, tx);
349
+
350
+ if (address != null) {
351
+ isRecipientAddressShown = true;
352
+ _items.add(
353
+ StandartListItem(
354
+ title: S.current.transaction_details_recipient_address,
355
+ value: address,
356
+ key: ValueKey('standard_list_item_transaction_details_recipient_address_key'),
357
+ ),
358
+ );
359
+ }
360
+ } catch (e) {
361
+ printV(e.toString());
362
+ }
363
+ }
364
+
365
items.addAll(_items);
366
}
367
lib/view_model/wallet_keys_view_model.dart
+19
-14
@@ -49,6 +49,8 @@ abstract class WalletKeysViewModelBase with Store {
49
}
50
}
51
52
+ bool get isBitcoin => _wallet.type == WalletType.bitcoin;
53
+
54
final ObservableList<StandartListItem> items;
55
56
final String title;
@@ -66,8 +68,7 @@ abstract class WalletKeysViewModelBase with Store {
68
bool get isLegacySeedOnly =>
69
[WalletType.monero, WalletType.wownero].contains(_wallet.type) &&
70
_wallet.seed != null &&
69
- !(Polyseed.isValidSeed(_wallet.seed!) ||
70
- _wallet.seed!.split(' ').length == 12);
71
+ !(Polyseed.isValidSeed(_wallet.seed!) || _wallet.seed!.split(' ').length == 12);
72
73
String get legacySeed {
74
if ((_wallet.type == WalletType.monero || _wallet.type == WalletType.wownero) &&
@@ -163,18 +164,22 @@ abstract class WalletKeysViewModelBase with Store {
164
case WalletType.bitcoin:
165
case WalletType.litecoin:
166
case WalletType.bitcoinCash:
166
- final keys = bitcoin!.getWalletKeys(_appStore.wallet!);
167
-
168
- items.addAll([
169
- if ((keys['wif']??'').isNotEmpty)
170
- StandartListItem(title: "WIF", value: keys['wif']!),
171
- if ((keys['privateKey']??'').isNotEmpty)
172
- StandartListItem(title: S.current.private_key, value: keys['privateKey']!),
173
- if (keys['publicKey'] != null)
174
- StandartListItem(title: S.current.public_key, value: keys['publicKey']!),
175
- if (keys['xpub'] != null)
176
- StandartListItem(title: "xPub", value: keys['xpub']!),
177
- ]);
167
+ if (_wallet.type == WalletType.bitcoin) {
168
+ keys = bitcoin!.getSilentPaymentKeys(_appStore.wallet!);
169
+ }
170
+
171
+ final electrumKeys = bitcoin!.getWalletKeys(_appStore.wallet!);
172
+
173
+ items.addAll([
174
+ if ((electrumKeys['wif'] ?? '').isNotEmpty)
175
+ StandartListItem(title: "WIF", value: electrumKeys['wif']!),
176
+ if ((electrumKeys['privateKey'] ?? '').isNotEmpty)
177
+ StandartListItem(title: S.current.private_key, value: electrumKeys['privateKey']!),
178
+ if (electrumKeys['publicKey'] != null)
179
+ StandartListItem(title: S.current.public_key, value: electrumKeys['publicKey']!),
180
+ if (electrumKeys['xpub'] != null)
181
+ StandartListItem(title: "xPub", value: electrumKeys['xpub']!),
182
+ ]);
183
break;
184
case WalletType.none:
185
case WalletType.haven:
tool/configure.dart
+10
-12
@@ -253,6 +253,8 @@ abstract class Bitcoin {
253
String getPayjoinEndpoint(Object wallet);
254
void resumePayjoinSessions(Object wallet);
255
void stopPayjoinSessions(Object wallet);
256
+ Map<String, String> getSilentPaymentKeys(Object wallet);
257
+ String? getTransactionAddress(Object wallet, TransactionInfo tx);
258
}
259
""";
260
@@ -939,15 +941,12 @@ abstract class BitcoinCash {
941
""";
942
943
const bitcoinCashEmptyDefinition = 'BitcoinCash? bitcoinCash;\n';
942
- const bitcoinCashCWDefinition =
943
- 'BitcoinCash? bitcoinCash = CWBitcoinCash();\n';
944
+ const bitcoinCashCWDefinition = 'BitcoinCash? bitcoinCash = CWBitcoinCash();\n';
945
946
final output = '$bitcoinCashCommonHeaders\n' +
947
(hasImplementation ? '$bitcoinCashCWHeaders\n' : '\n') +
948
(hasImplementation ? '$bitcoinCashCwPart\n\n' : '\n') +
948
- (hasImplementation
949
- ? bitcoinCashCWDefinition
950
- : bitcoinCashEmptyDefinition) +
949
+ (hasImplementation ? bitcoinCashCWDefinition : bitcoinCashEmptyDefinition) +
950
'\n' +
951
bitcoinCashContent;
952
@@ -1082,8 +1081,7 @@ abstract class NanoUtil {
1081
""";
1082
1083
const nanoEmptyDefinition = 'Nano? nano;\nNanoUtil? nanoUtil;\n';
1085
- const nanoCWDefinition =
1086
- 'Nano? nano = CWNano();\nNanoUtil? nanoUtil = CWNanoUtil();\n';
1084
+ const nanoCWDefinition = 'Nano? nano = CWNano();\nNanoUtil? nanoUtil = CWNanoUtil();\n';
1085
1086
final output = '$nanoCommonHeaders\n' +
1087
(hasImplementation ? '$nanoCWHeaders\n' : '\n') +
@@ -1324,11 +1322,11 @@ abstract class Zano {
1322
const zanoCWDefinition = 'Zano? zano = CWZano();\n';
1323
1324
final output = '$zanoCommonHeaders\n' +
1327
- (hasImplementation ? '$zanoCWHeaders\n' : '\n') +
1328
- (hasImplementation ? '$zanoCwPart\n\n' : '\n') +
1329
- (hasImplementation ? zanoCWDefinition : zanoEmptyDefinition) +
1330
- '\n' +
1331
- zanoContent;
1325
+ (hasImplementation ? '$zanoCWHeaders\n' : '\n') +
1326
+ (hasImplementation ? '$zanoCwPart\n\n' : '\n') +
1327
+ (hasImplementation ? zanoCWDefinition : zanoEmptyDefinition) +
1328
+ '\n' +
1329
+ zanoContent;
1330
1331
if (outputFile.existsSync()) {
1332
await outputFile.delete();