feat: bitcoin view-only

This commit is contained in:
Czarek Nakamoto 2025-05-13 10:10:32 +02:00 committed by cyan
parent 1a5601f755
commit a5b6ae6475
12 changed files with 145 additions and 62 deletions

View file

@ -54,6 +54,17 @@ class BitcoinRestoreWalletFromWIFCredentials extends WalletCredentials {
final String wif; final String wif;
} }
class BitcoinWalletFromKeysCredentials extends WalletCredentials {
BitcoinWalletFromKeysCredentials({
required String name,
required String password,
required this.xpub,
WalletInfo? walletInfo,
}) : super(name: name, password: password, walletInfo: walletInfo);
final String xpub;
}
class BitcoinRestoreWalletFromHardware extends WalletCredentials { class BitcoinRestoreWalletFromHardware extends WalletCredentials {
BitcoinRestoreWalletFromHardware({ BitcoinRestoreWalletFromHardware({
required String name, required String name,

View file

@ -1,7 +1,15 @@
class BitcoinWalletKeys { class BitcoinWalletKeys {
const BitcoinWalletKeys({required this.wif, required this.privateKey, required this.publicKey}); const BitcoinWalletKeys({required this.wif, required this.privateKey, required this.publicKey, required this.xpub});
final String wif; final String wif;
final String privateKey; final String privateKey;
final String publicKey; final String publicKey;
final String xpub;
Map<String, String> toJson() => {
'wif': wif,
'privateKey': privateKey,
'publicKey': publicKey,
'xpub': xpub
};
} }

View file

@ -20,7 +20,7 @@ import 'package:bip39/bip39.dart' as bip39;
class BitcoinWalletService extends WalletService< class BitcoinWalletService extends WalletService<
BitcoinNewWalletCredentials, BitcoinNewWalletCredentials,
BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromSeedCredentials,
BitcoinRestoreWalletFromWIFCredentials, BitcoinWalletFromKeysCredentials,
BitcoinRestoreWalletFromHardware> { BitcoinRestoreWalletFromHardware> {
BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource, BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource,
this.payjoinSessionSource, this.alwaysScan, this.isDirect); this.payjoinSessionSource, this.alwaysScan, this.isDirect);
@ -169,9 +169,24 @@ class BitcoinWalletService extends WalletService<
} }
@override @override
Future<BitcoinWallet> restoreFromKeys(BitcoinRestoreWalletFromWIFCredentials credentials, Future<BitcoinWallet> restoreFromKeys(BitcoinWalletFromKeysCredentials credentials,
{bool? isTestnet}) async => {bool? isTestnet}) async {
throw UnimplementedError(); final network = isTestnet == true ? BitcoinNetwork.testnet : BitcoinNetwork.mainnet;
credentials.walletInfo?.network = network.value;
final wallet = await BitcoinWallet(
password: credentials.password!,
xpub: credentials.xpub,
walletInfo: credentials.walletInfo!,
unspentCoinsInfo: unspentCoinsInfoSource,
networkParam: network,
encryptionFileUtils: encryptionFileUtilsFor(isDirect),
);
await wallet.save();
await wallet.init();
return wallet;
}
@override @override
Future<BitcoinWallet> restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials, Future<BitcoinWallet> restoreFromSeed(BitcoinRestoreWalletFromSeedCredentials credentials,

View file

@ -280,11 +280,26 @@ abstract class ElectrumWalletBase
} }
@override @override
BitcoinWalletKeys get keys => BitcoinWalletKeys( BitcoinWalletKeys get keys {
wif: WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer), String? wif;
privateKey: hd.privateKey.toHex(), String? privateKey;
publicKey: hd.publicKey.toHex(), String? publicKey;
); try {
wif = WifEncoder.encode(hd.privateKey.raw, netVer: network.wifNetVer);
} catch (_) {}
try {
privateKey = hd.privateKey.toHex();
} catch (_) {}
try {
publicKey = hd.publicKey.toHex();
} catch (_) {}
return BitcoinWalletKeys(
wif: wif ?? '',
privateKey: privateKey ?? '',
publicKey: publicKey ?? '',
xpub: xpub,
);
}
String _password; String _password;
List<BitcoinUnspent> unspentCoins; List<BitcoinUnspent> unspentCoins;

View file

@ -165,6 +165,6 @@ class PendingBitcoinTransaction with PendingTransaction {
@override @override
Future<String?> commitUR() { Future<String?> commitUR() {
throw UnimplementedError(); return Future.value("test");
} }
} }

View file

@ -18,6 +18,14 @@ class CWBitcoin extends Bitcoin {
passphrase: passphrase, passphrase: passphrase,
); );
@override
WalletCredentials createBitcoinWalletFromKeys({
required String name,
required String password,
required String xpub,
}) =>
BitcoinWalletFromKeysCredentials(name: name, password: password, xpub: xpub);
@override @override
WalletCredentials createBitcoinRestoreWalletFromWIFCredentials( WalletCredentials createBitcoinRestoreWalletFromWIFCredentials(
{required String name, {required String name,
@ -62,11 +70,7 @@ class CWBitcoin extends Bitcoin {
final bitcoinWallet = wallet as ElectrumWallet; final bitcoinWallet = wallet as ElectrumWallet;
final keys = bitcoinWallet.keys; final keys = bitcoinWallet.keys;
return <String, String>{ return bitcoinWallet.keys.toJson();
'wif': keys.wif,
'privateKey': keys.privateKey,
'publicKey': keys.publicKey
};
} }
@override @override

View file

@ -198,14 +198,14 @@ class WalletRestoreFromKeysFormState extends State<WalletRestoreFromKeysForm> {
Widget _restoreFromKeysFormFields() { Widget _restoreFromKeysFormFields() {
// Decred can only restore a view only wallet with an account pubkey. Other // Decred can only restore a view only wallet with an account pubkey. Other
// fields are not used. // fields are not used.
if (widget.walletRestoreViewModel.type == WalletType.decred) { if (widget.walletRestoreViewModel.onlyViewKeyRestore) {
return Column( return Column(
children: [ children: [
BaseTextFormField( BaseTextFormField(
controller: viewKeyController, controller: viewKeyController,
hintText: S.of(context).view_key_public, hintText: S.of(context).view_key_public,
maxLines: null, maxLines: null,
) ),
], ],
); );
} }
@ -253,13 +253,14 @@ class WalletRestoreFromKeysFormState extends State<WalletRestoreFromKeysForm> {
maxLines: null, maxLines: null,
), ),
), ),
BlockchainHeightWidget( if (widget.walletRestoreViewModel.hasBlockchainHeightSelector)
key: blockchainHeightKey, BlockchainHeightWidget(
hasDatePicker: widget.walletRestoreViewModel.type != WalletType.haven, key: blockchainHeightKey,
onHeightChange: (_) => null, hasDatePicker: widget.walletRestoreViewModel.type != WalletType.haven,
onHeightOrDateEntered: widget.onHeightOrDateEntered, onHeightChange: (_) => null,
walletType: widget.walletRestoreViewModel.type, onHeightOrDateEntered: widget.onHeightOrDateEntered,
), walletType: widget.walletRestoreViewModel.type,
),
], ],
); );
} }

View file

@ -199,7 +199,7 @@ class WalletRestorePage extends BasePage {
credentials['seed'] = credentials['seed'] =
walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text; walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text;
if (walletRestoreViewModel.hasBlockchainHeightLanguageSelector) { if (walletRestoreViewModel.hasBlockchainHeightSelector) {
credentials['height'] = credentials['height'] =
walletRestoreFromSeedFormKey.currentState!.blockchainHeightKey.currentState?.height ?? walletRestoreFromSeedFormKey.currentState!.blockchainHeightKey.currentState?.height ??
-1; -1;
@ -219,7 +219,7 @@ class WalletRestorePage extends BasePage {
credentials['name'] = credentials['name'] =
walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text; walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text;
credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text; credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text;
if (walletRestoreViewModel.type != WalletType.decred) { if (!walletRestoreViewModel.onlyViewKeyRestore) {
credentials['address'] = credentials['address'] =
walletRestoreFromKeysFormKey.currentState!.addressController.text; walletRestoreFromKeysFormKey.currentState!.addressController.text;
credentials['spendKey'] = credentials['spendKey'] =
@ -519,7 +519,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
} }
}, },
onViewKeyEntered: (bool entered) { onViewKeyEntered: (bool entered) {
if (walletRestoreViewModel.type == WalletType.decred) { if (widget.walletRestoreViewModel.onlyViewKeyRestore) {
walletRestoreViewModel.isButtonEnabled = entered; walletRestoreViewModel.isButtonEnabled = entered;
} }
}, },
@ -536,7 +536,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
key: widget.walletRestoreFromSeedFormKey, key: widget.walletRestoreFromSeedFormKey,
restoredWallet: walletRestoreViewModel.restoredWallet, restoredWallet: walletRestoreViewModel.restoredWallet,
seedSettingsViewModel: widget.seedSettingsViewModel, seedSettingsViewModel: widget.seedSettingsViewModel,
displayBlockHeightSelector: widget.walletRestoreViewModel.hasBlockchainHeightLanguageSelector, displayBlockHeightSelector: widget.walletRestoreViewModel.hasBlockchainHeightSelector,
displayLanguageSelector: widget.walletRestoreViewModel.hasSeedLanguageSelector, displayLanguageSelector: widget.walletRestoreViewModel.hasSeedLanguageSelector,
type: widget.walletRestoreViewModel.type, type: widget.walletRestoreViewModel.type,
blockHeightFocusNode: widget.blockHeightFocusNode, blockHeightFocusNode: widget.blockHeightFocusNode,
@ -563,7 +563,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
} }
void _validateOnChange({bool isPolyseed = false}) { void _validateOnChange({bool isPolyseed = false}) {
if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightLanguageSelector) { if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightSelector) {
final hasHeight = walletRestoreFromSeedFormKey final hasHeight = walletRestoreFromSeedFormKey
.currentState?.blockchainHeightKey.currentState?.restoreHeightController.text.isNotEmpty; .currentState?.blockchainHeightKey.currentState?.restoreHeightController.text.isNotEmpty;

View file

@ -82,7 +82,7 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
showLegacySeedTab = widget.walletKeysViewModel.legacySeedSplit.isNotEmpty; showLegacySeedTab = widget.walletKeysViewModel.legacySeedSplit.isNotEmpty;
isLegacySeedOnly = widget.walletKeysViewModel.isLegacySeedOnly; isLegacySeedOnly = widget.walletKeysViewModel.isLegacySeedOnly;
final totalTabs = 1 + (showKeyTab ? 1 : 0) + (showLegacySeedTab ? 1 : 0); final totalTabs = (_hasSeeds ? 1 : 0) + (showKeyTab ? 1 : 0) + (showLegacySeedTab ? 1 : 0);
_tabController = TabController(length: totalTabs, vsync: this); _tabController = TabController(length: totalTabs, vsync: this);
} }
@ -126,7 +126,7 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
dividerColor: Colors.transparent, dividerColor: Colors.transparent,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
tabs: [ tabs: [
Tab(text: S.of(context).widgets_seed, key: ValueKey('wallet_keys_page_seed')), 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 (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 (showLegacySeedTab) Tab(text: S.of(context).legacy, key: ValueKey('wallet_keys_page_seed_legacy')),
], ],
@ -137,10 +137,11 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
child: TabBarView( child: TabBarView(
controller: _tabController, controller: _tabController,
children: [ children: [
Padding( if (_hasSeeds)
padding: const EdgeInsets.only(left: 22, right: 22), Padding(
child: _buildSeedTab(context, false), padding: const EdgeInsets.only(left: 22, right: 22),
), child: _buildSeedTab(context, false),
),
if (showKeyTab) if (showKeyTab)
Padding( Padding(
padding: const EdgeInsets.only(left: 22, right: 22), padding: const EdgeInsets.only(left: 22, right: 22),

View file

@ -1,3 +1,4 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
@ -162,18 +163,21 @@ abstract class WalletKeysViewModelBase with Store {
case WalletType.bitcoin: case WalletType.bitcoin:
case WalletType.litecoin: case WalletType.litecoin:
case WalletType.bitcoinCash: 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']!),
]);
break;
case WalletType.none: case WalletType.none:
case WalletType.haven: case WalletType.haven:
// final keys = bitcoin!.getWalletKeys(_appStore.wallet!);
//
// items.addAll([
// if (keys['wif'] != null)
// StandartListItem(title: "WIF", value: keys['wif']!),
// if (keys['privateKey'] != null)
// StandartListItem(title: S.current.private_key, value: keys['privateKey']!),
// if (keys['publicKey'] != null)
// StandartListItem(title: S.current.public_key, value: keys['publicKey']!),
// ]);
break; break;
} }

View file

@ -32,17 +32,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
WalletRestoreViewModelBase(AppStore appStore, WalletCreationService walletCreationService, WalletRestoreViewModelBase(AppStore appStore, WalletCreationService walletCreationService,
Box<WalletInfo> walletInfoSource, SeedSettingsViewModel seedSettingsViewModel, Box<WalletInfo> walletInfoSource, SeedSettingsViewModel seedSettingsViewModel,
{required WalletType type, this.restoredWallet}) {required WalletType type, this.restoredWallet})
: hasSeedLanguageSelector = : isButtonEnabled = false,
type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero,
hasBlockchainHeightLanguageSelector =
type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero,
hasRestoreFromPrivateKey = type == WalletType.ethereum ||
type == WalletType.polygon ||
type == WalletType.nano ||
type == WalletType.banano ||
type == WalletType.solana ||
type == WalletType.tron,
isButtonEnabled = false,
hasPassphrase = false, hasPassphrase = false,
mode = restoredWallet?.restoreMode ?? WalletRestoreMode.seed, mode = restoredWallet?.restoreMode ?? WalletRestoreMode.seed,
super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel, super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel,
@ -60,9 +50,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
case WalletType.ethereum: case WalletType.ethereum:
case WalletType.polygon: case WalletType.polygon:
case WalletType.decred: case WalletType.decred:
case WalletType.bitcoin:
availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys];
break; break;
case WalletType.bitcoin:
case WalletType.litecoin: case WalletType.litecoin:
case WalletType.bitcoinCash: case WalletType.bitcoinCash:
case WalletType.zano: case WalletType.zano:
@ -82,9 +72,34 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
static const decredSeedMnemonicLength = 15; static const decredSeedMnemonicLength = 15;
late List<WalletRestoreMode> availableModes; late List<WalletRestoreMode> availableModes;
final bool hasSeedLanguageSelector; late final bool hasSeedLanguageSelector = [
final bool hasBlockchainHeightLanguageSelector; WalletType.monero,
final bool hasRestoreFromPrivateKey; WalletType.haven,
WalletType.wownero
].contains(type);
late final bool hasBlockchainHeightSelector = [
WalletType.monero,
WalletType.haven,
WalletType.wownero
].contains(type);
late final bool hasRestoreFromPrivateKey = [
WalletType.ethereum,
WalletType.polygon,
WalletType.nano,
WalletType.banano,
WalletType.solana,
WalletType.tron
].contains(type);
late final bool onlyViewKeyRestore = [
WalletType.bitcoin,
WalletType.litecoin,
WalletType.bitcoinCash,
WalletType.decred
].contains(type);
final RestoredWallet? restoredWallet; final RestoredWallet? restoredWallet;
@observable @observable
@ -198,6 +213,13 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
final address = options['address'] as String?; final address = options['address'] as String?;
switch (type) { switch (type) {
case WalletType.bitcoin:
return bitcoin!.createBitcoinWalletFromKeys(
name: name,
password: password,
xpub: viewKey!,
);
case WalletType.monero: case WalletType.monero:
return monero!.createMoneroRestoreWalletFromKeysCredentials( return monero!.createMoneroRestoreWalletFromKeysCredentials(
name: name, name: name,
@ -276,8 +298,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
case WalletType.litecoin: case WalletType.litecoin:
String? mnemonic = credentials['seed'] as String?; String? mnemonic = credentials['seed'] as String?;
String? passphrase = credentials['passphrase'] as String?; String? passphrase = credentials['passphrase'] as String?;
if (mnemonic == null) break;
return bitcoin!.getDerivationsFromMnemonic( return bitcoin!.getDerivationsFromMnemonic(
mnemonic: mnemonic!, mnemonic: mnemonic,
node: node, node: node,
passphrase: passphrase, passphrase: passphrase,
); );

View file

@ -163,6 +163,7 @@ abstract class Bitcoin {
String? passphrase, String? passphrase,
}); });
WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({required String name, required String password, required String wif, WalletInfo? walletInfo}); WalletCredentials createBitcoinRestoreWalletFromWIFCredentials({required String name, required String password, required String wif, WalletInfo? walletInfo});
WalletCredentials createBitcoinWalletFromKeys({required String name, required String password, required String xpub});
WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? passphrase, String? mnemonic}); WalletCredentials createBitcoinNewWalletCredentials({required String name, WalletInfo? walletInfo, String? password, String? passphrase, String? mnemonic});
WalletCredentials createBitcoinHardwareWalletCredentials({required String name, required HardwareAccountData accountData, WalletInfo? walletInfo}); WalletCredentials createBitcoinHardwareWalletCredentials({required String name, required HardwareAccountData accountData, WalletInfo? walletInfo});
List<String> getWordList(); List<String> getWordList();