From 1e5236a8a47942583fddb2e2f2a98f4f9de951c2 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sun, 29 Dec 2024 22:50:56 -0600 Subject: [PATCH] feat: add account index selection --- .../multisig_coordinator_view.dart | 51 ++++---- .../multisig_setup_view.dart | 114 ++++++++++++++---- lib/route_generator.dart | 13 +- .../extended_keys_interface.dart | 6 +- 4 files changed, 131 insertions(+), 53 deletions(-) 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 2e92a3ff2..9ba77494d 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 @@ -28,8 +28,8 @@ class MultisigCoordinatorState extends StateNotifier { state = state.copyWith(threshold: threshold); } - void updateTotalCosigners(int total) { - state = state.copyWith(totalCosigners: total); + void updateParticipants(int total) { + state = state.copyWith(participants: total); } void updateScriptType(MultisigScriptType type) { @@ -37,7 +37,7 @@ class MultisigCoordinatorState extends StateNotifier { } void addCosignerXpub(String xpub) { - if (state.cosignerXpubs.length < state.totalCosigners) { + if (state.cosignerXpubs.length < state.participants) { state = state.copyWith( cosignerXpubs: [...state.cosignerXpubs, xpub], ); @@ -50,14 +50,16 @@ class MultisigCoordinatorView extends ConsumerStatefulWidget { super.key, required this.walletId, required this.scriptType, - required this.totalCosigners, + required this.participants, required this.threshold, + required this.account, }); final String walletId; final MultisigScriptType scriptType; - final int totalCosigners; + final int participants; final int threshold; + final int account; static const String routeName = "/multisigCoordinator"; @@ -77,7 +79,7 @@ class _MultisigSetupViewState extends ConsumerState { super.initState(); // Initialize controllers. - for (int i = 0; i < widget.totalCosigners - 1; i++) { + for (int i = 0; i < widget.participants - 1; i++) { xpubControllers.add(TextEditingController()); } @@ -86,7 +88,7 @@ class _MultisigSetupViewState extends ConsumerState { final targetPath = _getTargetPathForScriptType(widget.scriptType); final xpubData = await (ref.read(pWallets).getWallet(widget.walletId) as ExtendedKeysInterface) - .getXPubs(bip48: true); + .getXPubs(bip48: true, account: widget.account); print(xpubData); final matchingPub = xpubData.xpubs.firstWhere( @@ -300,7 +302,7 @@ class _MultisigSetupViewState extends ConsumerState { const SizedBox(height: 24), // Generate input fields for each cosigner. - for (int i = 1; i < widget.totalCosigners; i++) + for (int i = 1; i < widget.participants; i++) Padding( padding: const EdgeInsets.only(bottom: 16), child: Column( @@ -394,7 +396,14 @@ class _MultisigSetupViewState extends ConsumerState { enabled: xpubControllers.every( (controller) => controller.text.isNotEmpty), onPressed: () { - // TODO. + // final privWallet = Bip48Wallet( + // masterKey: masterKey, + // coinType: 0, + // account: 0, + // scriptType: Bip48ScriptType.p2shMultisig, + // threshold: 2, + // totalKeys: 3, + // ); }, ), ], @@ -422,33 +431,33 @@ class _MultisigSetupViewState extends ConsumerState { class MultisigCoordinatorData { const MultisigCoordinatorData({ this.threshold = 2, - this.totalCosigners = 3, + this.participants = 3, this.coinType = 0, // Bitcoin mainnet. - this.accountIndex = 0, + this.account = 0, this.scriptType = MultisigScriptType.nativeSegwit, this.cosignerXpubs = const [], }); final int threshold; - final int totalCosigners; + final int participants; final int coinType; - final int accountIndex; + final int account; final MultisigScriptType scriptType; final List cosignerXpubs; MultisigCoordinatorData copyWith({ int? threshold, - int? totalCosigners, + int? participants, int? coinType, - int? accountIndex, + int? account, MultisigScriptType? scriptType, List? cosignerXpubs, }) { return MultisigCoordinatorData( threshold: threshold ?? this.threshold, - totalCosigners: totalCosigners ?? this.totalCosigners, + participants: participants ?? this.participants, coinType: coinType ?? this.coinType, - accountIndex: accountIndex ?? this.accountIndex, + account: account ?? this.account, scriptType: scriptType ?? this.scriptType, cosignerXpubs: cosignerXpubs ?? this.cosignerXpubs, ); @@ -456,9 +465,9 @@ class MultisigCoordinatorData { Map toJson() => { 'threshold': threshold, - 'totalCosigners': totalCosigners, + 'participants': participants, 'coinType': coinType, - 'accountIndex': accountIndex, + 'accountIndex': account, 'scriptType': scriptType.index, 'cosignerXpubs': cosignerXpubs, }; @@ -466,9 +475,9 @@ class MultisigCoordinatorData { factory MultisigCoordinatorData.fromJson(Map json) { return MultisigCoordinatorData( threshold: json['threshold'] as int, - totalCosigners: json['totalCosigners'] as int, + participants: json['participants'] as int, coinType: json['coinType'] as int, - accountIndex: json['accountIndex'] as int, + account: json['accountIndex'] as int, scriptType: MultisigScriptType.values[json['scriptType'] as int], cosignerXpubs: (json['cosignerXpubs'] as List).cast(), ); 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 2813272b3..4ba846d59 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 @@ -25,33 +25,33 @@ final multisigSetupStateProvider = class MultisigSetupData { const MultisigSetupData({ this.threshold = 2, - this.totalCosigners = 3, + this.participants = 3, this.coinType = 0, // Bitcoin mainnet. - this.accountIndex = 0, + this.account = 0, this.scriptType = MultisigScriptType.nativeSegwit, this.cosignerXpubs = const [], }); final int threshold; - final int totalCosigners; + final int participants; final int coinType; - final int accountIndex; + final int account; final MultisigScriptType scriptType; final List cosignerXpubs; MultisigSetupData copyWith({ int? threshold, - int? totalCosigners, + int? participants, int? coinType, - int? accountIndex, + int? account, MultisigScriptType? scriptType, List? cosignerXpubs, }) { return MultisigSetupData( threshold: threshold ?? this.threshold, - totalCosigners: totalCosigners ?? this.totalCosigners, + participants: participants ?? this.participants, coinType: coinType ?? this.coinType, - accountIndex: accountIndex ?? this.accountIndex, + account: account ?? this.account, scriptType: scriptType ?? this.scriptType, cosignerXpubs: cosignerXpubs ?? this.cosignerXpubs, ); @@ -59,9 +59,9 @@ class MultisigSetupData { Map toJson() => { 'threshold': threshold, - 'totalCosigners': totalCosigners, + 'participants': participants, 'coinType': coinType, - 'accountIndex': accountIndex, + 'account': account, 'scriptType': scriptType.index, 'cosignerXpubs': cosignerXpubs, }; @@ -69,9 +69,9 @@ class MultisigSetupData { factory MultisigSetupData.fromJson(Map json) { return MultisigSetupData( threshold: json['threshold'] as int, - totalCosigners: json['totalCosigners'] as int, + participants: json['participants'] as int, coinType: json['coinType'] as int, - accountIndex: json['accountIndex'] as int, + account: json['account'] as int, scriptType: MultisigScriptType.values[json['scriptType'] as int], cosignerXpubs: (json['cosignerXpubs'] as List).cast(), ); @@ -85,8 +85,8 @@ class MultisigSetupState extends StateNotifier { state = state.copyWith(threshold: threshold); } - void updateTotalCosigners(int total) { - state = state.copyWith(totalCosigners: total); + void updateParticipants(int total) { + state = state.copyWith(participants: total); } void updateScriptType(MultisigScriptType type) { @@ -94,7 +94,7 @@ class MultisigSetupState extends StateNotifier { } void addCosignerXpub(String xpub) { - if (state.cosignerXpubs.length < state.totalCosigners) { + if (state.cosignerXpubs.length < state.participants) { state = state.copyWith( cosignerXpubs: [...state.cosignerXpubs, xpub], ); @@ -107,14 +107,16 @@ class MultisigSetupView extends ConsumerStatefulWidget { super.key, required this.walletId, this.scriptType, - this.totalCosigners, + this.participants, this.threshold, + this.account, }); final String walletId; final MultisigScriptType? scriptType; - final int? totalCosigners; + final int? participants; final int? threshold; + final int? account; static const String routeName = "/multisigSetup"; @@ -128,11 +130,11 @@ class _MultisigSetupViewState extends ConsumerState { super.initState(); // Initialize participants count if provided. - if (widget.totalCosigners != null) { - _participantsCount = widget.totalCosigners!; - _participantsController.text = widget.totalCosigners!.toString(); + if (widget.participants != null) { + _participantsCount = widget.participants!; + _participantsController.text = widget.participants!.toString(); // Initialize the controllers list. - for (int i = 0; i < widget.totalCosigners!; i++) { + for (int i = 0; i < widget.participants!; i++) { controllers.add(TextEditingController()); } } @@ -226,6 +228,7 @@ class _MultisigSetupViewState extends ConsumerState { final _thresholdController = TextEditingController(); final _participantsController = TextEditingController(); + final _accountController = TextEditingController(); final List controllers = []; @@ -254,6 +257,10 @@ class _MultisigSetupViewState extends ConsumerState { return "Participants count error"; } + if (_accountController.text.isEmpty) { + return "Choose an account (0 is OK)"; + } + return "valid"; } @@ -323,10 +330,41 @@ class _MultisigSetupViewState extends ConsumerState { ); } + void _showWhatIsAccountDialog() { + showDialog( + context: context, + builder: (_) => SimpleMobileDialog( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "What is an account?", + style: STextStyles.w600_20(context), + ), + const SizedBox( + height: 12, + ), + Text( + "The account number you choose will determine which extended " + "public key (xPub) you share with all participants. If you use " + "the same xPub across multiple multisignature accounts, a shared " + "cosigner in any of them will be able to recognize your " + "participation in them and so your privacy could be degraded. " + "For maximum privacy, use a distinct account number for each " + "multisignature account.", + style: STextStyles.w400_16(context), + ), + ], + ), + ), + ); + } + @override void dispose() { _thresholdController.dispose(); _participantsController.dispose(); + _accountController.dispose(); for (final controller in controllers) { controller.dispose(); } @@ -449,7 +487,7 @@ class _MultisigSetupViewState extends ConsumerState { ), const SizedBox(height: 16), - // Threshold and Participants. + // Threshold, Participants, and Account. Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -500,6 +538,34 @@ class _MultisigSetupViewState extends ConsumerState { hintStyle: STextStyles.fieldLabel(context), ), ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Account", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + ), + CustomTextButton( + text: "What is an account?", + onTap: _showWhatIsAccountDialog, + ), + ], + ), + const SizedBox(height: 10), + TextField( + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + controller: _accountController, + decoration: InputDecoration( + hintText: "Enter account number", + hintStyle: STextStyles.fieldLabel(context), + ), + ), ], ), const SizedBox(height: 24), @@ -529,9 +595,9 @@ class _MultisigSetupViewState extends ConsumerState { builder: (context) => MultisigCoordinatorView( walletId: widget.walletId, scriptType: setupData.scriptType, // TODO, - totalCosigners: - int.parse(_participantsController.text), + participants: int.parse(_participantsController.text), threshold: int.parse(_thresholdController.text), + account: int.parse(_accountController.text), ), ), ); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 9de5a2991..1a353374f 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -2158,13 +2158,15 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case MultisigSetupView.routeName: - if (args is Tuple3) { + if (args is Tuple5) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => MultisigSetupView( walletId: args.item1, - totalCosigners: args.item2, - threshold: args.item3, + scriptType: args.item2, + participants: args.item3, + threshold: args.item4, + account: args.item5, ), settings: RouteSettings( name: settings.name, @@ -2184,14 +2186,15 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case MultisigCoordinatorView.routeName: - if (args is Tuple4) { + if (args is Tuple5) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => MultisigCoordinatorView( walletId: args.item1, scriptType: args.item2, - totalCosigners: args.item3, + participants: args.item3, threshold: args.item4, + account: args.item5, ), settings: RouteSettings( name: settings.name, 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 4e5c4e611..bcc52a161 100644 --- a/lib/wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart +++ b/lib/wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart @@ -29,15 +29,15 @@ class XPriv extends XKey { mixin ExtendedKeysInterface on ElectrumXInterface { Future<({List xpubs, String fingerprint})> getXPubs( - {bool bip48 = false}) async { + {bool bip48 = false, int account = 0}) 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'"); + paths.add("m/48'/0'/$account'/1'"); + paths.add("m/48'/0'/$account'/2'"); } final master = await getRootHDNode();