mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-06 18:59:24 +00:00
feat: add account index selection
This commit is contained in:
parent
94d7f566b7
commit
1e5236a8a4
4 changed files with 131 additions and 53 deletions
|
@ -28,8 +28,8 @@ class MultisigCoordinatorState extends StateNotifier<MultisigCoordinatorData> {
|
|||
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<MultisigCoordinatorData> {
|
|||
}
|
||||
|
||||
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<MultisigCoordinatorView> {
|
|||
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<MultisigCoordinatorView> {
|
|||
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<MultisigCoordinatorView> {
|
|||
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<MultisigCoordinatorView> {
|
|||
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<MultisigCoordinatorView> {
|
|||
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<String> cosignerXpubs;
|
||||
|
||||
MultisigCoordinatorData copyWith({
|
||||
int? threshold,
|
||||
int? totalCosigners,
|
||||
int? participants,
|
||||
int? coinType,
|
||||
int? accountIndex,
|
||||
int? account,
|
||||
MultisigScriptType? scriptType,
|
||||
List<String>? 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<String, dynamic> 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<String, dynamic> 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<String>(),
|
||||
);
|
||||
|
|
|
@ -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<String> cosignerXpubs;
|
||||
|
||||
MultisigSetupData copyWith({
|
||||
int? threshold,
|
||||
int? totalCosigners,
|
||||
int? participants,
|
||||
int? coinType,
|
||||
int? accountIndex,
|
||||
int? account,
|
||||
MultisigScriptType? scriptType,
|
||||
List<String>? 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<String, dynamic> 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<String, dynamic> 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<String>(),
|
||||
);
|
||||
|
@ -85,8 +85,8 @@ class MultisigSetupState extends StateNotifier<MultisigSetupData> {
|
|||
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<MultisigSetupData> {
|
|||
}
|
||||
|
||||
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<MultisigSetupView> {
|
|||
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<MultisigSetupView> {
|
|||
|
||||
final _thresholdController = TextEditingController();
|
||||
final _participantsController = TextEditingController();
|
||||
final _accountController = TextEditingController();
|
||||
|
||||
final List<TextEditingController> controllers = [];
|
||||
|
||||
|
@ -254,6 +257,10 @@ class _MultisigSetupViewState extends ConsumerState<MultisigSetupView> {
|
|||
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<MultisigSetupView> {
|
|||
);
|
||||
}
|
||||
|
||||
void _showWhatIsAccountDialog() {
|
||||
showDialog<void>(
|
||||
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<MultisigSetupView> {
|
|||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Threshold and Participants.
|
||||
// Threshold, Participants, and Account.
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -500,6 +538,34 @@ class _MultisigSetupViewState extends ConsumerState<MultisigSetupView> {
|
|||
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<StackColors>()!
|
||||
.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<MultisigSetupView> {
|
|||
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),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -2158,13 +2158,15 @@ class RouteGenerator {
|
|||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||
|
||||
case MultisigSetupView.routeName:
|
||||
if (args is Tuple3<String, int?, int?>) {
|
||||
if (args is Tuple5<String, MultisigScriptType?, int?, int?, int?>) {
|
||||
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<String, MultisigScriptType, int, int>) {
|
||||
if (args is Tuple5<String, MultisigScriptType, int, int, int>) {
|
||||
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,
|
||||
|
|
|
@ -29,15 +29,15 @@ class XPriv extends XKey {
|
|||
mixin ExtendedKeysInterface<T extends ElectrumXCurrencyInterface>
|
||||
on ElectrumXInterface<T> {
|
||||
Future<({List<XPub> 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();
|
||||
|
|
Loading…
Reference in a new issue