diff --git a/lib/pages/wallet_view/multisig_coordinator_view/multisig_coordinator_view.dart b/lib/pages/wallet_view/multisig_coordinator_view/multisig_coordinator_view.dart index c6e2f51de..2e92a3ff2 100644 --- a/lib/pages/wallet_view/multisig_coordinator_view/multisig_coordinator_view.dart +++ b/lib/pages/wallet_view/multisig_coordinator_view/multisig_coordinator_view.dart @@ -7,6 +7,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../themes/stack_colors.dart'; import '../../../../utilities/text_styles.dart'; import '../../../../widgets/background.dart'; +import '../../../providers/global/wallets_provider.dart'; +import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart'; import '../../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../../widgets/desktop/primary_button.dart'; import '../../../widgets/desktop/secondary_button.dart'; @@ -19,68 +21,6 @@ final multisigCoordinatorStateProvider = return MultisigCoordinatorState(); }); -class MultisigCoordinatorData { - const MultisigCoordinatorData({ - this.threshold = 2, - this.totalCosigners = 3, - this.coinType = 0, // Bitcoin mainnet. - this.accountIndex = 0, - this.scriptType = MultisigScriptType.nativeSegwit, - this.cosignerXpubs = const [], - }); - - final int threshold; - final int totalCosigners; - final int coinType; - final int accountIndex; - final MultisigScriptType scriptType; - final List cosignerXpubs; - - MultisigCoordinatorData copyWith({ - int? threshold, - int? totalCosigners, - int? coinType, - int? accountIndex, - MultisigScriptType? scriptType, - List? cosignerXpubs, - }) { - return MultisigCoordinatorData( - threshold: threshold ?? this.threshold, - totalCosigners: totalCosigners ?? this.totalCosigners, - coinType: coinType ?? this.coinType, - accountIndex: accountIndex ?? this.accountIndex, - scriptType: scriptType ?? this.scriptType, - cosignerXpubs: cosignerXpubs ?? this.cosignerXpubs, - ); - } - - Map toJson() => { - 'threshold': threshold, - 'totalCosigners': totalCosigners, - 'coinType': coinType, - 'accountIndex': accountIndex, - 'scriptType': scriptType.index, - 'cosignerXpubs': cosignerXpubs, - }; - - factory MultisigCoordinatorData.fromJson(Map json) { - return MultisigCoordinatorData( - threshold: json['threshold'] as int, - totalCosigners: json['totalCosigners'] as int, - coinType: json['coinType'] as int, - accountIndex: json['accountIndex'] as int, - scriptType: MultisigScriptType.values[json['scriptType'] as int], - cosignerXpubs: (json['cosignerXpubs'] as List).cast(), - ); - } -} - -enum MultisigScriptType { - legacy, // P2SH. - segwit, // P2SH-P2WSH. - nativeSegwit, // P2WSH. -} - class MultisigCoordinatorState extends StateNotifier { MultisigCoordinatorState() : super(const MultisigCoordinatorData()); @@ -109,11 +49,13 @@ class MultisigCoordinatorView extends ConsumerStatefulWidget { const MultisigCoordinatorView({ super.key, required this.walletId, + required this.scriptType, required this.totalCosigners, required this.threshold, }); final String walletId; + final MultisigScriptType scriptType; final int totalCosigners; final int threshold; @@ -126,6 +68,7 @@ class MultisigCoordinatorView extends ConsumerStatefulWidget { class _MultisigSetupViewState extends ConsumerState { final List xpubControllers = []; + String _myXpub = ""; // bool _isNfcAvailable = false; // String _nfcStatus = 'Checking NFC availability...'; @@ -134,10 +77,28 @@ class _MultisigSetupViewState extends ConsumerState { super.initState(); // Initialize controllers. - for (int i = 0; i < widget.totalCosigners; i++) { + for (int i = 0; i < widget.totalCosigners - 1; i++) { xpubControllers.add(TextEditingController()); } + // Get and set my xpub. + WidgetsBinding.instance.addPostFrameCallback((_) async { + final targetPath = _getTargetPathForScriptType(widget.scriptType); + final xpubData = await (ref.read(pWallets).getWallet(widget.walletId) + as ExtendedKeysInterface) + .getXPubs(bip48: true); + print(xpubData); + + final matchingPub = xpubData.xpubs.firstWhere( + (pub) => pub.path == targetPath, + orElse: () => XPub(path: "", encoded: "xPub not found!"), + ); + + if (matchingPub.path.isNotEmpty && mounted) { + setState(() => _myXpub = matchingPub.encoded); + } + }); + // _checkNfcAvailability(); } @@ -237,7 +198,7 @@ class _MultisigSetupViewState extends ConsumerState { }, ), title: Text( - "Enter cosigner xpubs", + "Enter cosigner xPubs", style: STextStyles.navBarTitle(context), ), titleSpacing: 0, @@ -256,7 +217,7 @@ class _MultisigSetupViewState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "This is your extended public key (xpub) for each cosigner. " + "This is your extended public key (xPub) for each cosigner. " "Share it with each participant.", style: STextStyles.itemSubtitle(context), ), @@ -268,7 +229,7 @@ class _MultisigSetupViewState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Your xpub", + "Your xPub", style: STextStyles.w500_14(context).copyWith( color: Theme.of(context) .extension()! @@ -280,15 +241,14 @@ class _MultisigSetupViewState extends ConsumerState { children: [ Expanded( child: TextField( - controller: xpubControllers[0], - enabled: - false, // Make field non-interactive + controller: TextEditingController( + text: _myXpub), + enabled: false, decoration: InputDecoration( - hintText: "xpub...", + hintText: "xPub...", hintStyle: STextStyles.fieldLabel(context), - filled: - true, // Add background to show disabled state + filled: true, fillColor: Theme.of(context) .extension()! .textFieldDefaultBG, @@ -322,18 +282,8 @@ class _MultisigSetupViewState extends ConsumerState { .buttonTextSecondary, ), onPressed: () async { - final data = await Clipboard.getData( - 'text/plain'); - if (data?.text != null) { - xpubControllers[0].text = data!.text!; - ref - .read( - multisigCoordinatorStateProvider - .notifier) - .addCosignerXpub(data.text!); - setState( - () {}); // Trigger rebuild to update button state. - } + await Clipboard.setData( + ClipboardData(text: _myXpub)); }, ), ], @@ -350,14 +300,14 @@ class _MultisigSetupViewState extends ConsumerState { const SizedBox(height: 24), // Generate input fields for each cosigner. - for (int i = 0; i < widget.totalCosigners; i++) + for (int i = 1; i < widget.totalCosigners; i++) Padding( padding: const EdgeInsets.only(bottom: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Cosigner ${i + 1} xpub", + "Cosigner $i xPub", style: STextStyles.w500_14(context).copyWith( color: Theme.of(context) @@ -370,9 +320,9 @@ class _MultisigSetupViewState extends ConsumerState { children: [ Expanded( child: TextField( - controller: xpubControllers[i], + controller: xpubControllers[i - 1], decoration: InputDecoration( - hintText: "Enter xpub", + hintText: "Enter cosigner $i xPub", hintStyle: STextStyles.fieldLabel(context), ), @@ -419,7 +369,7 @@ class _MultisigSetupViewState extends ConsumerState { final data = await Clipboard.getData( 'text/plain'); if (data?.text != null) { - xpubControllers[i].text = + xpubControllers[i - 1].text = data!.text!; ref .read( @@ -459,4 +409,77 @@ class _MultisigSetupViewState extends ConsumerState { ), ); } + + String _getTargetPathForScriptType(MultisigScriptType scriptType) { + const pathMap = { + MultisigScriptType.segwit: "m/48'/0'/0'/1'", + MultisigScriptType.nativeSegwit: "m/48'/0'/0'/2'", + }; + return pathMap[scriptType] ?? ''; + } +} + +class MultisigCoordinatorData { + const MultisigCoordinatorData({ + this.threshold = 2, + this.totalCosigners = 3, + this.coinType = 0, // Bitcoin mainnet. + this.accountIndex = 0, + this.scriptType = MultisigScriptType.nativeSegwit, + this.cosignerXpubs = const [], + }); + + final int threshold; + final int totalCosigners; + final int coinType; + final int accountIndex; + final MultisigScriptType scriptType; + final List cosignerXpubs; + + MultisigCoordinatorData copyWith({ + int? threshold, + int? totalCosigners, + int? coinType, + int? accountIndex, + MultisigScriptType? scriptType, + List? cosignerXpubs, + }) { + return MultisigCoordinatorData( + threshold: threshold ?? this.threshold, + totalCosigners: totalCosigners ?? this.totalCosigners, + coinType: coinType ?? this.coinType, + accountIndex: accountIndex ?? this.accountIndex, + scriptType: scriptType ?? this.scriptType, + cosignerXpubs: cosignerXpubs ?? this.cosignerXpubs, + ); + } + + Map toJson() => { + 'threshold': threshold, + 'totalCosigners': totalCosigners, + 'coinType': coinType, + 'accountIndex': accountIndex, + 'scriptType': scriptType.index, + 'cosignerXpubs': cosignerXpubs, + }; + + factory MultisigCoordinatorData.fromJson(Map json) { + return MultisigCoordinatorData( + threshold: json['threshold'] as int, + totalCosigners: json['totalCosigners'] as int, + coinType: json['coinType'] as int, + accountIndex: json['accountIndex'] as int, + scriptType: MultisigScriptType.values[json['scriptType'] as int], + cosignerXpubs: (json['cosignerXpubs'] as List).cast(), + ); + } +} + +enum MultisigScriptType { + // legacy, + // P2SH. + // "the only script types covered by this BIP are Native Segwit (p2wsh) and + // Nested Segwit (p2sh-p2wsh)." (BIP48). + segwit, // P2SH-P2WSH. + nativeSegwit, // P2WSH. } diff --git a/lib/pages/wallet_view/multisig_coordinator_view/multisig_setup_view.dart b/lib/pages/wallet_view/multisig_coordinator_view/multisig_setup_view.dart index ece7cf249..2813272b3 100644 --- a/lib/pages/wallet_view/multisig_coordinator_view/multisig_setup_view.dart +++ b/lib/pages/wallet_view/multisig_coordinator_view/multisig_setup_view.dart @@ -78,12 +78,6 @@ class MultisigSetupData { } } -enum MultisigScriptType { - legacy, // P2SH. - segwit, // P2SH-P2WSH. - nativeSegwit, // P2WSH. -} - class MultisigSetupState extends StateNotifier { MultisigSetupState() : super(const MultisigSetupData()); @@ -112,11 +106,13 @@ class MultisigSetupView extends ConsumerStatefulWidget { const MultisigSetupView({ super.key, required this.walletId, + this.scriptType, this.totalCosigners, this.threshold, }); final String walletId; + final MultisigScriptType? scriptType; final int? totalCosigners; final int? threshold; @@ -340,7 +336,7 @@ class _MultisigSetupViewState extends ConsumerState { @override Widget build(BuildContext context) { final setupData = ref.watch(multisigSetupStateProvider); - final bool isDesktop = Util.isDesktop; + // final bool isDesktop = Util.isDesktop; return Background( child: SafeArea( @@ -425,9 +421,10 @@ class _MultisigSetupViewState extends ConsumerState { items: MultisigScriptType.values.map((type) { String label; switch (type) { - case MultisigScriptType.legacy: - label = "Legacy (P2SH)"; - break; + // case MultisigScriptType.legacy: + // label = "Legacy (P2SH)"; + // break; + // BIP48 does not cover legacy P2SH script types. case MultisigScriptType.segwit: label = "Nested SegWit (P2SH-P2WSH)"; break; @@ -531,6 +528,7 @@ class _MultisigSetupViewState extends ConsumerState { MaterialPageRoute( builder: (context) => MultisigCoordinatorView( walletId: widget.walletId, + scriptType: setupData.scriptType, // TODO, totalCosigners: int.parse(_participantsController.text), threshold: int.parse(_thresholdController.text), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index bb6bfa7f3..9de5a2991 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -2184,13 +2184,14 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case MultisigCoordinatorView.routeName: - if (args is Tuple3) { + if (args is Tuple4) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => MultisigCoordinatorView( walletId: args.item1, - totalCosigners: args.item2, - threshold: args.item3, + scriptType: args.item2, + totalCosigners: args.item3, + threshold: args.item4, ), settings: RouteSettings( name: settings.name, diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index 3d64fb45a..16aacf756 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -19,7 +19,9 @@ enum DerivePathType { eCash44, solana, bip86, - cardanoShelley; + cardanoShelley, + bip48p2shp2wsh, + bip48p2wsh; AddressType getAddressType() { switch (this) { @@ -45,6 +47,12 @@ enum DerivePathType { case DerivePathType.cardanoShelley: return AddressType.cardanoShelley; + + case DerivePathType.bip48p2shp2wsh: + return AddressType.p2sh; + + case DerivePathType.bip48p2wsh: + return AddressType.p2wpkh; } } } diff --git a/lib/wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart b/lib/wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart index affb6d7de..4e5c4e611 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart @@ -28,9 +28,18 @@ class XPriv extends XKey { mixin ExtendedKeysInterface on ElectrumXInterface { - Future<({List xpubs, String fingerprint})> getXPubs() async { + Future<({List xpubs, String fingerprint})> getXPubs( + {bool bip48 = false}) async { final paths = cryptoCurrency.supportedHardenedDerivationPaths; + if (bip48) { + // This hack was used because I was not able to correctly cast an existing + // Bitcoin wallet as a BIP48Bitcoin wallet in order to use its + // supportedDerivationPathTypes override. + paths.add("m/48'/0'/0'/1'"); + paths.add("m/48'/0'/0'/2'"); + } + final master = await getRootHDNode(); final fingerprint = master.fingerprint.toRadixString(16);