mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-28 06:28:41 +01:00
CW948: feat: bitcoin view-only (#2261)
* feat: bitcoin view-only * fix: add UR to cw_bitcoin, fix payjoin issue * feat: add unsignedPsbt to PendingBitcoinTransaction * wip * feat: xpub restore from bitcoin URI QR * feat(dev): debug UR codes [skip ci] * fix: master fingerprint default in create bitcoin transaction * fix: handle empty private key in bitcoin transaction building * fix: show success dialog after btc ur transaction * fix(ui): remove SP from UI in view only wallet * fix(cw_bitcoin): tx sending on non-airgap * fix(cw_bitcoin): disable all non-segwit addresses * feat(ur): support BBQR for coldcard qr * flutter 3.27.0 -> 3.27.4 * feat: raw xpub qr support for coldcard q * fix: update ledger_bitcoin fix: update switching button * fix: restore button not showing up after scanning * feat: commit raw tx (from coldcard Q) * feat: legacy crypto-psbt support for seed signer * chore: address comments from review * fix(cw_bitcoin): disable sp and payjoin for cupcake * feat: enable DNSSEC BIP353 in PSBT (#2397) * feat: enable DNSSEC BIP353 in PSBT * chore: put bitcoin view only behind a featureflag * fix: update Monero wallet to work with new internal URQR return format * feat: add "Restore from Cupcake App" option with localized strings * feat: enable DNSSEC BIP353 in PSBT using dnssec-prover (#2401) * fix: use dnssec-prover chore: bump ndk to r28c * fix: validate DNSSEC proof before updating the UI * fix: rename button to copy debug info to not hide actions on URQR widget --------- Co-authored-by: Konstantin Ullrich <konstantinullrich12@gmail.com>
This commit is contained in:
co-authored by
Konstantin Ullrich
parent
10499814bb
commit
e4806ad141
@@ -18,6 +18,14 @@ class CWBitcoin extends Bitcoin {
|
||||
passphrase: passphrase,
|
||||
);
|
||||
|
||||
@override
|
||||
WalletCredentials createBitcoinWalletFromKeys({
|
||||
required String name,
|
||||
required String password,
|
||||
required String xpub,
|
||||
}) =>
|
||||
BitcoinWalletFromKeysCredentials(name: name, password: password, xpub: xpub);
|
||||
|
||||
@override
|
||||
WalletCredentials createBitcoinRestoreWalletFromWIFCredentials(
|
||||
{required String name,
|
||||
@@ -62,11 +70,7 @@ class CWBitcoin extends Bitcoin {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
final keys = bitcoinWallet.keys;
|
||||
|
||||
return <String, String>{
|
||||
'wif': keys.wif,
|
||||
'privateKey': keys.privateKey,
|
||||
'publicKey': keys.publicKey
|
||||
};
|
||||
return bitcoinWallet.keys.toJson();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -126,7 +130,8 @@ class CWBitcoin extends Bitcoin {
|
||||
extractedAddress: out.extractedAddress,
|
||||
isParsedAddress: out.isParsedAddress,
|
||||
formattedCryptoAmount: out.formattedCryptoAmount,
|
||||
memo: out.memo))
|
||||
memo: out.memo,
|
||||
extra: out.extra))
|
||||
.toList(),
|
||||
priority: priority as BitcoinTransactionPriority,
|
||||
feeRate: bitcoinFeeRate,
|
||||
@@ -275,7 +280,14 @@ class CWBitcoin extends Bitcoin {
|
||||
}
|
||||
|
||||
@override
|
||||
List<ReceivePageOption> getBitcoinReceivePageOptions() => BitcoinReceivePageOption.all;
|
||||
List<ReceivePageOption> getBitcoinReceivePageOptions(Object wallet) {
|
||||
final bitcoinWallet = wallet as ElectrumWallet;
|
||||
final keys = bitcoinWallet.keys;
|
||||
if (keys.privateKey.isEmpty) {
|
||||
return BitcoinReceivePageOption.allViewOnly;
|
||||
}
|
||||
return BitcoinReceivePageOption.all;
|
||||
}
|
||||
|
||||
@override
|
||||
List<ReceivePageOption> getLitecoinReceivePageOptions() {
|
||||
@@ -727,6 +739,12 @@ class CWBitcoin extends Bitcoin {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> commitPsbtUR(Object wallet, List<String> urCodes) {
|
||||
final _wallet = wallet as BitcoinWalletBase;
|
||||
return _wallet.commitPsbtUR(urCodes);
|
||||
}
|
||||
|
||||
@override
|
||||
String getPayjoinEndpoint(Object wallet) {
|
||||
final _wallet = wallet as ElectrumWallet;
|
||||
|
||||
+1
-1
@@ -978,7 +978,7 @@ Future<void> setup({
|
||||
|
||||
getIt.registerFactory(() => AnimatedURModel(getIt.get<AppStore>()));
|
||||
|
||||
getIt.registerFactoryParam<AnimatedURPage, String, void>((String urQr, _) =>
|
||||
getIt.registerFactoryParam<AnimatedURPage, Map<String, String>, void>((Map<String, String> urQr, _) =>
|
||||
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
|
||||
|
||||
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>(
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:basic_utils/basic_utils.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_picker_option.dart';
|
||||
@@ -5,6 +8,7 @@ import 'package:cake_wallet/src/widgets/picker.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:dnssec_proof/dnssec_proof.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Bip353Record {
|
||||
@@ -22,6 +26,19 @@ class Bip353Record {
|
||||
'address': 'On-Chain Address',
|
||||
};
|
||||
|
||||
static Future<String?> fetchDnsProof(String bip353Name) async {
|
||||
if (bip353Name.startsWith('₿')) {
|
||||
bip353Name = bip353Name.substring(1);
|
||||
}
|
||||
final parts = bip353Name.split('@');
|
||||
if (parts.length != 2) return null;
|
||||
final userPart = parts[0];
|
||||
final domainPart = parts[1];
|
||||
final bip353Domain = '$userPart.user._bitcoin-payment.$domainPart.';
|
||||
final proof = await Isolate.run(() => DnsProver.getTxtProof(bip353Domain));
|
||||
return base64.encode(proof);
|
||||
}
|
||||
|
||||
static Future<Map<String, String>?> fetchUriByCryptoCurrency(
|
||||
String bip353Name, String asset) async {
|
||||
try {
|
||||
@@ -30,7 +47,10 @@ class Bip353Record {
|
||||
|
||||
if (parts.length != 2) return null;
|
||||
|
||||
final userPart = parts[0];
|
||||
String userPart = parts[0];
|
||||
if (userPart.startsWith('₿')) {
|
||||
userPart = userPart.substring(1);
|
||||
}
|
||||
final domainPart = parts[1];
|
||||
|
||||
// 2. Construct the correct subdomain: "user._bitcoin-payment.domain"
|
||||
@@ -42,6 +62,10 @@ class Bip353Record {
|
||||
RRecordType.TXT,
|
||||
dnssec: true,
|
||||
);
|
||||
final proof = await fetchDnsProof(bip353Name);
|
||||
if (proof == null) {
|
||||
throw Exception('DNSSEC proof not found');
|
||||
}
|
||||
|
||||
if (txtRecords == null) return null;
|
||||
|
||||
|
||||
@@ -344,7 +344,13 @@ class AddressResolver {
|
||||
if (bip353AddressMap != null && bip353AddressMap.isNotEmpty) {
|
||||
final chosenAddress = await Bip353Record.pickBip353AddressChoice(context, text, bip353AddressMap);
|
||||
if (chosenAddress != null) {
|
||||
return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text);
|
||||
try {
|
||||
final dnsProof = await Bip353Record.fetchDnsProof(text);
|
||||
return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text, dnsProof: dnsProof);
|
||||
} catch (e) {
|
||||
printV('Bip353Record.fetchBip353AddressAddress error: $e');
|
||||
return ParsedAddress.fetchBip353AddressAddress(address: chosenAddress, name: text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ class ParsedAddress {
|
||||
this.profileImageUrl = '',
|
||||
this.profileName = '',
|
||||
this.parseFrom = ParseFrom.notParsed,
|
||||
this.bip353DnsProof,
|
||||
});
|
||||
|
||||
factory ParsedAddress.fetchEmojiAddress({
|
||||
@@ -59,11 +60,13 @@ class ParsedAddress {
|
||||
factory ParsedAddress.fetchBip353AddressAddress ({
|
||||
required String address,
|
||||
required String name,
|
||||
String? dnsProof,
|
||||
}) {
|
||||
return ParsedAddress(
|
||||
addresses: [address],
|
||||
name: name,
|
||||
parseFrom: ParseFrom.bip353,
|
||||
bip353DnsProof: dnsProof,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -178,4 +181,5 @@ class ParsedAddress {
|
||||
final String profileImageUrl;
|
||||
final String profileName;
|
||||
final ParseFrom parseFrom;
|
||||
final String? bip353DnsProof;
|
||||
}
|
||||
|
||||
@@ -399,9 +399,9 @@ class CWMonero extends Monero {
|
||||
}
|
||||
|
||||
@override
|
||||
String exportOutputsUR(Object wallet, bool all) {
|
||||
Map<String, String> exportOutputsUR(Object wallet) {
|
||||
final moneroWallet = wallet as MoneroWallet;
|
||||
return moneroWallet.exportOutputsUR(all);
|
||||
return moneroWallet.exportOutputsUR();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -115,7 +115,8 @@ class QRWidget extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
if (addressListViewModel.isPayjoinUnavailable &&
|
||||
!addressListViewModel.isSilentPayments) ...[
|
||||
!addressListViewModel.isSilentPayments &&
|
||||
!addressListViewModel.isCupcake) ...[
|
||||
GestureDetector(
|
||||
onTap: () => _onPayjoinInactivePressed(context),
|
||||
child: Row(
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/option_tile.dart';
|
||||
import 'package:cake_wallet/utils/device_info.dart';
|
||||
import 'package:cake_wallet/utils/feature_flag.dart';
|
||||
import 'package:cake_wallet/utils/permission_handler.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
@@ -53,13 +54,40 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> {
|
||||
return true;
|
||||
}
|
||||
|
||||
String get imageRestoreHWPath =>
|
||||
widget.themeType == ThemeType.dark
|
||||
? 'assets/images/restore_hw_dark.png'
|
||||
: 'assets/images/restore_hw.png';
|
||||
|
||||
String get imageRestoreCupcakePath =>
|
||||
widget.themeType == ThemeType.dark
|
||||
? 'assets/images/restore_cupcake_dark.png'
|
||||
: 'assets/images/restore_cupcake.png';
|
||||
|
||||
String get imageRestoreQRPath =>
|
||||
widget.themeType == ThemeType.dark
|
||||
? 'assets/images/restore_qr_dark.png'
|
||||
: 'assets/images/restore_qr.png';
|
||||
|
||||
|
||||
String get imageRestoreHotWalletPath =>
|
||||
widget.themeType == ThemeType.dark
|
||||
? 'assets/images/restore_hot_wallet_dark.png'
|
||||
: 'assets/images/restore_hot_wallet.png';
|
||||
|
||||
|
||||
String get imageRestoreBackupPath =>
|
||||
widget.themeType == ThemeType.dark
|
||||
? 'assets/images/restore_backup_dark.png'
|
||||
: 'assets/images/restore_backup.png';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final imageColor = Theme.of(context).colorScheme.primary;
|
||||
final imageLedger = Image.asset('assets/images/hardware_wallet/ledger_nano_x.png',
|
||||
width: 40, color: imageColor);
|
||||
final imageSeedKeys = Image.asset('assets/images/restore_wallet_image.png', color: imageColor);
|
||||
final imageBackup = Image.asset('assets/images/backup.png', color: imageColor);
|
||||
final imageRestoreHW = Image.asset(imageRestoreHWPath, width: 55);
|
||||
final imageRestoreCupcake = Image.asset(imageRestoreCupcakePath, width: 55);
|
||||
final imageRestoreQR = Image.asset(imageRestoreQRPath, width: 55);
|
||||
final imageSeedKeys = Image.asset(imageRestoreHotWalletPath, width: 55);
|
||||
final imageRestoreBackup = Image.asset(imageRestoreBackupPath, width: 55);
|
||||
|
||||
return Center(
|
||||
child: Container(
|
||||
@@ -76,15 +104,16 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> {
|
||||
title: S.of(context).restore_title_from_seed_keys,
|
||||
description: S.of(context).restore_description_from_seed_keys,
|
||||
),
|
||||
if (widget.isNewInstall)
|
||||
if (FeatureFlag.hasBitcoinViewOnly && DeviceInfo.instance.isMobile)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: OptionTile(
|
||||
key: ValueKey('restore_options_from_backup_button_key'),
|
||||
onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup),
|
||||
image: imageBackup,
|
||||
title: S.of(context).restore_title_from_backup,
|
||||
description: S.of(context).restore_description_from_backup,
|
||||
key: ValueKey('restore_options_from_cupcake_button_key'),
|
||||
onPressed: () => _onScanQRCode(context),
|
||||
image: imageRestoreCupcake,
|
||||
title: S.of(context).restore_title_from_cupcake,
|
||||
description: S.of(context).restore_description_from_cupcake,
|
||||
tag: S.of(context).new_tag,
|
||||
),
|
||||
),
|
||||
if (_doesSupportHardwareWallets)
|
||||
@@ -94,25 +123,33 @@ class _RestoreOptionsBodyState extends State<_RestoreOptionsBody> {
|
||||
key: ValueKey('restore_options_from_hardware_wallet_button_key'),
|
||||
onPressed: () =>
|
||||
Navigator.pushNamed(context, Routes.restoreWalletFromHardwareWallet),
|
||||
image: imageLedger,
|
||||
image: imageRestoreHW,
|
||||
title: S.of(context).restore_title_from_hardware_wallet,
|
||||
description: S.of(context).restore_description_from_hardware_wallet,
|
||||
),
|
||||
),
|
||||
if (widget.isNewInstall)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: OptionTile(
|
||||
key: ValueKey('restore_options_from_backup_button_key'),
|
||||
onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup),
|
||||
image: imageRestoreBackup,
|
||||
title: S.of(context).restore_title_from_backup,
|
||||
description: S.of(context).restore_description_from_backup,
|
||||
),
|
||||
),
|
||||
if (DeviceInfo.instance.isMobile)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: OptionTile(
|
||||
key: ValueKey('restore_options_from_qr_button_key'),
|
||||
onPressed: () => _onScanQRCode(context),
|
||||
icon: Icon(
|
||||
Icons.qr_code_rounded,
|
||||
color: imageColor,
|
||||
size: 50,
|
||||
),
|
||||
image: imageRestoreQR,
|
||||
title: S.of(context).scan_qr_code,
|
||||
description: S.of(context).cold_or_recover_wallet),
|
||||
)
|
||||
description: S.of(context).cold_or_recover_wallet,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
|
||||
@@ -198,14 +198,14 @@ class WalletRestoreFromKeysFormState extends State<WalletRestoreFromKeysForm> {
|
||||
Widget _restoreFromKeysFormFields() {
|
||||
// Decred can only restore a view only wallet with an account pubkey. Other
|
||||
// fields are not used.
|
||||
if (widget.walletRestoreViewModel.type == WalletType.decred) {
|
||||
if (widget.walletRestoreViewModel.onlyViewKeyRestore) {
|
||||
return Column(
|
||||
children: [
|
||||
BaseTextFormField(
|
||||
controller: viewKeyController,
|
||||
hintText: S.of(context).view_key_public,
|
||||
maxLines: null,
|
||||
)
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -253,13 +253,14 @@ class WalletRestoreFromKeysFormState extends State<WalletRestoreFromKeysForm> {
|
||||
maxLines: null,
|
||||
),
|
||||
),
|
||||
BlockchainHeightWidget(
|
||||
key: blockchainHeightKey,
|
||||
hasDatePicker: widget.walletRestoreViewModel.type != WalletType.haven,
|
||||
onHeightChange: (_) => null,
|
||||
onHeightOrDateEntered: widget.onHeightOrDateEntered,
|
||||
walletType: widget.walletRestoreViewModel.type,
|
||||
),
|
||||
if (widget.walletRestoreViewModel.hasBlockchainHeightSelector)
|
||||
BlockchainHeightWidget(
|
||||
key: blockchainHeightKey,
|
||||
hasDatePicker: widget.walletRestoreViewModel.type != WalletType.haven,
|
||||
onHeightChange: (_) => null,
|
||||
onHeightOrDateEntered: widget.onHeightOrDateEntered,
|
||||
walletType: widget.walletRestoreViewModel.type,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ class WalletRestorePage extends BasePage {
|
||||
credentials['seed'] =
|
||||
walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.text;
|
||||
|
||||
if (walletRestoreViewModel.hasBlockchainHeightLanguageSelector) {
|
||||
if (walletRestoreViewModel.hasBlockchainHeightSelector) {
|
||||
credentials['height'] =
|
||||
walletRestoreFromSeedFormKey.currentState!.blockchainHeightKey.currentState?.height ??
|
||||
-1;
|
||||
@@ -219,7 +219,7 @@ class WalletRestorePage extends BasePage {
|
||||
credentials['name'] =
|
||||
walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text;
|
||||
credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text;
|
||||
if (walletRestoreViewModel.type != WalletType.decred) {
|
||||
if (!walletRestoreViewModel.onlyViewKeyRestore) {
|
||||
credentials['address'] =
|
||||
walletRestoreFromKeysFormKey.currentState!.addressController.text;
|
||||
credentials['spendKey'] =
|
||||
@@ -519,7 +519,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
|
||||
}
|
||||
},
|
||||
onViewKeyEntered: (bool entered) {
|
||||
if (walletRestoreViewModel.type == WalletType.decred) {
|
||||
if (widget.walletRestoreViewModel.onlyViewKeyRestore) {
|
||||
walletRestoreViewModel.isButtonEnabled = entered;
|
||||
}
|
||||
},
|
||||
@@ -536,7 +536,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
|
||||
key: widget.walletRestoreFromSeedFormKey,
|
||||
restoredWallet: walletRestoreViewModel.restoredWallet,
|
||||
seedSettingsViewModel: widget.seedSettingsViewModel,
|
||||
displayBlockHeightSelector: widget.walletRestoreViewModel.hasBlockchainHeightLanguageSelector,
|
||||
displayBlockHeightSelector: widget.walletRestoreViewModel.hasBlockchainHeightSelector,
|
||||
displayLanguageSelector: widget.walletRestoreViewModel.hasSeedLanguageSelector,
|
||||
type: widget.walletRestoreViewModel.type,
|
||||
blockHeightFocusNode: widget.blockHeightFocusNode,
|
||||
@@ -563,7 +563,7 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody>
|
||||
}
|
||||
|
||||
void _validateOnChange({bool isPolyseed = false}) {
|
||||
if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightLanguageSelector) {
|
||||
if (!isPolyseed && walletRestoreViewModel.hasBlockchainHeightSelector) {
|
||||
final hasHeight = walletRestoreFromSeedFormKey
|
||||
.currentState?.blockchainHeightKey.currentState?.restoreHeightController.text.isNotEmpty;
|
||||
|
||||
|
||||
@@ -450,7 +450,7 @@ class SendPage extends BasePage {
|
||||
}
|
||||
if (monero!.needExportOutputs(sendViewModel.wallet, amount)) {
|
||||
await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage,
|
||||
arguments: 'export-outputs');
|
||||
arguments: monero!.exportOutputsUR(sendViewModel.wallet));
|
||||
await Future.delayed(
|
||||
Duration(seconds: 1)); // wait for monero to refresh the state
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bbqrdart/bbqrdart.dart';
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/entities/qr_scanner.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/monero/monero.dart';
|
||||
@@ -7,31 +9,49 @@ import 'package:cake_wallet/src/screens/base_page.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cake_wallet/utils/feature_flag.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/view_model/animated_ur_model.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
// ur:xmr-txunsigned - unsigned transaction
|
||||
// should show a scanner afterwards.
|
||||
|
||||
class AnimatedURPage extends BasePage {
|
||||
final bool isAll;
|
||||
AnimatedURPage(this.animatedURmodel, {required String urQr, this.isAll = false}) {
|
||||
if (urQr == "export-outputs") {
|
||||
this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, false);
|
||||
} else if (urQr == "export-outputs-all") {
|
||||
this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, true);
|
||||
} else {
|
||||
this.urQr = urQr;
|
||||
AnimatedURPage(this.animatedURmodel, {
|
||||
required Map<String, String> this.urQr,
|
||||
this.isAll = false,
|
||||
}) {
|
||||
if (urQr.keys.first.startsWith("export-outputs") && animatedURmodel.wallet.type == WalletType.monero) {
|
||||
if (animatedURmodel.wallet.type == WalletType.monero) {
|
||||
urQr = monero!.exportOutputsUR(animatedURmodel.wallet);
|
||||
} else {
|
||||
throw UnimplementedError("unable to handle UR: ${urQr.keys.first}");
|
||||
}
|
||||
}
|
||||
for (var key in urQr.keys) {
|
||||
if (key.isEmpty || urQr[key]!.isEmpty) {
|
||||
urQr.remove(key);
|
||||
continue;
|
||||
}
|
||||
urQr[key] = urQr[key]!.trim();
|
||||
}
|
||||
}
|
||||
|
||||
late String urQr;
|
||||
Map<String, String> urQr = {};
|
||||
|
||||
|
||||
final AnimatedURModel animatedURmodel;
|
||||
|
||||
String get urQrType {
|
||||
final first = urQr.trim().split("\n")[0];
|
||||
if (urQr.values.first.trim().substring(0, 2) == BBQR.header) {
|
||||
return BBQR.header;
|
||||
}
|
||||
if (urQr.isEmpty) return "unknown";
|
||||
final first = urQr.values.first.trim().split("\n")[0];
|
||||
return first.split('/')[0];
|
||||
}
|
||||
|
||||
@@ -43,11 +63,11 @@ class AnimatedURPage extends BasePage {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 64.0),
|
||||
child: URQR(
|
||||
frames: urQr.trim().split("\n"),
|
||||
urqr: urQr,
|
||||
),
|
||||
),
|
||||
if (["ur:xmr-txunsigned", "ur:xmr-output", "ur:psbt", BBQR.header].contains(urQrType)) ...{
|
||||
SizedBox(height: 32),
|
||||
if (urQrType == "ur:xmr-txunsigned" || urQrType == "ur:xmr-output")
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SizedBox(
|
||||
@@ -60,30 +80,8 @@ class AnimatedURPage extends BasePage {
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 32),
|
||||
if (urQrType == "ur:xmr-output" && !isAll) Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: PrimaryButton(
|
||||
onPressed: () => _exportAll(context),
|
||||
text: "Export all",
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _exportAll(BuildContext context) {
|
||||
Navigator.of(context).pushReplacement(
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return AnimatedURPage(animatedURmodel, urQr: "export-outputs-all", isAll: true);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,6 +104,11 @@ class AnimatedURPage extends BasePage {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
break;
|
||||
case "ur:psbt": // psbt
|
||||
final ur = await presentQRScanner(context);
|
||||
if (ur == null) return;
|
||||
await bitcoin!.commitPsbtUR(animatedURmodel.wallet, ur.trim().split("\n"));
|
||||
Navigator.of(context).pop(true);
|
||||
default:
|
||||
throw UnimplementedError("unable to handle UR: ${urQrType}");
|
||||
}
|
||||
@@ -124,9 +127,9 @@ class AnimatedURPage extends BasePage {
|
||||
}
|
||||
|
||||
class URQR extends StatefulWidget {
|
||||
URQR({super.key, required this.frames});
|
||||
URQR({super.key, required this.urqr});
|
||||
|
||||
List<String> frames;
|
||||
final Map<String, String> urqr;
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
@@ -160,6 +163,24 @@ class _URQRState extends State<URQR> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
late String selected = (widget.urqr.isEmpty) ? "unknown" : widget.urqr.keys.first;
|
||||
int selectedInt = 0;
|
||||
|
||||
List<String> get frames {
|
||||
return widget.urqr[selected]?.split("\n") ?? [];
|
||||
}
|
||||
|
||||
late String nextLabel = widget.urqr.keys.toList()[(selectedInt + 1) % widget.urqr.length] ;
|
||||
|
||||
void next() {
|
||||
final keys = widget.urqr.keys.toList();
|
||||
setState(() {
|
||||
selectedInt++;
|
||||
nextLabel = keys[(selectedInt + 1) % keys.length];
|
||||
selected = keys[(selectedInt) % keys.length];
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@@ -168,10 +189,37 @@ class _URQRState extends State<URQR> {
|
||||
children: [
|
||||
Center(
|
||||
child: QrImage(
|
||||
data: widget.frames[frame % widget.frames.length], version: -1,
|
||||
data: frames[frame % frames.length],
|
||||
version: -1,
|
||||
size: 400,
|
||||
),
|
||||
),
|
||||
if (widget.urqr.values.length > 1)
|
||||
SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: PrimaryButton(
|
||||
onPressed: next,
|
||||
text: nextLabel,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (FeatureFlag.hasDevOptions) ...{
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: """Current frame (${frame % frames.length}): ${frames[frame % frames.length]},
|
||||
All frames:
|
||||
- ${frames.join("\n - ")}"""));
|
||||
},
|
||||
child: Text("[dev] copy debug info"),
|
||||
),
|
||||
}
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
|
||||
showLegacySeedTab = widget.walletKeysViewModel.legacySeedSplit.isNotEmpty;
|
||||
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);
|
||||
}
|
||||
@@ -126,7 +126,7 @@ class _WalletKeysPageBodyState extends State<WalletKeysPageBody>
|
||||
dividerColor: Colors.transparent,
|
||||
padding: EdgeInsets.zero,
|
||||
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 (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(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 22, right: 22),
|
||||
child: _buildSeedTab(context, false),
|
||||
),
|
||||
if (_hasSeeds)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 22, right: 22),
|
||||
child: _buildSeedTab(context, false),
|
||||
),
|
||||
if (showKeyTab)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 22, right: 22),
|
||||
|
||||
@@ -7,6 +7,7 @@ class OptionTile extends StatelessWidget {
|
||||
this.icon,
|
||||
required this.title,
|
||||
required this.description,
|
||||
this.tag,
|
||||
super.key,
|
||||
}) : assert(image != null || icon != null);
|
||||
|
||||
@@ -15,6 +16,7 @@ class OptionTile extends StatelessWidget {
|
||||
final Icon? icon;
|
||||
final String title;
|
||||
final String description;
|
||||
final String? tag;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -36,25 +38,34 @@ class OptionTile extends StatelessWidget {
|
||||
icon ?? image!,
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 16),
|
||||
padding: EdgeInsets.only(left: 20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 220),
|
||||
child: Text(title, style: Theme.of(context).textTheme.titleMedium),
|
||||
),
|
||||
if (tag != null) Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: 5),
|
||||
margin: EdgeInsets.only(left: 5),
|
||||
child: Text(tag!))
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 5),
|
||||
child: Text(
|
||||
description,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -45,7 +45,7 @@ class SettingActions {
|
||||
name: (context) => S.of(context).export_outputs,
|
||||
image: 'assets/images/monero_menu.png',
|
||||
onTap: (BuildContext context) {
|
||||
Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: 'export-outputs');
|
||||
Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: {'export-outputs': 'export-outputs'});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -9,4 +9,5 @@ class FeatureFlag {
|
||||
static final bool isInAppTorEnabled = (Platform.isAndroid);
|
||||
static const int verificationWordsCount = kDebugMode ? 0 : 2;
|
||||
static const bool hasDevOptions = bool.fromEnvironment('hasDevOptions', defaultValue: kDebugMode);
|
||||
static const bool hasBitcoinViewOnly = true;
|
||||
}
|
||||
|
||||
@@ -536,7 +536,10 @@ abstract class DashboardViewModelBase with Store {
|
||||
}
|
||||
|
||||
@computed
|
||||
bool get hasSilentPayments => wallet.type == WalletType.bitcoin && !wallet.isHardwareWallet;
|
||||
bool get hasSilentPayments =>
|
||||
wallet.type == WalletType.bitcoin &&
|
||||
(bitcoin!.getWalletKeys(wallet)["privateKey"] ?? "").isNotEmpty &&
|
||||
!wallet.isHardwareWallet;
|
||||
|
||||
@computed
|
||||
bool get showSilentPaymentsCard => hasSilentPayments && settingsStore.silentPaymentsCardDisplay;
|
||||
|
||||
@@ -22,7 +22,7 @@ abstract class ReceiveOptionViewModelBase with Store {
|
||||
switch (walletType) {
|
||||
case WalletType.bitcoin:
|
||||
_options = [
|
||||
...bitcoin!.getBitcoinReceivePageOptions(),
|
||||
...bitcoin!.getBitcoinReceivePageOptions(_wallet),
|
||||
...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet)
|
||||
];
|
||||
break;
|
||||
|
||||
@@ -35,7 +35,7 @@ class RestoredWallet {
|
||||
|
||||
factory RestoredWallet.fromKey(Map<String, dynamic> json) {
|
||||
try {
|
||||
final codeParsed = jsonDecode(json['raw_qr'].toString());
|
||||
final codeParsed = jsonDecode(json['raw_qr'].toString());
|
||||
if (codeParsed["version"] == 0) {
|
||||
json['address'] = codeParsed["primaryAddress"];
|
||||
json['view_key'] = codeParsed["privateViewKey"];
|
||||
@@ -44,6 +44,7 @@ class RestoredWallet {
|
||||
} catch (e) {
|
||||
// fine, we don't care, it is only for monero anyway
|
||||
}
|
||||
json['view_key'] ??= json['xpub'];
|
||||
final height = json['height'] as String?;
|
||||
return RestoredWallet(
|
||||
restoreMode: json['mode'] as WalletRestoreMode,
|
||||
@@ -51,7 +52,7 @@ class RestoredWallet {
|
||||
address: json['address'] as String?,
|
||||
spendKey: json['spend_key'] as String?,
|
||||
viewKey: json['view_key'] as String?,
|
||||
height: height != null ? int.tryParse(height)??0 : 0,
|
||||
height: height != null ? int.tryParse(height) ?? 0 : 0,
|
||||
privateKey: json['private_key'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,6 +54,8 @@ class WalletRestoreFromQRCode {
|
||||
|
||||
final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key));
|
||||
|
||||
if (code.startsWith("xpub")) return WalletType.bitcoin;
|
||||
|
||||
if (extracted == null) {
|
||||
// Special case for view-only monero wallet
|
||||
try {
|
||||
@@ -117,11 +119,15 @@ class WalletRestoreFromQRCode {
|
||||
|
||||
formattedUri = seedPhrase != null
|
||||
? '$walletType:?seed=$seedPhrase'
|
||||
: throw Exception('Failed to determine valid seed phrase');
|
||||
: code.startsWith('xpub')
|
||||
? '$walletType:?xpub=$code'
|
||||
: throw Exception('Failed to determine valid seed phrase');
|
||||
} else {
|
||||
final index = code.indexOf(':');
|
||||
final query = code.substring(index + 1).replaceAll('?', '&');
|
||||
formattedUri = '$walletType:?$query';
|
||||
formattedUri = code.startsWith('xpub')
|
||||
? '$walletType:?xpub=$code'
|
||||
:'$walletType:?$query';
|
||||
}
|
||||
|
||||
final uri = Uri.parse(formattedUri);
|
||||
@@ -158,6 +164,10 @@ class WalletRestoreFromQRCode {
|
||||
throw Exception('Unexpected restore mode: tx_payment_id is invalid');
|
||||
}
|
||||
|
||||
if (credentials.containsKey("xpub")) {
|
||||
return WalletRestoreMode.keys;
|
||||
}
|
||||
|
||||
if (credentials['seed'] != null) {
|
||||
final seedValue = credentials['seed'] as String;
|
||||
final words = SeedValidator.getWordList(type: type, language: 'english');
|
||||
|
||||
@@ -289,6 +289,15 @@ abstract class OutputBase with Store {
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> get extra {
|
||||
final fields = <String, dynamic>{};
|
||||
if (parsedAddress.parseFrom == ParseFrom.bip353) {
|
||||
fields['bip353_name'] = parsedAddress.name;
|
||||
fields['bip353_proof'] = parsedAddress.bip353DnsProof;
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
void _setCryptoNumMaximumFractionDigits() {
|
||||
var maximumFractionDigits = 0;
|
||||
|
||||
|
||||
@@ -603,6 +603,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
|
||||
bool get isSilentPayments =>
|
||||
wallet.type == WalletType.bitcoin && bitcoin!.hasSelectedSilentPayments(wallet);
|
||||
|
||||
@computed
|
||||
bool get isCupcake =>
|
||||
wallet.type == WalletType.bitcoin &&
|
||||
(bitcoin!.getWalletKeys(wallet)["privateKey"] ?? "").isEmpty;
|
||||
|
||||
@computed
|
||||
bool get isAutoGenerateSubaddressEnabled =>
|
||||
_settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled &&
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/monero/monero.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.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']!),
|
||||
]);
|
||||
break;
|
||||
case WalletType.none:
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:cake_wallet/solana/solana.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/tron/tron.dart';
|
||||
import 'package:cake_wallet/decred/decred.dart';
|
||||
import 'package:cake_wallet/utils/feature_flag.dart';
|
||||
import 'package:cake_wallet/view_model/restore/restore_mode.dart';
|
||||
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
|
||||
import 'package:cake_wallet/view_model/seed_settings_view_model.dart';
|
||||
@@ -32,17 +33,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
|
||||
WalletRestoreViewModelBase(AppStore appStore, WalletCreationService walletCreationService,
|
||||
Box<WalletInfo> walletInfoSource, SeedSettingsViewModel seedSettingsViewModel,
|
||||
{required WalletType type, this.restoredWallet})
|
||||
: hasSeedLanguageSelector =
|
||||
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,
|
||||
: isButtonEnabled = restoredWallet != null,
|
||||
hasPassphrase = false,
|
||||
mode = restoredWallet?.restoreMode ?? WalletRestoreMode.seed,
|
||||
super(appStore, walletInfoSource, walletCreationService, seedSettingsViewModel,
|
||||
@@ -60,9 +51,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
|
||||
case WalletType.ethereum:
|
||||
case WalletType.polygon:
|
||||
case WalletType.decred:
|
||||
case WalletType.bitcoin:
|
||||
availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys];
|
||||
break;
|
||||
case WalletType.bitcoin:
|
||||
case WalletType.litecoin:
|
||||
case WalletType.bitcoinCash:
|
||||
case WalletType.zano:
|
||||
@@ -82,9 +73,32 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
|
||||
static const decredSeedMnemonicLength = 15;
|
||||
|
||||
late List<WalletRestoreMode> availableModes;
|
||||
final bool hasSeedLanguageSelector;
|
||||
final bool hasBlockchainHeightLanguageSelector;
|
||||
final bool hasRestoreFromPrivateKey;
|
||||
late final bool hasSeedLanguageSelector = [
|
||||
WalletType.monero,
|
||||
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 = [
|
||||
if (FeatureFlag.hasBitcoinViewOnly) WalletType.bitcoin,
|
||||
WalletType.decred
|
||||
].contains(type);
|
||||
|
||||
final RestoredWallet? restoredWallet;
|
||||
|
||||
@observable
|
||||
@@ -198,6 +212,13 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
|
||||
final address = options['address'] as String?;
|
||||
|
||||
switch (type) {
|
||||
case WalletType.bitcoin:
|
||||
return bitcoin!.createBitcoinWalletFromKeys(
|
||||
name: name,
|
||||
password: password,
|
||||
xpub: viewKey!,
|
||||
);
|
||||
|
||||
case WalletType.monero:
|
||||
return monero!.createMoneroRestoreWalletFromKeysCredentials(
|
||||
name: name,
|
||||
@@ -276,8 +297,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store {
|
||||
case WalletType.litecoin:
|
||||
String? mnemonic = credentials['seed'] as String?;
|
||||
String? passphrase = credentials['passphrase'] as String?;
|
||||
if (mnemonic == null) break;
|
||||
return bitcoin!.getDerivationsFromMnemonic(
|
||||
mnemonic: mnemonic!,
|
||||
mnemonic: mnemonic,
|
||||
node: node,
|
||||
passphrase: passphrase,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user