mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-08 03:39:23 +00:00
feat: (WIP) add nfc_manager plugin and non-working UI elements
added Android permissions, but TODO add iOS entitlements etc. ... see https://github.com/okadan/flutter-nfc-manager?tab=readme-ov-file#setup
This commit is contained in:
parent
4fc2a7acfa
commit
7b629e81b4
8 changed files with 791 additions and 0 deletions
|
@ -0,0 +1,216 @@
|
||||||
|
import 'package:bip48/bip48.dart';
|
||||||
|
import 'package:coinlib/coinlib.dart';
|
||||||
|
|
||||||
|
/// Represents the parameters needed to create a shared multisig account.
|
||||||
|
class MultisigParams {
|
||||||
|
/// Number of required signatures (M in M-of-N).
|
||||||
|
final int threshold;
|
||||||
|
|
||||||
|
/// Total number of participants (N in M-of-N).
|
||||||
|
final int totalCosigners;
|
||||||
|
|
||||||
|
/// BIP44 coin type (e.g., 0 for Bitcoin mainnet).
|
||||||
|
final int coinType;
|
||||||
|
|
||||||
|
/// BIP44/48 account index.
|
||||||
|
final int account;
|
||||||
|
|
||||||
|
/// BIP48 script type (e.g., p2sh, p2wsh).
|
||||||
|
final Bip48ScriptType scriptType;
|
||||||
|
|
||||||
|
/// Creates a new set of multisig parameters.
|
||||||
|
const MultisigParams({
|
||||||
|
required this.threshold,
|
||||||
|
required this.totalCosigners,
|
||||||
|
required this.coinType,
|
||||||
|
required this.account,
|
||||||
|
required this.scriptType,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Validates the parameters for consistency.
|
||||||
|
///
|
||||||
|
/// Returns true if all parameters are valid:
|
||||||
|
/// - threshold > 0
|
||||||
|
/// - threshold <= totalCosigners
|
||||||
|
/// - account >= 0
|
||||||
|
/// - coinType >= 0
|
||||||
|
bool isValid() {
|
||||||
|
return threshold > 0 &&
|
||||||
|
threshold <= totalCosigners &&
|
||||||
|
account >= 0 &&
|
||||||
|
coinType >= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a participant in the multisig setup process.
|
||||||
|
class CosignerInfo {
|
||||||
|
/// The cosigner's BIP48 account-level extended public key.
|
||||||
|
final String accountXpub;
|
||||||
|
|
||||||
|
/// Position in the sorted set of cosigners (0-based).
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
/// Creates info about a cosigner participant.
|
||||||
|
const CosignerInfo({
|
||||||
|
required this.accountXpub,
|
||||||
|
required this.index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coordinates the creation of a shared multisig account between multiple users.
|
||||||
|
class MultisigCoordinator {
|
||||||
|
/// Local master key if available (otherwise uses accountXpub).
|
||||||
|
final HDPrivateKey? localMasterKey;
|
||||||
|
|
||||||
|
/// Parameters for the shared multisig wallet.
|
||||||
|
final MultisigParams params;
|
||||||
|
|
||||||
|
/// Collected cosigner information.
|
||||||
|
final List<CosignerInfo> _cosigners = [];
|
||||||
|
|
||||||
|
/// Local account xpub when not using master key.
|
||||||
|
String? _accountXpub;
|
||||||
|
|
||||||
|
/// Creates a coordinator using the local HD master private key.
|
||||||
|
///
|
||||||
|
/// Uses the provided [localMasterKey] to derive the account xpub that will
|
||||||
|
/// be shared with other cosigners.
|
||||||
|
MultisigCoordinator({
|
||||||
|
required this.localMasterKey,
|
||||||
|
required this.params,
|
||||||
|
}) {
|
||||||
|
if (!params.isValid()) {
|
||||||
|
throw ArgumentError('Invalid multisig parameters');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a coordinator using a pre-derived account xpub.
|
||||||
|
///
|
||||||
|
/// This constructor should be used when you only want to verify addresses
|
||||||
|
/// or don't have access to the master private key.
|
||||||
|
MultisigCoordinator.fromXpub({
|
||||||
|
required String accountXpub,
|
||||||
|
required this.params,
|
||||||
|
}) : localMasterKey = null {
|
||||||
|
if (!params.isValid()) {
|
||||||
|
throw ArgumentError('Invalid multisig parameters');
|
||||||
|
}
|
||||||
|
_accountXpub = accountXpub;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets this user's account xpub that needs to be shared with other cosigners.
|
||||||
|
///
|
||||||
|
/// If created with a master key, derives the account xpub at the BIP48 path.
|
||||||
|
/// If created with fromXpub, returns the provided account xpub.
|
||||||
|
String getLocalAccountXpub() {
|
||||||
|
if (_accountXpub != null) {
|
||||||
|
return _accountXpub!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localMasterKey == null) {
|
||||||
|
throw StateError('No master key or account xpub available');
|
||||||
|
}
|
||||||
|
|
||||||
|
final path = bip48DerivationPath(
|
||||||
|
coinType: params.coinType,
|
||||||
|
account: params.account,
|
||||||
|
scriptType: params.scriptType,
|
||||||
|
);
|
||||||
|
final accountKey = localMasterKey!.derivePath(path);
|
||||||
|
return accountKey.hdPublicKey.encode(bitcoinNetwork.mainnet.pubHDPrefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a cosigner's account xpub to the set.
|
||||||
|
///
|
||||||
|
/// Throws [StateError] if all cosigners have already been added.
|
||||||
|
void addCosigner(String accountXpub) {
|
||||||
|
if (_cosigners.length >= params.totalCosigners - 1) {
|
||||||
|
throw StateError('All cosigners have been added');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign index based on current position
|
||||||
|
_cosigners.add(CosignerInfo(
|
||||||
|
accountXpub: accountXpub,
|
||||||
|
index: _cosigners.length + 1, // Local user is always index 0.
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if all required cosigner information has been collected.
|
||||||
|
bool isComplete() {
|
||||||
|
return _cosigners.length == params.totalCosigners - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates the local wallet instance once all cosigners are added.
|
||||||
|
///
|
||||||
|
/// Throws [StateError] if not all cosigners have been added yet.
|
||||||
|
Bip48Wallet createWallet() {
|
||||||
|
if (!isComplete()) {
|
||||||
|
throw StateError('Not all cosigners have been added');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create wallet with either our master key or xpub
|
||||||
|
final wallet = localMasterKey != null
|
||||||
|
? Bip48Wallet(
|
||||||
|
masterKey: localMasterKey,
|
||||||
|
coinType: params.coinType,
|
||||||
|
account: params.account,
|
||||||
|
scriptType: params.scriptType,
|
||||||
|
threshold: params.threshold,
|
||||||
|
totalKeys: params.totalCosigners,
|
||||||
|
)
|
||||||
|
: Bip48Wallet(
|
||||||
|
accountXpub: _accountXpub,
|
||||||
|
coinType: params.coinType,
|
||||||
|
account: params.account,
|
||||||
|
scriptType: params.scriptType,
|
||||||
|
threshold: params.threshold,
|
||||||
|
totalKeys: params.totalCosigners,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add all cosigner xpubs.
|
||||||
|
for (final cosigner in _cosigners) {
|
||||||
|
wallet.addCosignerXpub(cosigner.accountXpub);
|
||||||
|
}
|
||||||
|
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifies that derived addresses match between all participants.
|
||||||
|
///
|
||||||
|
/// Takes a list of [sharedAddresses] that other participants derived, along
|
||||||
|
/// with the [indices] used to derive them and whether they are [isChange]
|
||||||
|
/// addresses.
|
||||||
|
///
|
||||||
|
/// Returns true if all provided addresses match our local derivation.
|
||||||
|
bool verifyAddresses(List<String> sharedAddresses,
|
||||||
|
{required List<int> indices, required bool isChange}) {
|
||||||
|
if (!isComplete()) return false;
|
||||||
|
|
||||||
|
final wallet = createWallet();
|
||||||
|
for (final idx in indices) {
|
||||||
|
final derivedAddress =
|
||||||
|
wallet.deriveMultisigAddress(idx, isChange: isChange);
|
||||||
|
final sharedAddress = sharedAddresses[indices.indexOf(idx)];
|
||||||
|
if (derivedAddress != sharedAddress) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a list of derived addresses for verification.
|
||||||
|
///
|
||||||
|
/// Derives addresses at the specified [indices] on either the external
|
||||||
|
/// or change chain based on [isChange].
|
||||||
|
///
|
||||||
|
/// Throws [StateError] if not all cosigners have been added yet.
|
||||||
|
List<String> getVerificationAddresses(
|
||||||
|
{required List<int> indices, required bool isChange}) {
|
||||||
|
if (!isComplete()) {
|
||||||
|
throw StateError('Not all cosigners have been added');
|
||||||
|
}
|
||||||
|
|
||||||
|
final wallet = createWallet();
|
||||||
|
return indices
|
||||||
|
.map((idx) => wallet.deriveMultisigAddress(idx, isChange: isChange))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,494 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data' show Uint8List;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
import 'package:nfc_manager/nfc_manager.dart';
|
||||||
|
|
||||||
|
import '../../../../themes/stack_colors.dart';
|
||||||
|
import '../../../../utilities/assets.dart';
|
||||||
|
import '../../../../utilities/text_styles.dart';
|
||||||
|
import '../../../../utilities/util.dart';
|
||||||
|
import '../../../../widgets/background.dart';
|
||||||
|
import '../../../../widgets/custom_buttons/app_bar_icondart';
|
||||||
|
import '../../../widgets/stack_dialog.dart';
|
||||||
|
|
||||||
|
final multisigSetupStateProvider =
|
||||||
|
StateNotifierProvider<MultisigSetupState, MultisigSetupData>((ref) {
|
||||||
|
return MultisigSetupState();
|
||||||
|
});
|
||||||
|
|
||||||
|
class MultisigSetupData {
|
||||||
|
const MultisigSetupData({
|
||||||
|
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;
|
||||||
|
|
||||||
|
MultisigSetupData copyWith({
|
||||||
|
int? threshold,
|
||||||
|
int? totalCosigners,
|
||||||
|
int? coinType,
|
||||||
|
int? accountIndex,
|
||||||
|
MultisigScriptType? scriptType,
|
||||||
|
List<String>? cosignerXpubs,
|
||||||
|
}) {
|
||||||
|
return MultisigSetupData(
|
||||||
|
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 MultisigSetupData.fromJson(Map<String, dynamic> json) {
|
||||||
|
return MultisigSetupData(
|
||||||
|
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 MultisigSetupState extends StateNotifier<MultisigSetupData> {
|
||||||
|
MultisigSetupState() : super(const MultisigSetupData());
|
||||||
|
|
||||||
|
void updateThreshold(int threshold) {
|
||||||
|
state = state.copyWith(threshold: threshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTotalCosigners(int total) {
|
||||||
|
state = state.copyWith(totalCosigners: total);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateScriptType(MultisigScriptType type) {
|
||||||
|
state = state.copyWith(scriptType: type);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addCosignerXpub(String xpub) {
|
||||||
|
if (state.cosignerXpubs.length < state.totalCosigners) {
|
||||||
|
state = state.copyWith(
|
||||||
|
cosignerXpubs: [...state.cosignerXpubs, xpub],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MultisigSetupView extends ConsumerStatefulWidget {
|
||||||
|
const MultisigSetupView({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const String routeName = "/multisigSetup";
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<MultisigSetupView> createState() => _MultisigSetupViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MultisigSetupViewState extends ConsumerState<MultisigSetupView> {
|
||||||
|
bool _isNfcAvailable = false;
|
||||||
|
String _nfcStatus = 'Checking NFC availability...';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_checkNfcAvailability();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkNfcAvailability() async {
|
||||||
|
try {
|
||||||
|
final availability = await NfcManager.instance.isAvailable();
|
||||||
|
setState(() {
|
||||||
|
_isNfcAvailable = availability;
|
||||||
|
_nfcStatus = _isNfcAvailable
|
||||||
|
? 'NFC is available'
|
||||||
|
: 'NFC is not available on this device';
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_nfcStatus = 'Error checking NFC: $e';
|
||||||
|
_isNfcAvailable = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startNfcSession() async {
|
||||||
|
if (!_isNfcAvailable) return;
|
||||||
|
|
||||||
|
setState(() => _nfcStatus = 'Ready to exchange information...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await NfcManager.instance.startSession(
|
||||||
|
onDiscovered: (tag) async {
|
||||||
|
try {
|
||||||
|
final ndef = Ndef.from(tag);
|
||||||
|
|
||||||
|
if (ndef == null) {
|
||||||
|
setState(() => _nfcStatus = 'Tag is not NDEF compatible');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final setupData = ref.watch(multisigSetupStateProvider);
|
||||||
|
|
||||||
|
if (ndef.isWritable) {
|
||||||
|
final message = NdefMessage([
|
||||||
|
NdefRecord.createMime(
|
||||||
|
'application/x-multisig-setup',
|
||||||
|
Uint8List.fromList(
|
||||||
|
utf8.encode(jsonEncode(setupData.toJson()))),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ndef.write(message);
|
||||||
|
setState(
|
||||||
|
() => _nfcStatus = 'Configuration shared successfully');
|
||||||
|
} catch (e) {
|
||||||
|
setState(
|
||||||
|
() => _nfcStatus = 'Failed to share configuration: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await NfcManager.instance.stopSession();
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _nfcStatus = 'Error during NFC exchange: $e');
|
||||||
|
await NfcManager.instance.stopSession();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
setState(() => _nfcStatus = 'Error: $e');
|
||||||
|
await NfcManager.instance.stopSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays a short explanation dialog about musig.
|
||||||
|
Future<void> _showMultisigInfoDialog() async {
|
||||||
|
await showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return const StackOkDialog(
|
||||||
|
title: "What is a multisignature account?",
|
||||||
|
message:
|
||||||
|
"Multisignature accounts, also called shared accounts, require "
|
||||||
|
"multiple signatures to authorize a transaction. This can "
|
||||||
|
"increase security by preventing a single point of failure or "
|
||||||
|
"allow multiple parties to jointly control funds."
|
||||||
|
"For example, in a 2-of-3 multisig account, two of the three "
|
||||||
|
"cosigners are required in order to sign a transaction and spend "
|
||||||
|
"funds.",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final bool isDesktop = Util.isDesktop;
|
||||||
|
final setupData = ref.watch(multisigSetupStateProvider);
|
||||||
|
|
||||||
|
// Required signatures<= total cosigners.
|
||||||
|
final clampedThreshold = (setupData.threshold > setupData.totalCosigners)
|
||||||
|
? setupData.totalCosigners
|
||||||
|
: setupData.threshold;
|
||||||
|
|
||||||
|
return Background(
|
||||||
|
child: SafeArea(
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor:
|
||||||
|
Theme.of(context).extension<StackColors>()!.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
leading: AppBarBackButton(
|
||||||
|
onPressed: () async {
|
||||||
|
if (FocusScope.of(context).hasFocus) {
|
||||||
|
FocusScope.of(context).unfocus();
|
||||||
|
await Future<void>.delayed(const Duration(milliseconds: 75));
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
"Multisignature account setup",
|
||||||
|
style: STextStyles.navBarTitle(context),
|
||||||
|
),
|
||||||
|
titleSpacing: 0,
|
||||||
|
actions: [
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 1,
|
||||||
|
child: AppBarIconButton(
|
||||||
|
size: 36,
|
||||||
|
icon: SvgPicture.asset(
|
||||||
|
Assets.svg.circleQuestion,
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.topNavIconPrimary,
|
||||||
|
),
|
||||||
|
onPressed: _showMultisigInfoDialog,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: LayoutBuilder(
|
||||||
|
builder: (builderContext, constraints) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
minHeight: constraints.maxHeight,
|
||||||
|
),
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// We'll add a method to share the config w/ cosigners
|
||||||
|
// so there's not much need to remind them here.
|
||||||
|
// RoundedWhiteContainer(
|
||||||
|
// child: Text(
|
||||||
|
// "Make sure all cosigners use the same "
|
||||||
|
// "configuration when creating the shared account.",
|
||||||
|
// style: STextStyles.w500_12(context).copyWith(
|
||||||
|
// color: Theme.of(context)
|
||||||
|
// .extension<StackColors>()!
|
||||||
|
// .textSubtitle1,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 16),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
"Configuration",
|
||||||
|
style: STextStyles.itemSubtitle(context),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Script Type Selection
|
||||||
|
RoundedContainer(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.popupBG,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Script Type",
|
||||||
|
style: STextStyles.titleBold12(context),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
DropdownButtonFormField<MultisigScriptType>(
|
||||||
|
value: setupData.scriptType,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: MultisigScriptType.values.map((type) {
|
||||||
|
String label;
|
||||||
|
switch (type) {
|
||||||
|
case MultisigScriptType.legacy:
|
||||||
|
label = "Legacy (P2SH)";
|
||||||
|
break;
|
||||||
|
case MultisigScriptType.segwit:
|
||||||
|
label = "Nested SegWit (P2SH-P2WSH)";
|
||||||
|
break;
|
||||||
|
case MultisigScriptType.nativeSegwit:
|
||||||
|
label = "Native SegWit (P2WSH)";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return DropdownMenuItem(
|
||||||
|
value: type,
|
||||||
|
child: Text(label),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
multisigSetupStateProvider.notifier,
|
||||||
|
)
|
||||||
|
.updateScriptType(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Multisig params setup
|
||||||
|
|
||||||
|
RoundedContainer(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
color: Theme.of(context)
|
||||||
|
.extension<StackColors>()!
|
||||||
|
.popupBG,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Total cosigners: ${setupData.totalCosigners}",
|
||||||
|
style: STextStyles.titleBold12(context),
|
||||||
|
),
|
||||||
|
Slider(
|
||||||
|
value: setupData.totalCosigners.toDouble(),
|
||||||
|
min: 2,
|
||||||
|
max: 7, // There's not actually a max.
|
||||||
|
divisions: 7, // Match the above or look off.
|
||||||
|
label:
|
||||||
|
"${setupData.totalCosigners} cosigners",
|
||||||
|
onChanged: (value) {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
multisigSetupStateProvider.notifier)
|
||||||
|
.updateTotalCosigners(value.toInt());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
"Required Signatures: $clampedThreshold of ${setupData.totalCosigners}",
|
||||||
|
style: STextStyles.titleBold12(context),
|
||||||
|
),
|
||||||
|
Slider(
|
||||||
|
value: clampedThreshold.toDouble(),
|
||||||
|
min: 1,
|
||||||
|
max: setupData.totalCosigners.toDouble(),
|
||||||
|
divisions: setupData.totalCosigners - 1,
|
||||||
|
label:
|
||||||
|
"$clampedThreshold of ${setupData.totalCosigners}",
|
||||||
|
onChanged: (value) {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
multisigSetupStateProvider.notifier)
|
||||||
|
.updateThreshold(value.toInt());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// We'll make a FROST-like progress indicator in a
|
||||||
|
// dialog to show the progress of the setup process.
|
||||||
|
// This simpler example will be removed soon.
|
||||||
|
// Text(
|
||||||
|
// "Exchange Method",
|
||||||
|
// style: STextStyles.itemSubtitle(context),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 16),
|
||||||
|
//
|
||||||
|
// // NFC exchange.
|
||||||
|
// RoundedContainer(
|
||||||
|
// padding: const EdgeInsets.all(16),
|
||||||
|
// color: Theme.of(context)
|
||||||
|
// .extension<StackColors>()!
|
||||||
|
// .popupBG,
|
||||||
|
// child: Column(
|
||||||
|
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
// children: [
|
||||||
|
// Row(
|
||||||
|
// children: [
|
||||||
|
// Icon(
|
||||||
|
// _isNfcAvailable
|
||||||
|
// ? Icons.nfc
|
||||||
|
// : Icons.nfc_outlined,
|
||||||
|
// color: _isNfcAvailable
|
||||||
|
// ? Theme.of(context)
|
||||||
|
// .extension<StackColors>()!
|
||||||
|
// .accentColorGreen
|
||||||
|
// : Theme.of(context)
|
||||||
|
// .extension<StackColors>()!
|
||||||
|
// .textDark3,
|
||||||
|
// ),
|
||||||
|
// const SizedBox(width: 8),
|
||||||
|
// Text(
|
||||||
|
// "NFC Exchange",
|
||||||
|
// style: STextStyles.titleBold12(context),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: 16),
|
||||||
|
// Text(
|
||||||
|
// _nfcStatus,
|
||||||
|
// style: STextStyles.baseXS(context),
|
||||||
|
// ),
|
||||||
|
// if (_isNfcAvailable) ...[
|
||||||
|
// const SizedBox(height: 16),
|
||||||
|
// SizedBox(
|
||||||
|
// width: double.infinity,
|
||||||
|
// child: !isDesktop
|
||||||
|
// ? TextButton(
|
||||||
|
// onPressed: _startNfcSession,
|
||||||
|
// style: Theme.of(context)
|
||||||
|
// .extension<StackColors>()!
|
||||||
|
// .getPrimaryEnabledButtonStyle(
|
||||||
|
// context),
|
||||||
|
// child: Text(
|
||||||
|
// "Tap to Exchange Information",
|
||||||
|
// style:
|
||||||
|
// STextStyles.button(context),
|
||||||
|
// ),
|
||||||
|
// )
|
||||||
|
// : PrimaryButton(
|
||||||
|
// label:
|
||||||
|
// "Tap to Exchange Information",
|
||||||
|
// onPressed: _startNfcSession,
|
||||||
|
// enabled: true,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ],
|
||||||
|
// ],
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -73,6 +73,7 @@ import '../../widgets/wallet_navigation_bar/components/icons/coin_control_nav_ic
|
||||||
import '../../widgets/wallet_navigation_bar/components/icons/exchange_nav_icon.dart';
|
import '../../widgets/wallet_navigation_bar/components/icons/exchange_nav_icon.dart';
|
||||||
import '../../widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart';
|
import '../../widgets/wallet_navigation_bar/components/icons/frost_sign_nav_icon.dart';
|
||||||
import '../../widgets/wallet_navigation_bar/components/icons/fusion_nav_icon.dart';
|
import '../../widgets/wallet_navigation_bar/components/icons/fusion_nav_icon.dart';
|
||||||
|
import '../../widgets/wallet_navigation_bar/components/icons/multisig_setup_nav_icon.dart';
|
||||||
import '../../widgets/wallet_navigation_bar/components/icons/ordinals_nav_icon.dart';
|
import '../../widgets/wallet_navigation_bar/components/icons/ordinals_nav_icon.dart';
|
||||||
import '../../widgets/wallet_navigation_bar/components/icons/paynym_nav_icon.dart';
|
import '../../widgets/wallet_navigation_bar/components/icons/paynym_nav_icon.dart';
|
||||||
import '../../widgets/wallet_navigation_bar/components/icons/receive_nav_icon.dart';
|
import '../../widgets/wallet_navigation_bar/components/icons/receive_nav_icon.dart';
|
||||||
|
@ -96,6 +97,7 @@ import '../settings_views/wallet_settings_view/wallet_network_settings_view/wall
|
||||||
import '../settings_views/wallet_settings_view/wallet_settings_view.dart';
|
import '../settings_views/wallet_settings_view/wallet_settings_view.dart';
|
||||||
import '../special/firo_rescan_recovery_error_dialog.dart';
|
import '../special/firo_rescan_recovery_error_dialog.dart';
|
||||||
import '../token_view/my_tokens_view.dart';
|
import '../token_view/my_tokens_view.dart';
|
||||||
|
import 'multisig_setup_view/multisig_setup_view.dart';
|
||||||
import 'sub_widgets/transactions_list.dart';
|
import 'sub_widgets/transactions_list.dart';
|
||||||
import 'sub_widgets/wallet_summary.dart';
|
import 'sub_widgets/wallet_summary.dart';
|
||||||
import 'transaction_views/all_transactions_view.dart';
|
import 'transaction_views/all_transactions_view.dart';
|
||||||
|
@ -1233,6 +1235,18 @@ class _WalletViewState extends ConsumerState<WalletView> {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (wallet.info.coin
|
||||||
|
is Bitcoin) // TODO [prio=low]: test if !isViewOnly is necessary... I think not, we should be able to make view-only shared multisig wallets.
|
||||||
|
WalletNavigationBarItemData(
|
||||||
|
label: "Make multisignature account",
|
||||||
|
icon: const MultisigSetupNavIcon(),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pushNamed(
|
||||||
|
MultisigSetupView.routeName,
|
||||||
|
arguments: walletId,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -426,6 +426,14 @@ class _MoreFeaturesDialogState extends ConsumerState<MoreFeaturesDialog> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// TODO [prio=low]: Implement BIP48 accounts on desktop using copy/paste and/or webcam scanning.
|
||||||
|
// if (wallet.info.coin is Bitcoin) // TODO [prio=low]: test if !isViewOnly is necessary... I think not, we should be able to make view-only shared multisig wallets.
|
||||||
|
// _MoreFeaturesItem(
|
||||||
|
// label: "Make multisignature account",
|
||||||
|
// detail: "Share an account with other wallets",
|
||||||
|
// iconAsset: Assets.svg.peers, // I just picked a suitable icon, maybe another is more appropriate.
|
||||||
|
// onPressed: () async => widget.onBIP48Pressed?.call(),
|
||||||
|
// ),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 28,
|
height: 28,
|
||||||
),
|
),
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
import 'package:stackwallet/pages/wallet_view/multisig_setup_view/multisig_setup_view.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
import 'models/add_wallet_list_entity/add_wallet_list_entity.dart';
|
import 'models/add_wallet_list_entity/add_wallet_list_entity.dart';
|
||||||
|
@ -2155,6 +2156,16 @@ class RouteGenerator {
|
||||||
}
|
}
|
||||||
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
|
case MultisigSetupView.routeName:
|
||||||
|
return getRoute(
|
||||||
|
shouldUseMaterialRoute: useMaterialPageRoute,
|
||||||
|
builder: (_) => const MultisigSetupView(),
|
||||||
|
settings: RouteSettings(
|
||||||
|
name: settings.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return _routeError("${settings.name} invalid args: ${args.toString()}");
|
||||||
|
|
||||||
// == Desktop specific routes ============================================
|
// == Desktop specific routes ============================================
|
||||||
case CreatePasswordView.routeName:
|
case CreatePasswordView.routeName:
|
||||||
if (args is bool) {
|
if (args is bool) {
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* This file is part of Stack Wallet.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2023 Cypher Stack
|
||||||
|
* All Rights Reserved.
|
||||||
|
* The code is distributed under GPLv3 license, see LICENSE file for details.
|
||||||
|
* Generated by Cypher Stack on 2023-05-26
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
||||||
|
import '../../../../themes/stack_colors.dart';
|
||||||
|
import '../../../../utilities/assets.dart';
|
||||||
|
|
||||||
|
class MultisigSetupNavIcon extends StatelessWidget {
|
||||||
|
const MultisigSetupNavIcon({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SvgPicture.asset(
|
||||||
|
Assets.svg
|
||||||
|
.peers, // I just picked a suitable icon, maybe another is more appropriate.
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
color: Theme.of(context).extension<StackColors>()!.bottomNavIconIcon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
16
pubspec.lock
16
pubspec.lock
|
@ -121,6 +121,14 @@ packages:
|
||||||
url: "https://github.com/cypherstack/bip47.git"
|
url: "https://github.com/cypherstack/bip47.git"
|
||||||
source: git
|
source: git
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
bip48:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: bip48
|
||||||
|
sha256: c31fa9a3fc1d755048c49317aa33b4cc8a396af387ffa1561010a981e4c9e8ca
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.3"
|
||||||
bitbox:
|
bitbox:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -1376,6 +1384,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
nfc_manager:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: nfc_manager
|
||||||
|
sha256: f5be75e90f8f2bff3ee49fbd7ef65bdd4a86ee679c2412e71ab2846a8cff8c59
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.5.0"
|
||||||
nm:
|
nm:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -204,6 +204,8 @@ dependencies:
|
||||||
cbor: ^6.3.3
|
cbor: ^6.3.3
|
||||||
cs_monero: 1.0.0-pre.1
|
cs_monero: 1.0.0-pre.1
|
||||||
cs_monero_flutter_libs: 1.0.0-pre.0
|
cs_monero_flutter_libs: 1.0.0-pre.0
|
||||||
|
nfc_manager: ^3.5.0
|
||||||
|
bip48: ^0.0.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
Loading…
Reference in a new issue