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();