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>
This commit is contained in:
rafael_xmr
2025-08-06 21:02:08 +03:00
committed by GitHub
co-authored by Omar Hatem
parent 4dc5be7640
commit 778ac4783e
5 changed files with 117 additions and 48 deletions
+34 -7
View File
@@ -73,6 +73,19 @@ class CWBitcoin extends Bitcoin {
return bitcoinWallet.keys.toJson();
}
@override
Map<String, String> getSilentPaymentKeys(Object wallet) {
final bitcoinWallet = wallet as ElectrumWallet;
final keysOwner = bitcoinWallet.walletAddresses.silentAddress!;
return <String, String>{
'privateSpendKey': keysOwner.b_spend.toHex(),
'publicSpendKey': keysOwner.B_spend.toHex(),
'privateViewKey': keysOwner.b_scan.toHex(),
'publicViewKey': keysOwner.B_scan.toHex(),
};
}
@override
List<TransactionPriority> getTransactionPriorities() => BitcoinTransactionPriority.all;
@@ -116,9 +129,7 @@ class CWBitcoin extends Bitcoin {
String? payjoinUri,
}) {
final bitcoinFeeRate =
priority == BitcoinTransactionPriority.custom && feeRate != null
? feeRate
: null;
priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null;
return BitcoinTransactionCredentials(
outputs
.map((out) => OutputInfo(
@@ -237,12 +248,12 @@ class CWBitcoin extends Bitcoin {
Box<UnspentCoinsInfo> unspentCoinSource,
Box<PayjoinSession> payjoinSessionSource,
bool isDirect) {
return BitcoinWalletService(walletInfoSource, unspentCoinSource,
payjoinSessionSource, isDirect);
return BitcoinWalletService(
walletInfoSource, unspentCoinSource, payjoinSessionSource, isDirect);
}
WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource,
Box<UnspentCoinsInfo> unspentCoinSource, bool isDirect) {
WalletService createLitecoinWalletService(
Box<WalletInfo> walletInfoSource, Box<UnspentCoinsInfo> unspentCoinSource, bool isDirect) {
return LitecoinWalletService(walletInfoSource, unspentCoinSource, isDirect);
}
@@ -775,4 +786,20 @@ class CWBitcoin extends Bitcoin {
(_wallet.walletAddresses as BitcoinWalletAddresses).currentPayjoinReceiver = null;
(_wallet.walletAddresses as BitcoinWalletAddresses).payjoinEndpoint = null;
}
@override
String? getTransactionAddress(Object wallet, TransactionInfo tx) {
final bitcoinWallet = wallet as BitcoinWallet;
final bitcoinTx = tx as ElectrumTransactionInfo;
if (bitcoinTx.unspents == null || bitcoinTx.unspents!.isEmpty) {
return null;
}
// final bitcoinSPAddrs =
// bitcoinTx.unspents!.first.bitcoinAddressRecord as BitcoinSilentPaymentAddressRecord;
final bitcoinSPAddrs = wallet.walletAddresses as BitcoinWalletAddresses;
final bitcoinSPAddr = bitcoinSPAddrs.silentAddresses.first;
return bitcoinSPAddr.address;
}
}
@@ -2,6 +2,7 @@ import 'package:cake_wallet/entities/qr_view_data.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/seedphrase_grid_widget.dart';
import 'package:cake_wallet/src/widgets/text_info_box.dart';
@@ -67,6 +68,7 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
with SingleTickerProviderStateMixin {
late TabController _tabController;
late bool showKeyTab;
late bool showSilentPaymentsTab;
late bool showLegacySeedTab;
late bool isLegacySeedOnly;
@@ -79,10 +81,15 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
super.initState();
showKeyTab = widget.walletKeysViewModel.items.isNotEmpty;
showSilentPaymentsTab =
widget.walletKeysViewModel.isBitcoin && widget.walletKeysViewModel.items.length > 4;
showLegacySeedTab = widget.walletKeysViewModel.legacySeedSplit.isNotEmpty;
isLegacySeedOnly = widget.walletKeysViewModel.isLegacySeedOnly;
final totalTabs = (_hasSeeds ? 1 : 0) + (showKeyTab ? 1 : 0) + (showLegacySeedTab ? 1 : 0);
final totalTabs = (_hasSeeds ? 1 : 0) +
(showKeyTab ? 1 : 0) +
(showLegacySeedTab ? 1 : 0) +
(showSilentPaymentsTab ? 1 : 0);
_tabController = TabController(length: totalTabs, vsync: this);
}
@@ -108,13 +115,11 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
isScrollable: true,
labelStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
unselectedLabelStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.5),
),
@@ -126,9 +131,20 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
dividerColor: Colors.transparent,
padding: EdgeInsets.zero,
tabs: [
if (_hasSeeds) Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')),
if (showKeyTab) Tab(text: S.of(context).keys, key: ValueKey('wallet_keys_page_keys'),),
if (showLegacySeedTab) Tab(text: S.of(context).legacy, key: ValueKey('wallet_keys_page_seed_legacy')),
if (_hasSeeds)
Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')),
if (showKeyTab)
Tab(
text: S.of(context).keys,
key: ValueKey('wallet_keys_page_keys'),
),
if (showSilentPaymentsTab)
Tab(
text: S.of(context).silent_payments,
key: ValueKey('wallet_keys_silent_payments_keys'),
),
if (showLegacySeedTab)
Tab(text: S.of(context).legacy, key: ValueKey('wallet_keys_page_seed_legacy')),
],
),
),
@@ -145,7 +161,12 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
if (showKeyTab)
Padding(
padding: const EdgeInsets.only(left: 22, right: 22),
child: _buildKeysTab(context),
child: _buildKeysTab(context, showSilentPaymentsTab ? widget.walletKeysViewModel.items.sublist(0, 4) : widget.walletKeysViewModel.items),
),
if (showSilentPaymentsTab)
Padding(
padding: const EdgeInsets.only(left: 22, right: 22),
child: _buildKeysTab(context, widget.walletKeysViewModel.items.sublist(4)),
),
if (showLegacySeedTab)
Padding(
@@ -163,7 +184,7 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
Widget _buildSeedTab(BuildContext context, bool isLegacySeed) {
return Column(
children: [
if (isLegacySeedOnly || isLegacySeed) ...[
if (isLegacySeedOnly || isLegacySeed || widget.walletKeysViewModel.isBitcoin) ...[
_buildHeightBox(),
const SizedBox(height: 20),
],
@@ -189,15 +210,15 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
);
}
Widget _buildKeysTab(BuildContext context) {
Widget _buildKeysTab(BuildContext context, List<StandartListItem> items) {
return Column(
children: [
Expanded(
child: ListView.separated(
shrinkWrap: true,
itemCount: widget.walletKeysViewModel.items.length,
itemCount: items.length,
itemBuilder: (context, index) {
final item = widget.walletKeysViewModel.items[index];
final item = items[index];
return TextInfoBox(
key: item.key,
title: item.title,
@@ -90,7 +90,7 @@ abstract class TransactionDetailsViewModelBase with Store {
break;
case WalletType.none:
case WalletType.banano:
break;
break;
}
final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}';
@@ -235,9 +235,8 @@ abstract class TransactionDetailsViewModelBase with Store {
void _addMoneroListItems(TransactionInfo tx, DateFormat dateFormat) {
final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}';
final description = transactionDescriptionBox.values.firstWhere(
(val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey));
(val) => val.id == descriptionKey || val.id == transactionInfo.txHash,
orElse: () => TransactionDescription(id: descriptionKey));
final key = tx.additionalInfo['key'] as String? ?? description.transactionKey;
final accountIndex = tx.additionalInfo['accountIndex'] as int;
@@ -344,6 +343,25 @@ abstract class TransactionDetailsViewModelBase with Store {
),
];
if (wallet.type == WalletType.bitcoin && tx.direction == TransactionDirection.incoming) {
try {
final address = bitcoin!.getTransactionAddress(wallet, tx);
if (address != null) {
isRecipientAddressShown = true;
_items.add(
StandartListItem(
title: S.current.transaction_details_recipient_address,
value: address,
key: ValueKey('standard_list_item_transaction_details_recipient_address_key'),
),
);
}
} catch (e) {
printV(e.toString());
}
}
items.addAll(_items);
}
+19 -14
View File
@@ -49,6 +49,8 @@ abstract class WalletKeysViewModelBase with Store {
}
}
bool get isBitcoin => _wallet.type == WalletType.bitcoin;
final ObservableList<StandartListItem> items;
final String title;
@@ -66,8 +68,7 @@ abstract class WalletKeysViewModelBase with Store {
bool get isLegacySeedOnly =>
[WalletType.monero, WalletType.wownero].contains(_wallet.type) &&
_wallet.seed != null &&
!(Polyseed.isValidSeed(_wallet.seed!) ||
_wallet.seed!.split(' ').length == 12);
!(Polyseed.isValidSeed(_wallet.seed!) || _wallet.seed!.split(' ').length == 12);
String get legacySeed {
if ((_wallet.type == WalletType.monero || _wallet.type == WalletType.wownero) &&
@@ -163,18 +164,22 @@ abstract class WalletKeysViewModelBase with Store {
case WalletType.bitcoin:
case WalletType.litecoin:
case WalletType.bitcoinCash:
final keys = bitcoin!.getWalletKeys(_appStore.wallet!);
items.addAll([
if ((keys['wif']??'').isNotEmpty)
StandartListItem(title: "WIF", value: keys['wif']!),
if ((keys['privateKey']??'').isNotEmpty)
StandartListItem(title: S.current.private_key, value: keys['privateKey']!),
if (keys['publicKey'] != null)
StandartListItem(title: S.current.public_key, value: keys['publicKey']!),
if (keys['xpub'] != null)
StandartListItem(title: "xPub", value: keys['xpub']!),
]);
if (_wallet.type == WalletType.bitcoin) {
keys = bitcoin!.getSilentPaymentKeys(_appStore.wallet!);
}
final electrumKeys = bitcoin!.getWalletKeys(_appStore.wallet!);
items.addAll([
if ((electrumKeys['wif'] ?? '').isNotEmpty)
StandartListItem(title: "WIF", value: electrumKeys['wif']!),
if ((electrumKeys['privateKey'] ?? '').isNotEmpty)
StandartListItem(title: S.current.private_key, value: electrumKeys['privateKey']!),
if (electrumKeys['publicKey'] != null)
StandartListItem(title: S.current.public_key, value: electrumKeys['publicKey']!),
if (electrumKeys['xpub'] != null)
StandartListItem(title: "xPub", value: electrumKeys['xpub']!),
]);
break;
case WalletType.none:
case WalletType.haven:
+10 -12
View File
@@ -253,6 +253,8 @@ abstract class Bitcoin {
String getPayjoinEndpoint(Object wallet);
void resumePayjoinSessions(Object wallet);
void stopPayjoinSessions(Object wallet);
Map<String, String> getSilentPaymentKeys(Object wallet);
String? getTransactionAddress(Object wallet, TransactionInfo tx);
}
""";
@@ -939,15 +941,12 @@ abstract class BitcoinCash {
""";
const bitcoinCashEmptyDefinition = 'BitcoinCash? bitcoinCash;\n';
const bitcoinCashCWDefinition =
'BitcoinCash? bitcoinCash = CWBitcoinCash();\n';
const bitcoinCashCWDefinition = 'BitcoinCash? bitcoinCash = CWBitcoinCash();\n';
final output = '$bitcoinCashCommonHeaders\n' +
(hasImplementation ? '$bitcoinCashCWHeaders\n' : '\n') +
(hasImplementation ? '$bitcoinCashCwPart\n\n' : '\n') +
(hasImplementation
? bitcoinCashCWDefinition
: bitcoinCashEmptyDefinition) +
(hasImplementation ? bitcoinCashCWDefinition : bitcoinCashEmptyDefinition) +
'\n' +
bitcoinCashContent;
@@ -1082,8 +1081,7 @@ abstract class NanoUtil {
""";
const nanoEmptyDefinition = 'Nano? nano;\nNanoUtil? nanoUtil;\n';
const nanoCWDefinition =
'Nano? nano = CWNano();\nNanoUtil? nanoUtil = CWNanoUtil();\n';
const nanoCWDefinition = 'Nano? nano = CWNano();\nNanoUtil? nanoUtil = CWNanoUtil();\n';
final output = '$nanoCommonHeaders\n' +
(hasImplementation ? '$nanoCWHeaders\n' : '\n') +
@@ -1324,11 +1322,11 @@ abstract class Zano {
const zanoCWDefinition = 'Zano? zano = CWZano();\n';
final output = '$zanoCommonHeaders\n' +
(hasImplementation ? '$zanoCWHeaders\n' : '\n') +
(hasImplementation ? '$zanoCwPart\n\n' : '\n') +
(hasImplementation ? zanoCWDefinition : zanoEmptyDefinition) +
'\n' +
zanoContent;
(hasImplementation ? '$zanoCWHeaders\n' : '\n') +
(hasImplementation ? '$zanoCwPart\n\n' : '\n') +
(hasImplementation ? zanoCWDefinition : zanoEmptyDefinition) +
'\n' +
zanoContent;
if (outputFile.existsSync()) {
await outputFile.delete();