mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-06 18:59:24 +00:00
feat: show xPub corresponding to selected multisig script type
using a hack to add BIP48 derivation paths to Bitcoin wallets as needed
This commit is contained in:
parent
b8105215e2
commit
c16a6ecb67
5 changed files with 143 additions and 104 deletions
|
@ -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<String> cosignerXpubs;
|
||||
|
||||
MultisigCoordinatorData copyWith({
|
||||
int? threshold,
|
||||
int? totalCosigners,
|
||||
int? coinType,
|
||||
int? accountIndex,
|
||||
MultisigScriptType? scriptType,
|
||||
List<String>? 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<String, dynamic> toJson() => {
|
||||
'threshold': threshold,
|
||||
'totalCosigners': totalCosigners,
|
||||
'coinType': coinType,
|
||||
'accountIndex': accountIndex,
|
||||
'scriptType': scriptType.index,
|
||||
'cosignerXpubs': cosignerXpubs,
|
||||
};
|
||||
|
||||
factory MultisigCoordinatorData.fromJson(Map<String, dynamic> 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<String>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum MultisigScriptType {
|
||||
legacy, // P2SH.
|
||||
segwit, // P2SH-P2WSH.
|
||||
nativeSegwit, // P2WSH.
|
||||
}
|
||||
|
||||
class MultisigCoordinatorState extends StateNotifier<MultisigCoordinatorData> {
|
||||
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<MultisigCoordinatorView> {
|
||||
final List<TextEditingController> xpubControllers = [];
|
||||
String _myXpub = "";
|
||||
// bool _isNfcAvailable = false;
|
||||
// String _nfcStatus = 'Checking NFC availability...';
|
||||
|
||||
|
@ -134,10 +77,28 @@ class _MultisigSetupViewState extends ConsumerState<MultisigCoordinatorView> {
|
|||
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<MultisigCoordinatorView> {
|
|||
},
|
||||
),
|
||||
title: Text(
|
||||
"Enter cosigner xpubs",
|
||||
"Enter cosigner xPubs",
|
||||
style: STextStyles.navBarTitle(context),
|
||||
),
|
||||
titleSpacing: 0,
|
||||
|
@ -256,7 +217,7 @@ class _MultisigSetupViewState extends ConsumerState<MultisigCoordinatorView> {
|
|||
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<MultisigCoordinatorView> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Your xpub",
|
||||
"Your xPub",
|
||||
style: STextStyles.w500_14(context).copyWith(
|
||||
color: Theme.of(context)
|
||||
.extension<StackColors>()!
|
||||
|
@ -280,15 +241,14 @@ class _MultisigSetupViewState extends ConsumerState<MultisigCoordinatorView> {
|
|||
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<StackColors>()!
|
||||
.textFieldDefaultBG,
|
||||
|
@ -322,18 +282,8 @@ class _MultisigSetupViewState extends ConsumerState<MultisigCoordinatorView> {
|
|||
.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<MultisigCoordinatorView> {
|
|||
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<MultisigCoordinatorView> {
|
|||
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<MultisigCoordinatorView> {
|
|||
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<MultisigCoordinatorView> {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
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<String> cosignerXpubs;
|
||||
|
||||
MultisigCoordinatorData copyWith({
|
||||
int? threshold,
|
||||
int? totalCosigners,
|
||||
int? coinType,
|
||||
int? accountIndex,
|
||||
MultisigScriptType? scriptType,
|
||||
List<String>? 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<String, dynamic> toJson() => {
|
||||
'threshold': threshold,
|
||||
'totalCosigners': totalCosigners,
|
||||
'coinType': coinType,
|
||||
'accountIndex': accountIndex,
|
||||
'scriptType': scriptType.index,
|
||||
'cosignerXpubs': cosignerXpubs,
|
||||
};
|
||||
|
||||
factory MultisigCoordinatorData.fromJson(Map<String, dynamic> 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<String>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
|
|
|
@ -78,12 +78,6 @@ class MultisigSetupData {
|
|||
}
|
||||
}
|
||||
|
||||
enum MultisigScriptType {
|
||||
legacy, // P2SH.
|
||||
segwit, // P2SH-P2WSH.
|
||||
nativeSegwit, // P2WSH.
|
||||
}
|
||||
|
||||
class MultisigSetupState extends StateNotifier<MultisigSetupData> {
|
||||
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<MultisigSetupView> {
|
|||
@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<MultisigSetupView> {
|
|||
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<MultisigSetupView> {
|
|||
MaterialPageRoute<void>(
|
||||
builder: (context) => MultisigCoordinatorView(
|
||||
walletId: widget.walletId,
|
||||
scriptType: setupData.scriptType, // TODO,
|
||||
totalCosigners:
|
||||
int.parse(_participantsController.text),
|
||||
threshold: int.parse(_thresholdController.text),
|
||||
|
|
|
@ -2184,13 +2184,14 @@ class RouteGenerator {
|
|||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case MultisigCoordinatorView.routeName:
|
||||
if (args is Tuple3<String, int, int>) {
|
||||
if (args is Tuple4<String, MultisigScriptType, int, int>) {
|
||||
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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,9 +28,18 @@ class XPriv extends XKey {
|
|||
|
||||
mixin ExtendedKeysInterface<T extends ElectrumXCurrencyInterface>
|
||||
on ElectrumXInterface<T> {
|
||||
Future<({List<XPub> xpubs, String fingerprint})> getXPubs() async {
|
||||
Future<({List<XPub> 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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue