mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-06 18:59:24 +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/frost_sign_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/paynym_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 '../special/firo_rescan_recovery_error_dialog.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/wallet_summary.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(
|
||||
height: 28,
|
||||
),
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.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 'models/add_wallet_list_entity/add_wallet_list_entity.dart';
|
||||
|
@ -2155,6 +2156,16 @@ class RouteGenerator {
|
|||
}
|
||||
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 ============================================
|
||||
case CreatePasswordView.routeName:
|
||||
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"
|
||||
source: git
|
||||
version: "2.0.0"
|
||||
bip48:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bip48
|
||||
sha256: c31fa9a3fc1d755048c49317aa33b4cc8a396af387ffa1561010a981e4c9e8ca
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.3"
|
||||
bitbox:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1376,6 +1384,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -204,6 +204,8 @@ dependencies:
|
|||
cbor: ^6.3.3
|
||||
cs_monero: 1.0.0-pre.1
|
||||
cs_monero_flutter_libs: 1.0.0-pre.0
|
||||
nfc_manager: ^3.5.0
|
||||
bip48: ^0.0.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in a new issue