Merge remote-tracking branch 'origin/staging' into scripts

This commit is contained in:
sneurlax 2024-07-04 15:55:51 -05:00
commit 3ed8f6c442
63 changed files with 3974 additions and 1115 deletions

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="75.209999mm"
height="75.209999mm"
viewBox="0 0 213.19692 213.2"
version="1.1"
id="svg30"
sodipodi:docname="nanswap2.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
<metadata
id="metadata36">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>nanswap</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs34" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2635"
inkscape:window-height="1461"
id="namedview32"
showgrid="false"
inkscape:zoom="0.29390619"
inkscape:cx="-79.957486"
inkscape:cy="142.12913"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="0"
inkscape:current-layer="svg30" />
<title
id="title2">nanswap</title>
<circle
cx="106.6"
cy="106.6"
r="106.6"
id="circle4"
style="fill:#4a90e2" />
<path
d="m 166.7,66.500006 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,19.999994 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,20 a 20,20 0 1 1 -20,-20 c 15,0 20,-5 20,-20 a 20,20 0 0 1 20,-19.999994 c 15,0 20,-5 20,-20 a 20,20 0 0 1 40,0 z"
id="path6"
inkscape:connector-curvature="0"
style="fill:#000034" />
<path
d="m 66.5,46.500006 a 20,20 0 0 1 20,20 c 0,15 5,20 20,20 A 20,20 0 0 1 126.5,106.5 c 0,15 5,20 20,20 a 20,20 0 1 1 -20,20 c 0,-15 -5,-20 -20,-20 a 20,20 0 0 1 -20,-20 c 0,-14.999994 -5,-19.999994 -20,-19.999994 a 20,20 0 0 1 0,-40 z"
id="path8"
inkscape:connector-curvature="0"
style="fill:#000034" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="75.209999mm"
height="75.209999mm"
viewBox="0 0 213.19692 213.2"
version="1.1"
id="svg30"
sodipodi:docname="nanswap2.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
<metadata
id="metadata36">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>nanswap</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs34" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2635"
inkscape:window-height="1461"
id="namedview32"
showgrid="false"
inkscape:zoom="0.29390619"
inkscape:cx="-79.957486"
inkscape:cy="142.12913"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="0"
inkscape:current-layer="svg30" />
<title
id="title2">nanswap</title>
<circle
cx="106.6"
cy="106.6"
r="106.6"
id="circle4"
style="fill:#4a90e2" />
<path
d="m 166.7,66.500006 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,19.999994 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,20 a 20,20 0 1 1 -20,-20 c 15,0 20,-5 20,-20 a 20,20 0 0 1 20,-19.999994 c 15,0 20,-5 20,-20 a 20,20 0 0 1 40,0 z"
id="path6"
inkscape:connector-curvature="0"
style="fill:#000034" />
<path
d="m 66.5,46.500006 a 20,20 0 0 1 20,20 c 0,15 5,20 20,20 A 20,20 0 0 1 126.5,106.5 c 0,15 5,20 20,20 a 20,20 0 1 1 -20,20 c 0,-15 -5,-20 -20,-20 a 20,20 0 0 1 -20,-20 c 0,-14.999994 -5,-19.999994 -20,-19.999994 a 20,20 0 0 1 0,-40 z"
id="path8"
inkscape:connector-curvature="0"
style="fill:#000034" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="75.209999mm"
height="75.209999mm"
viewBox="0 0 213.19692 213.2"
version="1.1"
id="svg30"
sodipodi:docname="nanswap2.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
<metadata
id="metadata36">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>nanswap</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs34" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2635"
inkscape:window-height="1461"
id="namedview32"
showgrid="false"
inkscape:zoom="0.29390619"
inkscape:cx="-79.957486"
inkscape:cy="142.12913"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="0"
inkscape:current-layer="svg30" />
<title
id="title2">nanswap</title>
<circle
cx="106.6"
cy="106.6"
r="106.6"
id="circle4"
style="fill:#4a90e2" />
<path
d="m 166.7,66.500006 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,19.999994 a 20,20 0 0 1 -20,20 c -15,0 -20,5 -20,20 a 20,20 0 1 1 -20,-20 c 15,0 20,-5 20,-20 a 20,20 0 0 1 20,-19.999994 c 15,0 20,-5 20,-20 a 20,20 0 0 1 40,0 z"
id="path6"
inkscape:connector-curvature="0"
style="fill:#000034" />
<path
d="m 66.5,46.500006 a 20,20 0 0 1 20,20 c 0,15 5,20 20,20 A 20,20 0 0 1 126.5,106.5 c 0,15 5,20 20,20 a 20,20 0 1 1 -20,20 c 0,-15 -5,-20 -20,-20 a 20,20 0 0 1 -20,-20 c 0,-14.999994 -5,-19.999994 -20,-19.999994 a 20,20 0 0 1 0,-40 z"
id="path8"
inkscape:connector-curvature="0"
style="fill:#000034" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@ -1 +1 @@
Subproject commit dd2d493199dd9c697abc8bc6bc94f466005bf70a
Subproject commit 982f5ab19fe0dd3dd3f6be2c46f8dff13d49027c

View file

@ -116,6 +116,22 @@ cd ..
or manually by creating the files referenced in that script with the specified content.
### Build plugins
#### Build script: `build_app.sh`
The `build_app.sh` script is use to build applications Stack Wallet. View the script's help message with `./build_app.sh -h` for more information on its usage.
Options:
- `a <app>`: Specify the application ID (required). Valid options are `stack_wallet` or `stack_duo`.
- `b <build_number>`: Specify the build number in 123 (required).
- `p <platform>`: Specify the platform to build for (required). Valid options are `android`, `ios`, `macos`, `linux`, or `windows`.
- `v <version>`: Specify the version of the application in 1.2.3 format (required).
- `i`: Optional flag to skip building crypto plugins. Useful for updating `pubspec.yaml` and white-labelling different apps with the same plugins.
For example,
```
./build_app.sh -a stack_wallet -p linux -v 2.1.0 -b 210
```
#### Building plugins for Android
> Warning: This will take a long time, please be patient
```

View file

@ -405,6 +405,7 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
color: Colors.transparent,
child: Column(
children: [
if (coin is Firo)
CheckboxTextButton(
label: "Scan for Lelantus transactions",
onChanged: (newValue) {
@ -413,6 +414,7 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
});
},
),
if (coin is Firo)
const SizedBox(
height: 8,
),
@ -500,6 +502,9 @@ class _RestoreOptionsViewState extends ConsumerState<RestoreOptionsView> {
),
),
),
const SizedBox(
height: 16,
),
],
),
),

View file

@ -8,11 +8,15 @@
*
*/
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../models/buy/response_objects/order.dart';
import '../../notifications/show_flush_bar.dart';
import '../../themes/stack_colors.dart';
import '../../themes/theme_providers.dart';
import '../../utilities/assets.dart';
@ -44,6 +48,16 @@ class _BuyOrderDetailsViewState extends ConsumerState<BuyOrderDetailsView> {
@override
Widget build(BuildContext context) {
final orderDetails = '''
Purchase ID: ${widget.order.paymentId}
User ID: ${widget.order.userId}
Quote ID: ${widget.order.quote.id}
Quoted cost: ${widget.order.quote.youPayFiatPrice.toStringAsFixed(2)} ${widget.order.quote.fiat.ticker.toUpperCase()}
Quoted amount: ${widget.order.quote.youReceiveCryptoAmount} ${widget.order.quote.crypto.ticker.toUpperCase()}
Receiving ${widget.order.quote.crypto.ticker.toUpperCase()} address: ${widget.order.quote.receivingAddress}
Provider: Simplex
''';
return ConditionalParent(
condition: !isDesktop,
builder: (child) {
@ -272,6 +286,43 @@ class _BuyOrderDetailsViewState extends ConsumerState<BuyOrderDetailsView> {
),
],
),
const SizedBox(height: 8),
TextButton(
onPressed: () async {
await Clipboard.setData(ClipboardData(text: orderDetails));
if (context.mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
context: context,
),
);
}
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SvgPicture.asset(
Assets.svg.copy,
width: 20,
height: 20,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
const SizedBox(
width: 10,
),
Text(
"Copy to clipboard",
style: STextStyles.desktopButtonSecondaryEnabled(
context,
),
),
],
),
),
const Spacer(),
PrimaryButton(
label: "Dismiss",

View file

@ -22,6 +22,7 @@ import '../../../services/exchange/change_now/change_now_exchange.dart';
import '../../../services/exchange/exchange.dart';
import '../../../services/exchange/exchange_data_loading_service.dart';
import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart';
import '../../../services/exchange/nanswap/nanswap_exchange.dart';
import '../../../services/exchange/trocador/trocador_exchange.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/assets.dart';
@ -117,6 +118,8 @@ class _ExchangeCurrencySelectionViewState
.exchangeNameEqualTo(MajesticBankExchange.exchangeName)
.or()
.exchangeNameStartsWith(TrocadorExchange.exchangeName)
.or()
.exchangeNameStartsWith(NanswapExchange.exchangeName)
.findAll();
final cn = await ChangeNowExchange.instance.getPairedCurrencies(

View file

@ -33,6 +33,7 @@ import '../../services/exchange/exchange.dart';
import '../../services/exchange/exchange_data_loading_service.dart';
import '../../services/exchange/exchange_response.dart';
import '../../services/exchange/majestic_bank/majestic_bank_exchange.dart';
import '../../services/exchange/nanswap/nanswap_exchange.dart';
import '../../services/exchange/trocador/trocador_exchange.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/amount/amount_unit.dart';
@ -87,6 +88,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
MajesticBankExchange.instance,
ChangeNowExchange.instance,
TrocadorExchange.instance,
NanswapExchange.instance,
];
}
}

View file

@ -15,7 +15,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../app_config.dart';
import '../../../models/exchange/incomplete_exchange.dart';
import '../../../providers/providers.dart';
import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/address_utils.dart';
import '../../../utilities/barcode_scanner_interface.dart';
@ -126,8 +125,7 @@ class _Step2ViewState extends ConsumerState<Step2View> {
@override
Widget build(BuildContext context) {
final supportsRefund =
ref.watch(efExchangeProvider).name != MajesticBankExchange.exchangeName;
final supportsRefund = ref.watch(efExchangeProvider).supportsRefundAddress;
return Background(
child: Scaffold(

View file

@ -18,7 +18,6 @@ import '../../../models/exchange/response_objects/trade.dart';
import '../../../providers/global/trades_service_provider.dart';
import '../../../providers/providers.dart';
import '../../../services/exchange/exchange_response.dart';
import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart';
import '../../../services/notifications_api.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/assets.dart';
@ -63,8 +62,7 @@ class _Step3ViewState extends ConsumerState<Step3View> {
@override
Widget build(BuildContext context) {
final supportsRefund =
ref.watch(efExchangeProvider).name != MajesticBankExchange.exchangeName;
final supportsRefund = ref.watch(efExchangeProvider).supportsRefundAddress;
return Background(
child: Scaffold(

View file

@ -10,17 +10,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../models/exchange/aggregate_currency.dart';
import 'exchange_provider_option.dart';
import '../../../providers/providers.dart';
import '../../../services/exchange/change_now/change_now_exchange.dart';
import '../../../services/exchange/exchange.dart';
import '../../../services/exchange/majestic_bank/majestic_bank_exchange.dart';
import '../../../services/exchange/nanswap/nanswap_exchange.dart';
import '../../../services/exchange/trocador/trocador_exchange.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/prefs.dart';
import '../../../utilities/util.dart';
import '../../../widgets/rounded_white_container.dart';
import 'exchange_provider_option.dart';
class ExchangeProviderOptions extends ConsumerStatefulWidget {
const ExchangeProviderOptions({
@ -88,6 +90,11 @@ class _ExchangeProviderOptionsState
sendCurrency: sendCurrency,
receiveCurrency: receivingCurrency,
);
final showNanswap = exchangeSupported(
exchangeName: NanswapExchange.exchangeName,
sendCurrency: sendCurrency,
receiveCurrency: receivingCurrency,
);
return RoundedWhiteContainer(
padding: isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(12),
@ -134,6 +141,23 @@ class _ExchangeProviderOptionsState
reversed: widget.reversed,
exchange: TrocadorExchange.instance,
),
if ((showChangeNow || showMajesticBank || showTrocador) &&
showNanswap)
isDesktop
? Container(
height: 1,
color:
Theme.of(context).extension<StackColors>()!.background,
)
: const SizedBox(
height: 16,
),
if (showNanswap)
ExchangeOption(
fixedRate: widget.fixedRate,
reversed: widget.reversed,
exchange: NanswapExchange.instance,
),
],
),
);

View file

@ -30,6 +30,7 @@ import '../../route_generator.dart';
import '../../services/exchange/change_now/change_now_exchange.dart';
import '../../services/exchange/exchange.dart';
import '../../services/exchange/majestic_bank/majestic_bank_exchange.dart';
import '../../services/exchange/nanswap/nanswap_exchange.dart';
import '../../services/exchange/simpleswap/simpleswap_exchange.dart';
import '../../services/exchange/trocador/trocador_exchange.dart';
import '../../themes/stack_colors.dart';
@ -1330,6 +1331,10 @@ class _TradeDetailsViewState extends ConsumerState<TradeDetailsView> {
url =
"https://majesticbank.sc/track?trx=${trade.tradeId}";
break;
case NanswapExchange.exchangeName:
url =
"https://nanswap.com/transaction/${trade.tradeId}";
break;
default:
if (trade.exchangeName

View file

@ -188,12 +188,14 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> {
_timeout = Duration.zero;
_checkUseBiometrics();
_pinTextController.addListener(_onPinChanged);
super.initState();
}
@override
dispose() {
// _shakeController.dispose();
_pinTextController.removeListener(_onPinChanged);
super.dispose();
}
@ -208,13 +210,27 @@ class _LockscreenViewState extends ConsumerState<LockscreenView> {
);
}
final _pinTextController = TextEditingController();
final FocusNode _pinFocusNode = FocusNode();
late SecureStorageInterface _secureStore;
late Biometrics biometrics;
int pinCount = 1;
final _pinTextController = TextEditingController();
void _onPinChanged() async {
String enteredPin = _pinTextController.text;
final storedPin = await _secureStore.read(key: 'stack_pin');
final autoPin = ref.read(prefsChangeNotifierProvider).autoPin;
if (enteredPin.length >= 4 && autoPin && enteredPin == storedPin) {
await Future<void>.delayed(
const Duration(milliseconds: 200),
);
unawaited(_onUnlock());
}
}
Widget get _body => Background(
child: SafeArea(
child: Scaffold(

View file

@ -0,0 +1,292 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../notifications/show_flush_bar.dart';
import '../../providers/global/prefs_provider.dart';
import '../../providers/global/secure_store_provider.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/assets.dart';
import '../../utilities/biometrics.dart';
import '../../utilities/flutter_secure_storage_interface.dart';
import '../../utilities/text_styles.dart';
import '../../widgets/custom_pin_put/custom_pin_put.dart';
import '../../widgets/shake/shake.dart';
import '../../widgets/stack_dialog.dart';
class PinpadDialog extends ConsumerStatefulWidget {
const PinpadDialog({
super.key,
required this.biometricsAuthenticationTitle,
required this.biometricsLocalizedReason,
required this.biometricsCancelButtonString,
this.biometrics = const Biometrics(),
this.customKeyLabel = "Button",
});
final String biometricsAuthenticationTitle;
final String biometricsLocalizedReason;
final String biometricsCancelButtonString;
final Biometrics biometrics;
final String customKeyLabel;
@override
ConsumerState<PinpadDialog> createState() => _PinpadDialogState();
}
class _PinpadDialogState extends ConsumerState<PinpadDialog> {
late final ShakeController _shakeController;
late int _attempts;
bool _attemptLock = false;
late Duration _timeout;
static const maxAttemptsBeforeThrottling = 3;
Timer? _timer;
final FocusNode _pinFocusNode = FocusNode();
late SecureStorageInterface _secureStore;
late Biometrics biometrics;
int pinCount = 1;
final _pinTextController = TextEditingController();
BoxDecoration get _pinPutDecoration {
return BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.infoItemIcons,
border: Border.all(
width: 1,
color: Theme.of(context).extension<StackColors>()!.infoItemIcons,
),
borderRadius: BorderRadius.circular(6),
);
}
Future<void> _onPinChanged() async {
final enteredPin = _pinTextController.text;
final storedPin = await _secureStore.read(key: 'stack_pin');
final autoPin = ref.read(prefsChangeNotifierProvider).autoPin;
if (enteredPin.length >= 4 && autoPin && enteredPin == storedPin) {
await Future<void>.delayed(
const Duration(milliseconds: 200),
);
unawaited(_onUnlock());
}
}
Future<void> _onUnlock() async {
final now = DateTime.now().toUtc();
ref.read(prefsChangeNotifierProvider).lastUnlocked =
now.millisecondsSinceEpoch ~/ 1000;
Navigator.of(context).pop("verified success");
}
Future<void> _checkUseBiometrics() async {
if (!ref.read(prefsChangeNotifierProvider).isInitialized) {
await ref.read(prefsChangeNotifierProvider).init();
}
final bool useBiometrics =
ref.read(prefsChangeNotifierProvider).useBiometrics;
final title = widget.biometricsAuthenticationTitle;
final localizedReason = widget.biometricsLocalizedReason;
final cancelButtonText = widget.biometricsCancelButtonString;
if (useBiometrics) {
if (await biometrics.authenticate(
title: title,
localizedReason: localizedReason,
cancelButtonText: cancelButtonText,
)) {
unawaited(_onUnlock());
}
// leave this commented to enable pin fall back should biometrics not work properly
// else {
// Navigator.pop(context);
// }
}
}
Future<void> _onSubmit(String pin) async {
_attempts++;
if (_attempts > maxAttemptsBeforeThrottling) {
_attemptLock = true;
switch (_attempts) {
case 4:
_timeout = const Duration(seconds: 30);
break;
case 5:
_timeout = const Duration(seconds: 60);
break;
case 6:
_timeout = const Duration(minutes: 5);
break;
case 7:
_timeout = const Duration(minutes: 10);
break;
case 8:
_timeout = const Duration(minutes: 20);
break;
case 9:
_timeout = const Duration(minutes: 30);
break;
default:
_timeout = const Duration(minutes: 60);
}
_timer?.cancel();
_timer = Timer(_timeout, () {
_attemptLock = false;
_attempts = 0;
});
}
if (_attemptLock) {
String prettyTime = "";
if (_timeout.inSeconds >= 60) {
prettyTime += "${_timeout.inMinutes} minutes";
} else {
prettyTime += "${_timeout.inSeconds} seconds";
}
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message:
"Incorrect PIN entered too many times. Please wait $prettyTime",
context: context,
iconAsset: Assets.svg.alertCircle,
),
);
await Future<void>.delayed(
const Duration(milliseconds: 100),
);
_pinTextController.text = '';
return;
}
final storedPin = await _secureStore.read(key: 'stack_pin');
if (mounted) {
if (storedPin == pin) {
await Future<void>.delayed(
const Duration(milliseconds: 200),
);
unawaited(_onUnlock());
} else {
unawaited(_shakeController.shake());
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Incorrect PIN. Please try again",
context: context,
iconAsset: Assets.svg.alertCircle,
),
);
await Future<void>.delayed(
const Duration(milliseconds: 100),
);
_pinTextController.text = '';
}
}
}
@override
void initState() {
_shakeController = ShakeController();
_secureStore = ref.read(secureStoreProvider);
biometrics = widget.biometrics;
_attempts = 0;
_timeout = Duration.zero;
_checkUseBiometrics();
_pinTextController.addListener(_onPinChanged);
super.initState();
}
@override
dispose() {
// _shakeController.dispose();
_pinTextController.removeListener(_onPinChanged);
super.dispose();
}
@override
Widget build(BuildContext context) {
return StackDialogBase(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Shake(
animationDuration: const Duration(milliseconds: 700),
animationRange: 12,
controller: _shakeController,
child: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
"Enter PIN",
style: STextStyles.pageTitleH1(context),
),
),
const SizedBox(
height: 40,
),
CustomPinPut(
fieldsCount: pinCount,
eachFieldHeight: 12,
eachFieldWidth: 12,
textStyle: STextStyles.label(context).copyWith(
fontSize: 1,
),
focusNode: _pinFocusNode,
controller: _pinTextController,
useNativeKeyboard: false,
obscureText: "",
inputDecoration: InputDecoration(
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
disabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
focusedErrorBorder: InputBorder.none,
fillColor:
Theme.of(context).extension<StackColors>()!.popupBG,
counterText: "",
),
submittedFieldDecoration: _pinPutDecoration,
isRandom:
ref.read(prefsChangeNotifierProvider).randomizePIN,
onSubmit: _onSubmit,
),
const SizedBox(
height: 32,
),
],
),
),
),
],
),
);
}
}

View file

@ -22,6 +22,7 @@ import '../../../utilities/address_utils.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../widgets/address_private_key.dart';
import '../../../widgets/background.dart';
import '../../../widgets/conditional_parent.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
@ -30,6 +31,7 @@ import '../../../widgets/custom_buttons/simple_copy_button.dart';
import '../../../widgets/custom_buttons/simple_edit_button.dart';
import '../../../widgets/desktop/desktop_dialog.dart';
import '../../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../../widgets/detail_item.dart';
import '../../../widgets/qr.dart';
import '../../../widgets/rounded_white_container.dart';
import '../../../widgets/transaction_card.dart';
@ -298,9 +300,9 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
const SizedBox(
height: 16,
),
_Item(
DetailItem(
title: "Address",
data: address.value,
detail: address.value,
button: isDesktop
? IconCopyButton(
data: address.value,
@ -312,9 +314,9 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
const _Div(
height: 12,
),
_Item(
DetailItem(
title: "Label",
data: label!.value,
detail: label!.value,
button: SimpleEditButton(
editValue: label!.value,
editLabel: 'label',
@ -338,9 +340,9 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
height: 12,
),
if (address.derivationPath != null)
_Item(
DetailItem(
title: "Derivation path",
data: address.derivationPath!.value,
detail: address.derivationPath!.value,
button: Container(),
),
if (address.type == AddressType.spark)
@ -348,27 +350,34 @@ class _AddressDetailsViewState extends ConsumerState<AddressDetailsView> {
height: 12,
),
if (address.type == AddressType.spark)
_Item(
DetailItem(
title: "Diversifier",
data: address.derivationIndex.toString(),
detail: address.derivationIndex.toString(),
button: Container(),
),
const _Div(
height: 12,
),
_Item(
DetailItem(
title: "Type",
data: address.type.readableName,
detail: address.type.readableName,
button: Container(),
),
const _Div(
height: 12,
),
_Item(
DetailItem(
title: "Sub type",
data: address.subType.prettyName,
detail: address.subType.prettyName,
button: Container(),
),
const _Div(
height: 12,
),
AddressPrivateKey(
walletId: widget.walletId,
address: address,
),
if (!isDesktop)
const SizedBox(
height: 20,
@ -631,64 +640,3 @@ class _Tags extends StatelessWidget {
);
}
}
class _Item extends StatelessWidget {
const _Item({
super.key,
required this.title,
required this.data,
required this.button,
});
final String title;
final String data;
final Widget button;
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: !Util.isDesktop,
builder: (child) => RoundedWhiteContainer(
child: child,
),
child: ConditionalParent(
condition: Util.isDesktop,
builder: (child) => Padding(
padding: const EdgeInsets.all(16),
child: child,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: STextStyles.itemSubtitle(context),
),
button,
],
),
const SizedBox(
height: 5,
),
data.isNotEmpty
? SelectableText(
data,
style: STextStyles.w500_14(context),
)
: Text(
"$title will appear here",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle3,
),
),
],
),
),
);
}
}

View file

@ -61,9 +61,12 @@ class _ChangePinViewState extends ConsumerState<ChangePinView> {
int pinCount = 1;
final TextEditingController _pinTextController = TextEditingController();
@override
void initState() {
_secureStore = ref.read(secureStoreProvider);
_pinTextController.addListener(_onPinChanged);
super.initState();
}
@ -74,9 +77,23 @@ class _ChangePinViewState extends ConsumerState<ChangePinView> {
_pinPutController2.dispose();
_pinPutFocusNode1.dispose();
_pinPutFocusNode2.dispose();
_pinTextController.removeListener(_onPinChanged);
super.dispose();
}
void _onPinChanged() async {
String enteredPin = _pinTextController.text;
final storedPin = await _secureStore.read(key: 'stack_pin');
final autoPin = ref.read(prefsChangeNotifierProvider).autoPin;
if (enteredPin.length >= 4 && autoPin && enteredPin == storedPin) {
await _pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.linear,
);
}
}
@override
Widget build(BuildContext context) {
return Background(

View file

@ -10,8 +10,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../pinpad_views/lock_screen_view.dart';
import 'change_pin_view/change_pin_view.dart';
import '../../../../providers/global/prefs_provider.dart';
import '../../../../route_generator.dart';
import '../../../../themes/stack_colors.dart';
@ -21,6 +20,8 @@ import '../../../../widgets/background.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/custom_buttons/draggable_switch_button.dart';
import '../../../../widgets/rounded_white_container.dart';
import '../../../pinpad_views/lock_screen_view.dart';
import 'change_pin_view/change_pin_view.dart';
class SecurityView extends StatelessWidget {
const SecurityView({
@ -203,6 +204,54 @@ class SecurityView extends StatelessWidget {
},
),
),
// The "autoPin" preference (whether to automatically accept a correct PIN).
const SizedBox(
height: 8,
),
RoundedWhiteContainer(
child: Consumer(
builder: (_, ref, __) {
return RawMaterialButton(
// splashColor: Theme.of(context).extension<StackColors>()!.highlight,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: null,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Auto-accept correct PIN",
style: STextStyles.titleBold12(context),
textAlign: TextAlign.left,
),
SizedBox(
height: 20,
width: 40,
child: DraggableSwitchButton(
isOn: ref.watch(
prefsChangeNotifierProvider
.select((value) => value.autoPin),
),
onValueChanged: (newValue) {
ref
.read(prefsChangeNotifierProvider)
.autoPin = newValue;
},
),
),
],
),
),
);
},
),
),
],
),
),

View file

@ -13,7 +13,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../../../../app_config.dart';
import '../../../../notifications/show_flush_bar.dart';
@ -25,8 +25,10 @@ import '../../../../utilities/constants.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../../../widgets/background.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/custom_buttons/blue_text_button.dart';
import '../../../../widgets/custom_buttons/simple_copy_button.dart';
import '../../../../widgets/detail_item.dart';
import '../../../../widgets/qr.dart';
@ -34,6 +36,7 @@ import '../../../../widgets/rounded_white_container.dart';
import '../../../../widgets/stack_dialog.dart';
import '../../../add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
import '../../../wallet_view/transaction_views/transaction_details_view.dart';
import 'wallet_xprivs.dart';
class WalletBackupView extends ConsumerWidget {
const WalletBackupView({
@ -42,6 +45,7 @@ class WalletBackupView extends ConsumerWidget {
required this.mnemonic,
this.frostWalletData,
this.clipboardInterface = const ClipboardWrapper(),
this.xprivData,
});
static const String routeName = "/walletBackup";
@ -55,13 +59,13 @@ class WalletBackupView extends ConsumerWidget {
({String config, String keys})? prevGen,
})? frostWalletData;
final ClipboardInterface clipboardInterface;
final ({List<XPriv> xprivs, String fingerprint})? xprivData;
@override
Widget build(BuildContext context, WidgetRef ref) {
debugPrint("BUILD: $runtimeType");
final bool frost = frostWalletData != null;
final prevGen = frostWalletData?.prevGen != null;
return Background(
child: Scaffold(
@ -77,12 +81,31 @@ class WalletBackupView extends ConsumerWidget {
style: STextStyles.navBarTitle(context),
),
actions: [
if (xprivData != null)
Padding(
padding: const EdgeInsets.all(10),
child: CustomTextButton(
text: "xpriv(s)",
onTap: () {
Navigator.pushNamed(
context,
MobileXPrivsView.routeName,
arguments: (
walletId: walletId,
xprivData: xprivData!,
),
);
},
),
),
if (!frost && xprivData == null)
Padding(
padding: const EdgeInsets.all(10),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
color: Theme.of(context).extension<StackColors>()!.background,
color:
Theme.of(context).extension<StackColors>()!.background,
shadows: const [],
icon: SvgPicture.asset(
Assets.svg.copy,
@ -95,6 +118,7 @@ class WalletBackupView extends ConsumerWidget {
onPressed: () async {
await clipboardInterface
.setData(ClipboardData(text: mnemonic.join(" ")));
if (context.mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
@ -103,6 +127,7 @@ class WalletBackupView extends ConsumerWidget {
context: context,
),
);
}
},
),
),
@ -112,7 +137,184 @@ class WalletBackupView extends ConsumerWidget {
body: Padding(
padding: const EdgeInsets.all(16),
child: frost
? LayoutBuilder(
? _FrostKeys(
frostWalletData: frostWalletData,
walletId: walletId,
)
: _Mnemonic(
walletId: walletId,
mnemonic: mnemonic,
),
),
),
);
}
}
class _Mnemonic extends ConsumerWidget {
const _Mnemonic({super.key, required this.walletId, required this.mnemonic});
final String walletId;
final List<String> mnemonic;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(
height: 4,
),
Text(
ref.watch(pWalletName(walletId)),
textAlign: TextAlign.center,
style: STextStyles.label(context).copyWith(
fontSize: 12,
),
),
const SizedBox(
height: 4,
),
Text(
"Recovery Phrase",
textAlign: TextAlign.center,
style: STextStyles.pageTitleH1(context),
),
const SizedBox(
height: 16,
),
Container(
decoration: BoxDecoration(
color: Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
"Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.",
style: STextStyles.label(context),
),
),
),
const SizedBox(
height: 8,
),
Expanded(
child: SingleChildScrollView(
child: MnemonicTable(
words: mnemonic,
isDesktop: false,
),
),
),
const SizedBox(
height: 12,
),
TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
onPressed: () {
final String data = AddressUtils.encodeQRSeedData(mnemonic);
showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (_) {
final width = MediaQuery.of(context).size.width / 2;
return StackDialogBase(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
"Recovery phrase QR code",
style: STextStyles.pageTitleH2(context),
),
),
const SizedBox(
height: 12,
),
Center(
child: RepaintBoundary(
// key: _qrKey,
child: SizedBox(
width: width + 20,
height: width + 20,
child: QR(
data: data,
size: width,
),
),
),
),
const SizedBox(
height: 12,
),
Center(
child: SizedBox(
width: width,
child: TextButton(
onPressed: () async {
// await _capturePng(true);
Navigator.of(context).pop();
},
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(
context,
),
child: Text(
"Cancel",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
),
),
],
),
);
},
);
},
child: Text(
"Show QR Code",
style: STextStyles.button(context),
),
),
],
);
}
}
class _FrostKeys extends StatelessWidget {
const _FrostKeys({
super.key,
required this.walletId,
this.frostWalletData,
});
static const String routeName = "/walletBackup";
final String walletId;
final ({
String myName,
String config,
String keys,
({String config, String keys})? prevGen,
})? frostWalletData;
@override
Widget build(BuildContext context) {
final prevGen = frostWalletData?.prevGen != null;
return LayoutBuilder(
builder: (builderContext, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
@ -199,12 +401,10 @@ class WalletBackupView extends ConsumerWidget {
detail: frostWalletData!.prevGen!.config,
button: Util.isDesktop
? IconCopyButton(
data:
frostWalletData!.prevGen!.config,
data: frostWalletData!.prevGen!.config,
)
: SimpleCopyButton(
data:
frostWalletData!.prevGen!.config,
data: frostWalletData!.prevGen!.config,
),
),
if (prevGen)
@ -229,141 +429,66 @@ class WalletBackupView extends ConsumerWidget {
),
);
},
)
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
);
}
}
class MobileXPrivsView extends StatelessWidget {
const MobileXPrivsView({
super.key,
required this.walletId,
this.clipboardInterface = const ClipboardWrapper(),
required this.xprivData,
});
static const String routeName = "/mobileXPrivView";
final String walletId;
final ClipboardInterface clipboardInterface;
final ({List<XPriv> xprivs, String fingerprint}) xprivData;
@override
Widget build(BuildContext context) {
return Background(
child: Scaffold(
backgroundColor: Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () {
Navigator.of(context).pop();
},
),
title: Text(
"Wallet xpriv(s)",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 4,
Expanded(
child: WalletXPrivs(
walletId: walletId,
xprivData: xprivData,
),
Text(
ref.watch(pWalletName(walletId)),
textAlign: TextAlign.center,
style: STextStyles.label(context).copyWith(
fontSize: 12,
),
),
const SizedBox(
height: 4,
),
Text(
"Recovery Phrase",
textAlign: TextAlign.center,
style: STextStyles.pageTitleH1(context),
),
const SizedBox(
height: 16,
),
Container(
decoration: BoxDecoration(
color:
Theme.of(context).extension<StackColors>()!.popupBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(
"Please write down your backup key. Keep it safe and never share it with anyone. Your backup key is the only way you can access your funds if you forget your PIN, lose your phone, etc.\n\nStack Wallet does not keep nor is able to restore your backup key. Only you have access to your wallet.",
style: STextStyles.label(context),
),
),
),
const SizedBox(
height: 8,
),
Expanded(
child: SingleChildScrollView(
child: MnemonicTable(
words: mnemonic,
isDesktop: false,
),
),
),
const SizedBox(
height: 12,
),
TextButton(
style: Theme.of(context)
.extension<StackColors>()!
.getPrimaryEnabledButtonStyle(context),
onPressed: () {
final String data =
AddressUtils.encodeQRSeedData(mnemonic);
showDialog<dynamic>(
context: context,
useSafeArea: false,
barrierDismissible: true,
builder: (_) {
final width = MediaQuery.of(context).size.width / 2;
return StackDialogBase(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Text(
"Recovery phrase QR code",
style: STextStyles.pageTitleH2(context),
),
),
const SizedBox(
height: 12,
),
Center(
child: RepaintBoundary(
// key: _qrKey,
child: SizedBox(
width: width + 20,
height: width + 20,
child: QR(
data: data,
size: width,
),
),
),
),
const SizedBox(
height: 12,
),
Center(
child: SizedBox(
width: width,
child: TextButton(
onPressed: () async {
// await _capturePng(true);
Navigator.of(context).pop();
},
style: Theme.of(context)
.extension<StackColors>()!
.getSecondaryEnabledButtonStyle(
context,
),
child: Text(
"Cancel",
style: STextStyles.button(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark,
),
),
),
),
),
],
),
);
},
);
},
child: Text(
"Show QR Code",
style: STextStyles.button(context),
),
),
],
),
),
),
),
),

View file

@ -0,0 +1,221 @@
/*
* 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 'dart:async';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../../../notifications/show_flush_bar.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/assets.dart';
import '../../../../utilities/clipboard_interface.dart';
import '../../../../utilities/constants.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../../../widgets/desktop/primary_button.dart';
import '../../../../widgets/detail_item.dart';
import '../../../../widgets/qr.dart';
import '../../../../widgets/rounded_white_container.dart';
class WalletXPrivs extends ConsumerStatefulWidget {
const WalletXPrivs({
super.key,
required this.xprivData,
required this.walletId,
this.clipboardInterface = const ClipboardWrapper(),
});
final ({List<XPriv> xprivs, String fingerprint}) xprivData;
final String walletId;
final ClipboardInterface clipboardInterface;
@override
ConsumerState<WalletXPrivs> createState() => WalletXPrivsState();
}
class WalletXPrivsState extends ConsumerState<WalletXPrivs> {
late String _currentDropDownValue;
String _current(String key) =>
widget.xprivData.xprivs.firstWhere((e) => e.path == key).xpriv;
Future<void> _copy() async {
await widget.clipboardInterface.setData(
ClipboardData(text: _current(_currentDropDownValue)),
);
if (mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
),
);
}
}
@override
void initState() {
_currentDropDownValue = widget.xprivData.xprivs.first.path;
super.initState();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: Util.isDesktop
? const EdgeInsets.symmetric(horizontal: 20)
: EdgeInsets.zero,
child: Column(
mainAxisSize: Util.isDesktop ? MainAxisSize.min : MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: Util.isDesktop ? 12 : 16,
),
DetailItem(
title: "Master fingerprint",
detail: widget.xprivData.fingerprint,
horizontal: true,
borderColor: Util.isDesktop
? Theme.of(context).extension<StackColors>()!.textFieldDefaultBG
: null,
),
SizedBox(
height: Util.isDesktop ? 12 : 16,
),
DetailItemBase(
horizontal: true,
borderColor: Util.isDesktop
? Theme.of(context).extension<StackColors>()!.textFieldDefaultBG
: null,
title: Text(
"Derivation",
style: STextStyles.itemSubtitle(context),
),
detail: SizedBox(
width: Util.isDesktop ? 200 : 170,
child: DropdownButtonHideUnderline(
child: DropdownButton2<String>(
value: _currentDropDownValue,
items: [
...widget.xprivData.xprivs.map(
(e) => DropdownMenuItem(
value: e.path,
child: Text(
e.path,
style: STextStyles.w500_14(context),
),
),
),
],
onChanged: (value) {
if (value is String) {
setState(() {
_currentDropDownValue = value;
});
}
},
isExpanded: true,
buttonStyleData: ButtonStyleData(
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
iconStyleData: IconStyleData(
icon: Padding(
padding: const EdgeInsets.only(right: 10),
child: SvgPicture.asset(
Assets.svg.chevronDown,
width: 12,
height: 6,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveSearchIconRight,
),
),
),
dropdownStyleData: DropdownStyleData(
offset: const Offset(0, -10),
elevation: 0,
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
menuItemStyleData: const MenuItemStyleData(
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
),
),
),
),
SizedBox(
height: Util.isDesktop ? 12 : 16,
),
QR(
data: _current(_currentDropDownValue),
size:
Util.isDesktop ? 256 : MediaQuery.of(context).size.width / 1.5,
),
SizedBox(
height: Util.isDesktop ? 12 : 16,
),
RoundedWhiteContainer(
borderColor: Util.isDesktop
? Theme.of(context).extension<StackColors>()!.textFieldDefaultBG
: null,
child: SelectableText(
_current(_currentDropDownValue),
style: STextStyles.w500_14(context),
),
),
SizedBox(
height: Util.isDesktop ? 12 : 16,
),
if (!Util.isDesktop) const Spacer(),
Row(
children: [
if (Util.isDesktop) const Spacer(),
if (Util.isDesktop)
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
label: "Copy",
onPressed: _copy,
),
),
],
),
],
),
);
}
}

View file

@ -35,6 +35,7 @@ import '../../../wallets/crypto_currency/intermediate/frost_currency.dart';
import '../../../wallets/crypto_currency/intermediate/nano_currency.dart';
import '../../../wallets/wallet/impl/bitcoin_frost_wallet.dart';
import '../../../wallets/wallet/impl/epiccash_wallet.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
import '../../../widgets/background.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
@ -95,9 +96,9 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
void initState() {
walletId = widget.walletId;
coin = widget.coin;
// TODO: [prio=low] xpubs
// xPubEnabled = ref.read(pWallets).getWallet(walletId).hasXPub;
xPubEnabled = false;
xPubEnabled =
ref.read(pWallets).getWallet(walletId) is ExtendedKeysInterface;
xpub = "";
_currentSyncStatus = widget.initialSyncStatus;
@ -260,6 +261,10 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
// TODO: [prio=med] take wallets that don't have a mnemonic into account
({
List<XPriv> xprivs,
String fingerprint
})? xprivData;
List<String>? mnemonic;
({
String myName,
@ -301,6 +306,10 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
await wallet.getMnemonicAsWords();
}
if (wallet is ExtendedKeysInterface) {
xprivData = await wallet.getXPrivs();
}
if (context.mounted) {
await Navigator.push(
context,
@ -314,6 +323,7 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
mnemonic: mnemonic ?? [],
frostWalletData:
frostWalletData,
xprivData: xprivData,
),
showBackButton: true,
routeOnSuccess:
@ -373,11 +383,30 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
return SettingsListButton(
iconAssetName: Assets.svg.eye,
title: "Wallet xPub",
onPressed: () {
Navigator.of(context).pushNamed(
XPubView.routeName,
arguments: widget.walletId,
onPressed: () async {
final xpubData = await showLoading(
delay: const Duration(
milliseconds: 800,
),
whileFuture: (ref
.read(pWallets)
.getWallet(walletId)
as ExtendedKeysInterface)
.getXPubs(),
context: context,
message: "Loading xpubs",
rootNavigator: Util.isDesktop,
);
if (context.mounted) {
await Navigator.of(context)
.pushNamed(
XPubView.routeName,
arguments: (
widget.walletId,
xpubData
),
);
}
},
);
},

View file

@ -13,80 +13,45 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../../../notifications/show_flush_bar.dart';
import '../../../../providers/global/wallets_provider.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/assets.dart';
import '../../../../utilities/clipboard_interface.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.dart';
import '../../../../wallets/wallet/wallet.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../../../widgets/background.dart';
import '../../../../widgets/conditional_parent.dart';
import '../../../../widgets/custom_buttons/app_bar_icon_button.dart';
import '../../../../widgets/custom_tab_view.dart';
import '../../../../widgets/desktop/desktop_dialog.dart';
import '../../../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../../../widgets/desktop/primary_button.dart';
import '../../../../widgets/desktop/secondary_button.dart';
import '../../../../widgets/loading_indicator.dart';
import '../../../../widgets/detail_item.dart';
import '../../../../widgets/qr.dart';
import '../../../../widgets/rounded_white_container.dart';
class XPubView extends ConsumerStatefulWidget {
class XPubView extends ConsumerWidget {
const XPubView({
super.key,
required this.walletId,
required this.xpubData,
this.clipboardInterface = const ClipboardWrapper(),
});
final String walletId;
final ClipboardInterface clipboardInterface;
final ({List<XPub> xpubs, String fingerprint}) xpubData;
static const String routeName = "/xpub";
@override
ConsumerState<XPubView> createState() => _XPubViewState();
}
class _XPubViewState extends ConsumerState<XPubView> {
Widget build(BuildContext context, WidgetRef ref) {
final bool isDesktop = Util.isDesktop;
late ClipboardInterface _clipboardInterface;
late final Wallet wallet;
String? xpub;
@override
void initState() {
_clipboardInterface = widget.clipboardInterface;
wallet = ref.read(pWallets).getWallet(widget.walletId);
super.initState();
}
@override
void dispose() {
super.dispose();
}
Future<void> _copy() async {
await _clipboardInterface.setData(ClipboardData(text: xpub!));
if (mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
),
);
}
}
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: !isDesktop,
builder: (child) => Background(
@ -100,35 +65,9 @@ class _XPubViewState extends ConsumerState<XPubView> {
},
),
title: Text(
"Wallet xPub",
"Wallet xpub(s)",
style: STextStyles.navBarTitle(context),
),
actions: [
Padding(
padding: const EdgeInsets.all(10),
child: AspectRatio(
aspectRatio: 1,
child: AppBarIconButton(
color:
Theme.of(context).extension<StackColors>()!.background,
shadows: const [],
icon: SvgPicture.asset(
Assets.svg.copy,
width: 24,
height: 24,
color: Theme.of(context)
.extension<StackColors>()!
.topNavIconPrimary,
),
onPressed: () {
if (xpub != null) {
_copy();
}
},
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.only(
@ -136,7 +75,7 @@ class _XPubViewState extends ConsumerState<XPubView> {
left: 16,
right: 16,
),
child: child,
child: SingleChildScrollView(child: child),
),
),
),
@ -146,6 +85,7 @@ class _XPubViewState extends ConsumerState<XPubView> {
maxWidth: 600,
maxHeight: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -155,7 +95,7 @@ class _XPubViewState extends ConsumerState<XPubView> {
left: 32,
),
child: Text(
"${wallet.info.name} xPub",
"${ref.watch(pWalletName(walletId))} xpub(s)",
style: STextStyles.desktopH2(context),
),
),
@ -167,62 +107,40 @@ class _XPubViewState extends ConsumerState<XPubView> {
),
],
),
AnimatedSize(
duration: const Duration(
milliseconds: 150,
),
Flexible(
child: Padding(
padding: const EdgeInsets.fromLTRB(32, 0, 32, 32),
child: SingleChildScrollView(
child: child,
),
),
),
],
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (isDesktop) const SizedBox(height: 44),
ConditionalParent(
condition: !isDesktop,
builder: (child) => Expanded(
child: child,
if (isDesktop) const SizedBox(height: 16),
DetailItem(
title: "Master fingerprint",
detail: xpubData.fingerprint,
horizontal: true,
),
child: FutureBuilder(
future: Future(() => "fixme"),
// future: wallet.xpub,
builder: (context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {
xpub = snapshot.data!;
}
const height = 600.0;
Widget child;
if (xpub == null) {
child = const SizedBox(
key: Key("loadingXPUB"),
height: height,
child: Center(
child: LoadingIndicator(
width: 100,
if (isDesktop) const SizedBox(height: 16),
CustomTabView(
titles: xpubData.xpubs.map((e) => e.path).toList(),
children: xpubData.xpubs
.map(
(e) => Padding(
padding: const EdgeInsets.only(top: 16),
child: _XPub(
xpub: e.xpub,
derivation: e.path,
),
),
);
} else {
child = _XPub(
xpub: xpub!,
height: height,
);
}
return AnimatedSwitcher(
duration: const Duration(
milliseconds: 200,
),
child: child,
);
},
),
)
.toList(),
),
],
),
@ -235,22 +153,25 @@ class _XPub extends StatelessWidget {
const _XPub({
super.key,
required this.xpub,
required this.height,
required this.derivation,
this.clipboardInterface = const ClipboardWrapper(),
});
final String xpub;
final double height;
final String derivation;
final ClipboardInterface clipboardInterface;
@override
Widget build(BuildContext context) {
final bool isDesktop = Util.isDesktop;
return SizedBox(
height: isDesktop ? height : double.infinity,
child: Column(
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 25,
),
ConditionalParent(
condition: !isDesktop,
builder: (child) => RoundedWhiteContainer(
@ -311,9 +232,7 @@ class _XPub extends StatelessWidget {
),
],
),
if (!isDesktop) const Spacer(),
],
),
);
}
}

View file

@ -11,6 +11,7 @@ import '../../utilities/assets.dart';
import '../../utilities/text_styles.dart';
import '../../utilities/util.dart';
import '../../wallets/isar/providers/wallet_info_provider.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
import '../../widgets/background.dart';
import '../../widgets/conditional_parent.dart';
@ -264,7 +265,12 @@ class _FiroRescanRecoveryErrorViewState
if (wallet is MnemonicInterface) {
final mnemonic = await wallet.getMnemonicAsWords();
if (mounted) {
({List<XPriv> xprivs, String fingerprint})? xprivData;
if (wallet is ExtendedKeysInterface) {
xprivData = await wallet.getXPrivs();
}
if (context.mounted) {
await Navigator.push(
context,
RouteGenerator.getRoute(
@ -274,6 +280,7 @@ class _FiroRescanRecoveryErrorViewState
routeOnSuccessArguments: (
walletId: widget.walletId,
mnemonic: mnemonic,
xprivData: xprivData,
),
showBackButton: true,
routeOnSuccess: WalletBackupView.routeName,

View file

@ -15,8 +15,7 @@ import 'package:tuple/tuple.dart';
import '../../../../app_config.dart';
import '../../../../models/contact_address_entry.dart';
import '../../../../providers/exchange/exchange_send_from_wallet_id_provider.dart';
import '../../../../providers/global/wallets_provider.dart';
import '../../../../providers/providers.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/clipboard_interface.dart';
import '../../../../utilities/constants.dart';
@ -88,7 +87,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
}
widget.enableNextChanged.call(
_toController.text.isNotEmpty && _refundController.text.isNotEmpty,
_next(),
);
}
@ -120,7 +119,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
Logging.instance.log("$e\n$s", level: LogLevel.Info);
}
widget.enableNextChanged.call(
_toController.text.isNotEmpty && _refundController.text.isNotEmpty,
_next(),
);
}
@ -167,7 +166,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
_toController.text = entry.address;
ref.read(desktopExchangeModelProvider)!.recipientAddress = entry.address;
widget.enableNextChanged.call(
_toController.text.isNotEmpty && _refundController.text.isNotEmpty,
_next(),
);
}
}
@ -215,11 +214,21 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
_refundController.text = entry.address;
ref.read(desktopExchangeModelProvider)!.refundAddress = entry.address;
widget.enableNextChanged.call(
_toController.text.isNotEmpty && _refundController.text.isNotEmpty,
_next(),
);
}
}
bool _next() {
if (doesRefundAddress) {
return _toController.text.isNotEmpty && _refundController.text.isNotEmpty;
} else {
return _toController.text.isNotEmpty;
}
}
late final bool doesRefundAddress;
@override
void initState() {
clipboard = widget.clipboard;
@ -230,6 +239,13 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
_toFocusNode = FocusNode();
_refundFocusNode = FocusNode();
doesRefundAddress = ref.read(efExchangeProvider).supportsRefundAddress;
if (!doesRefundAddress) {
// hack: set to empty to not throw null unwrap error later
ref.read(desktopExchangeModelProvider)!.refundAddress = "";
}
final tuple = ref.read(exchangeSendFromWalletIdStateProvider.state).state;
if (tuple != null) {
if (ref.read(desktopExchangeModelProvider)!.receiveTicker.toLowerCase() ==
@ -243,7 +259,8 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
ref.read(desktopExchangeModelProvider)!.recipientAddress =
_toController.text;
} else {
if (ref.read(desktopExchangeModelProvider)!.sendTicker.toUpperCase() ==
if (doesRefundAddress &&
ref.read(desktopExchangeModelProvider)!.sendTicker.toUpperCase() ==
tuple.item2.ticker.toUpperCase()) {
_refundController.text = ref
.read(pWallets)
@ -341,8 +358,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
style: STextStyles.field(context),
onChanged: (value) {
widget.enableNextChanged.call(
_toController.text.isNotEmpty &&
_refundController.text.isNotEmpty,
_next(),
);
},
decoration: standardInputDecoration(
@ -376,8 +392,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
.read(desktopExchangeModelProvider)!
.recipientAddress = _toController.text;
widget.enableNextChanged.call(
_toController.text.isNotEmpty &&
_refundController.text.isNotEmpty,
_next(),
);
},
child: const XIcon(),
@ -397,8 +412,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
.read(desktopExchangeModelProvider)!
.recipientAddress = _toController.text;
widget.enableNextChanged.call(
_toController.text.isNotEmpty &&
_refundController.text.isNotEmpty,
_next(),
);
}
},
@ -435,9 +449,11 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
style: STextStyles.desktopTextExtraExtraSmall(context),
),
),
if (doesRefundAddress)
const SizedBox(
height: 24,
),
if (doesRefundAddress)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -461,9 +477,11 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
),
],
),
if (doesRefundAddress)
const SizedBox(
height: 10,
),
if (doesRefundAddress)
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
@ -487,8 +505,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
style: STextStyles.field(context),
onChanged: (value) {
widget.enableNextChanged.call(
_toController.text.isNotEmpty &&
_refundController.text.isNotEmpty,
_next(),
);
},
decoration: standardInputDecoration(
@ -523,8 +540,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
.refundAddress = _refundController.text;
widget.enableNextChanged.call(
_toController.text.isNotEmpty &&
_refundController.text.isNotEmpty,
_next(),
);
},
child: const XIcon(),
@ -546,8 +562,7 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
.refundAddress = _refundController.text;
widget.enableNextChanged.call(
_toController.text.isNotEmpty &&
_refundController.text.isNotEmpty,
_next(),
);
}
},
@ -574,9 +589,11 @@ class _DesktopStep2State extends ConsumerState<DesktopStep2> {
),
),
),
if (doesRefundAddress)
const SizedBox(
height: 10,
),
if (doesRefundAddress)
RoundedWhiteContainer(
borderColor: Theme.of(context).extension<StackColors>()!.background,
child: Text(

View file

@ -10,13 +10,14 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../step_scaffold.dart';
import 'desktop_step_item.dart';
import '../../../../providers/providers.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/enums/exchange_rate_type_enum.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../widgets/rounded_white_container.dart';
import '../step_scaffold.dart';
import 'desktop_step_item.dart';
class DesktopStep3 extends ConsumerStatefulWidget {
const DesktopStep3({
@ -97,10 +98,12 @@ class _DesktopStep3State extends ConsumerState<DesktopStep3> {
) ??
"Error",
),
if (ref.watch(efExchangeProvider).supportsRefundAddress)
Container(
height: 1,
color: Theme.of(context).extension<StackColors>()!.background,
),
if (ref.watch(efExchangeProvider).supportsRefundAddress)
DesktopStepItem(
vertical: true,
label:

View file

@ -29,11 +29,15 @@ import '../../../../utilities/address_utils.dart';
import '../../../../utilities/assets.dart';
import '../../../../utilities/clipboard_interface.dart';
import '../../../../utilities/constants.dart';
import '../../../../utilities/enums/derive_path_type_enum.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.dart';
import '../../../../wallets/crypto_currency/crypto_currency.dart';
import '../../../../wallets/isar/providers/eth/current_token_wallet_provider.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../wallets/wallet/impl/bitcoin_wallet.dart';
import '../../../../wallets/wallet/intermediate/bip39_hd_wallet.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/bcash_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/multi_address_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import '../../../../widgets/conditional_parent.dart';
@ -65,10 +69,13 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
late final String walletId;
late final ClipboardInterface clipboard;
late final bool supportsSpark;
late final bool showMultiType;
String? _sparkAddress;
String? _qrcodeContent;
bool _showSparkAddress = true;
int _currentIndex = 0;
final List<AddressType> _walletAddressTypes = [];
final Map<AddressType, String> _addressMap = {};
final Map<AddressType, StreamSubscription<Address?>> _addressSubMap = {};
Future<void> generateNewAddress() async {
final wallet = ref.read(pWallets).getWallet(walletId);
@ -95,12 +102,31 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
),
);
final Address? address;
if (wallet is Bip39HDWallet && wallet is! BCashInterface) {
final type = DerivePathType.values.firstWhere(
(e) => e.getAddressType() == _walletAddressTypes[_currentIndex],
);
address = await wallet.generateNextReceivingAddress(
derivePathType: type,
);
await ref.read(mainDBProvider).isar.writeTxn(() async {
await ref.read(mainDBProvider).isar.addresses.put(address!);
});
} else {
await wallet.generateNewReceivingAddress();
address = null;
}
shouldPop = true;
if (mounted) {
Navigator.of(context, rootNavigator: true).pop();
setState(() {
_addressMap[_walletAddressTypes[_currentIndex]] =
address?.value ?? ref.read(pWalletReceivingAddress(walletId));
});
}
}
}
@ -139,14 +165,12 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
if (mounted) {
Navigator.of(context, rootNavigator: true).pop();
if (_sparkAddress != address.value) {
setState(() {
_sparkAddress = address.value;
_addressMap[AddressType.spark] = address.value;
});
}
}
}
}
StreamSubscription<Address?>? _streamSub;
@ -155,17 +179,42 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
walletId = widget.walletId;
coin = ref.read(pWalletInfo(walletId)).coin;
clipboard = widget.clipboard;
final wallet = ref.read(pWallets).getWallet(walletId);
supportsSpark = ref.read(pWallets).getWallet(walletId) is SparkInterface;
showMultiType = supportsSpark ||
ref.read(pWallets).getWallet(walletId) is MultiAddressInterface;
_walletAddressTypes.add(wallet.info.mainAddressType);
if (showMultiType) {
if (supportsSpark) {
_streamSub = ref
_walletAddressTypes.insert(0, AddressType.spark);
} else {
_walletAddressTypes.addAll(
(wallet as Bip39HDWallet)
.supportedAddressTypes
.where((e) => e != wallet.info.mainAddressType),
);
}
}
if (_walletAddressTypes.length > 1 && wallet is BitcoinWallet) {
_walletAddressTypes.removeWhere((e) => e == AddressType.p2pkh);
}
_addressMap[_walletAddressTypes[_currentIndex]] =
ref.read(pWalletReceivingAddress(walletId));
if (showMultiType) {
for (final type in _walletAddressTypes) {
_addressSubMap[type] = ref
.read(mainDBProvider)
.isar
.addresses
.where()
.walletIdEqualTo(walletId)
.filter()
.typeEqualTo(AddressType.spark)
.typeEqualTo(type)
.sortByDerivationIndexDesc()
.findFirst()
.asStream()
@ -173,12 +222,14 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
setState(() {
_sparkAddress = event?.value;
_addressMap[type] =
event?.value ?? _addressMap[type] ?? "[No address yet]";
});
}
});
});
}
}
super.initState();
}
@ -193,47 +244,41 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
Widget build(BuildContext context) {
debugPrint("BUILD: $runtimeType");
if (supportsSpark) {
if (_showSparkAddress) {
_qrcodeContent = _sparkAddress;
final String address;
if (showMultiType) {
address = _addressMap[_walletAddressTypes[_currentIndex]]!;
} else {
_qrcodeContent = ref.watch(pWalletReceivingAddress(walletId));
}
} else {
_qrcodeContent = ref.watch(pWalletReceivingAddress(walletId));
address = ref.watch(pWalletReceivingAddress(walletId));
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ConditionalParent(
condition: supportsSpark,
condition: showMultiType,
builder: (child) => Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DropdownButtonHideUnderline(
child: DropdownButton2<bool>(
value: _showSparkAddress,
child: DropdownButton2<int>(
value: _currentIndex,
items: [
for (int i = 0; i < _walletAddressTypes.length; i++)
DropdownMenuItem(
value: true,
value: i,
child: Text(
"Spark address",
style: STextStyles.desktopTextMedium(context),
),
),
DropdownMenuItem(
value: false,
child: Text(
"Transparent address",
style: STextStyles.desktopTextMedium(context),
supportsSpark &&
_walletAddressTypes[i] == AddressType.p2pkh
? "Transparent address"
: "${_walletAddressTypes[i].readableName} address",
style: STextStyles.w500_14(context),
),
),
],
onChanged: (value) {
if (value is bool && value != _showSparkAddress) {
if (value != null && value != _currentIndex) {
setState(() {
_showSparkAddress = value;
_currentIndex = value;
});
}
},
@ -251,6 +296,16 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
),
),
),
buttonStyleData: ButtonStyleData(
decoration: BoxDecoration(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
),
dropdownStyleData: DropdownStyleData(
offset: const Offset(0, -10),
elevation: 0,
@ -274,95 +329,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
const SizedBox(
height: 12,
),
if (_showSparkAddress)
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
clipboard.setData(
ClipboardData(text: _sparkAddress ?? "Error"),
);
showFloatingFlushBar(
type: FlushBarType.info,
message: "Copied to clipboard",
iconAsset: Assets.svg.copy,
context: context,
);
},
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context)
.extension<StackColors>()!
.backgroundAppBar,
width: 1,
),
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
child: RoundedWhiteContainer(
child: Column(
children: [
Row(
children: [
Text(
"Your ${widget.contractAddress == null ? coin.ticker : ref.watch(
pCurrentTokenWallet.select(
(value) => value!.tokenContract.symbol,
),
)} SPARK address",
style: STextStyles.itemSubtitle(context),
),
const Spacer(),
Row(
children: [
SvgPicture.asset(
Assets.svg.copy,
width: 15,
height: 15,
color: Theme.of(context)
.extension<StackColors>()!
.infoItemIcons,
),
const SizedBox(
width: 4,
),
Text(
"Copy",
style: STextStyles.link2(context),
),
],
),
],
),
const SizedBox(
height: 8,
),
Row(
children: [
Expanded(
child: Text(
_sparkAddress ?? "Error",
style:
STextStyles.desktopTextExtraExtraSmall(
context,
).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
),
),
),
],
),
],
),
),
),
),
),
if (!_showSparkAddress) child,
child,
],
),
child: MouseRegion(
@ -435,7 +402,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
children: [
Expanded(
child: Text(
ref.watch(pWalletReceivingAddress(walletId)),
address,
style: STextStyles.desktopTextExtraExtraSmall(
context,
).copyWith(
@ -467,7 +434,8 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
supportsSpark)
SecondaryButton(
buttonHeight: ButtonHeight.l,
onPressed: supportsSpark && _showSparkAddress
onPressed: supportsSpark &&
_walletAddressTypes[_currentIndex] == AddressType.spark
? generateNewSparkAddress
: generateNewAddress,
label: "Generate new address",
@ -479,7 +447,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
child: QR(
data: AddressUtils.buildUriString(
coin,
_qrcodeContent ?? "",
address,
{},
),
size: 200,
@ -518,7 +486,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
RouteGenerator.generateRoute(
RouteSettings(
name: GenerateUriQrCodeView.routeName,
arguments: Tuple2(coin, _qrcodeContent ?? ""),
arguments: Tuple2(coin, address),
),
),
],
@ -535,7 +503,7 @@ class _DesktopReceiveState extends ConsumerState<DesktopReceive> {
shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
builder: (_) => GenerateUriQrCodeView(
coin: coin,
receivingAddress: _qrcodeContent ?? "",
receivingAddress: address,
),
settings: const RouteSettings(
name: GenerateUriQrCodeView.routeName,

View file

@ -22,6 +22,7 @@ import '../../../../utilities/assets.dart';
import '../../../../utilities/constants.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../wallets/wallet/impl/bitcoin_frost_wallet.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/mnemonic_interface.dart';
import '../../../../widgets/desktop/desktop_dialog.dart';
import '../../../../widgets/desktop/desktop_dialog_close_button.dart';
@ -84,6 +85,7 @@ class _UnlockWalletKeysDesktopState
final wallet = ref.read(pWallets).getWallet(widget.walletId);
({String keys, String config})? frostData;
List<String>? words;
({List<XPriv> xprivs, String fingerprint})? xprivData;
// TODO: [prio=low] handle wallets that don't have a mnemonic
// All wallets currently are mnemonic based
@ -100,12 +102,18 @@ class _UnlockWalletKeysDesktopState
words = await wallet.getMnemonicAsWords();
}
if (wallet is ExtendedKeysInterface) {
xprivData = await wallet.getXPrivs();
}
if (mounted) {
await Navigator.of(context).pushReplacementNamed(
WalletKeysDesktopPopup.routeName,
arguments: (
mnemonic: words ?? [],
walletId: widget.walletId,
frostData: frostData,
xprivData: xprivData,
),
);
}
@ -319,6 +327,10 @@ class _UnlockWalletKeysDesktopState
({String keys, String config})? frostData;
List<String>? words;
({
List<XPriv> xprivs,
String fingerprint
})? xprivData;
final wallet =
ref.read(pWallets).getWallet(widget.walletId);
@ -338,13 +350,19 @@ class _UnlockWalletKeysDesktopState
words = await wallet.getMnemonicAsWords();
}
if (wallet is ExtendedKeysInterface) {
xprivData = await wallet.getXPrivs();
}
if (mounted) {
await Navigator.of(context)
.pushReplacementNamed(
WalletKeysDesktopPopup.routeName,
arguments: (
mnemonic: words ?? [],
walletId: widget.walletId,
frostData: frostData,
xprivData: xprivData,
),
);
}

View file

@ -12,15 +12,19 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../notifications/show_flush_bar.dart';
import '../../../../pages/add_wallet_views/new_wallet_recovery_phrase_view/sub_widgets/mnemonic_table.dart';
import '../../../../pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_xprivs.dart';
import '../../../../pages/wallet_view/transaction_views/transaction_details_view.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/address_utils.dart';
import '../../../../utilities/assets.dart';
import '../../../../utilities/clipboard_interface.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../../../widgets/custom_tab_view.dart';
import '../../../../widgets/desktop/desktop_dialog.dart';
import '../../../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../../../widgets/desktop/primary_button.dart';
@ -28,22 +32,26 @@ import '../../../../widgets/desktop/secondary_button.dart';
import '../../../../widgets/rounded_white_container.dart';
import 'qr_code_desktop_popup_content.dart';
class WalletKeysDesktopPopup extends StatelessWidget {
class WalletKeysDesktopPopup extends ConsumerWidget {
const WalletKeysDesktopPopup({
super.key,
required this.words,
required this.walletId,
this.frostData,
this.clipboardInterface = const ClipboardWrapper(),
this.xprivData,
});
final List<String> words;
final String walletId;
final ({String keys, String config})? frostData;
final ClipboardInterface clipboardInterface;
final ({List<XPriv> xprivs, String fingerprint})? xprivData;
static const String routeName = "walletKeysDesktopPopup";
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
return DesktopDialog(
maxWidth: 614,
maxHeight: double.infinity,
@ -69,7 +77,7 @@ class WalletKeysDesktopPopup extends StatelessWidget {
],
),
const SizedBox(
height: 28,
height: 6,
),
frostData != null
? Column(
@ -168,7 +176,47 @@ class WalletKeysDesktopPopup extends StatelessWidget {
),
],
)
: Column(
: xprivData != null
? CustomTabView(
titles: const ["Mnemonic", "XPriv(s)"],
children: [
Padding(
padding: const EdgeInsets.only(top: 16),
child: _Mnemonic(
words: words,
),
),
WalletXPrivs(
xprivData: xprivData!,
walletId: walletId,
),
],
)
: _Mnemonic(
words: words,
),
const SizedBox(
height: 32,
),
],
),
);
}
}
class _Mnemonic extends StatelessWidget {
const _Mnemonic({
super.key,
required this.words,
this.clipboardInterface = const ClipboardWrapper(),
});
final List<String> words;
final ClipboardInterface clipboardInterface;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
"Recovery phrase",
@ -183,9 +231,10 @@ class WalletKeysDesktopPopup extends StatelessWidget {
horizontal: 32,
),
child: Text(
"Please write down your recovery phrase in the correct order and save it to keep your funds secure. You will also be asked to verify the words on the next screen.",
style:
STextStyles.desktopTextExtraExtraSmall(context),
"Please write down your recovery phrase in the correct order and "
"save it to keep your funds secure. You will also be asked to"
" verify the words on the next screen.",
style: STextStyles.desktopTextExtraExtraSmall(context),
textAlign: TextAlign.center,
),
),
@ -200,9 +249,8 @@ class WalletKeysDesktopPopup extends StatelessWidget {
child: MnemonicTable(
words: words,
isDesktop: true,
itemBorderColor: Theme.of(context)
.extension<StackColors>()!
.buttonBackSecondary,
itemBorderColor:
Theme.of(context).extension<StackColors>()!.buttonBackSecondary,
),
),
const SizedBox(
@ -219,8 +267,7 @@ class WalletKeysDesktopPopup extends StatelessWidget {
label: "Show QR code",
onPressed: () {
// TODO: address utils
final String value =
AddressUtils.encodeQRSeedData(words);
final String value = AddressUtils.encodeQRSeedData(words);
Navigator.of(context).pushNamed(
QRCodeDesktopPopupContent.routeName,
arguments: value,
@ -255,12 +302,6 @@ class WalletKeysDesktopPopup extends StatelessWidget {
),
),
],
),
const SizedBox(
height: 32,
),
],
),
);
}
}

View file

@ -18,15 +18,19 @@ import 'package:flutter_svg/svg.dart';
import '../../../../pages/settings_views/wallet_settings_view/frost_ms/frost_ms_options_view.dart';
import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart';
import '../../../../pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart';
import '../../../../providers/global/wallets_provider.dart';
import '../../../../route_generator.dart';
import '../../../../themes/stack_colors.dart';
import '../../../../utilities/assets.dart';
import '../../../../utilities/constants.dart';
import '../../../../utilities/show_loading.dart';
import '../../../../utilities/text_styles.dart';
import '../../../../utilities/util.dart';
import '../../../../wallets/crypto_currency/coins/firo.dart';
import '../../../../wallets/crypto_currency/intermediate/frost_currency.dart';
import '../../../../wallets/crypto_currency/intermediate/nano_currency.dart';
import '../../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../../wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import '../../../addresses/desktop_wallet_addresses_view.dart';
import '../../../lelantus_coins/lelantus_coins_view.dart';
import '../../../spark_coins/spark_coins_view.dart';
@ -61,7 +65,7 @@ enum _WalletOptions {
}
}
class WalletOptionsButton extends StatelessWidget {
class WalletOptionsButton extends ConsumerWidget {
const WalletOptionsButton({
super.key,
required this.walletId,
@ -70,7 +74,7 @@ class WalletOptionsButton extends StatelessWidget {
final String walletId;
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
return RawMaterialButton(
constraints: const BoxConstraints(
minHeight: 32,
@ -148,6 +152,17 @@ class WalletOptionsButton extends StatelessWidget {
}
break;
case _WalletOptions.showXpub:
final xpubData = await showLoading(
delay: const Duration(milliseconds: 800),
whileFuture: (ref.read(pWallets).getWallet(walletId)
as ExtendedKeysInterface)
.getXPubs(),
context: context,
message: "Loading xpubs",
rootNavigator: Util.isDesktop,
);
if (context.mounted) {
final result = await showDialog<bool?>(
context: context,
barrierDismissible: false,
@ -159,7 +174,7 @@ class WalletOptionsButton extends StatelessWidget {
RouteGenerator.generateRoute(
RouteSettings(
name: XPubView.routeName,
arguments: walletId,
arguments: (walletId, xpubData),
),
),
];
@ -172,6 +187,7 @@ class WalletOptionsButton extends StatelessWidget {
Navigator.of(context).pop();
}
}
}
break;
case _WalletOptions.changeRepresentative:
final result = await showDialog<bool?>(
@ -279,9 +295,8 @@ class WalletOptionsPopupMenu extends ConsumerWidget {
final firoDebug = kDebugMode && (coin is Firo);
// TODO: [prio=low]
// final bool xpubEnabled = manager.hasXPub;
final bool xpubEnabled = false;
final bool xpubEnabled =
ref.watch(pWallets).getWallet(walletId) is ExtendedKeysInterface;
final bool canChangeRep = coin is NanoCurrency;

View file

@ -0,0 +1,264 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import '../../notifications/show_flush_bar.dart';
import '../../providers/desktop/storage_crypto_handler_provider.dart';
import '../../themes/stack_colors.dart';
import '../../utilities/assets.dart';
import '../../utilities/constants.dart';
import '../../utilities/show_loading.dart';
import '../../utilities/text_styles.dart';
import '../../widgets/desktop/desktop_dialog.dart';
import '../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../widgets/desktop/primary_button.dart';
import '../../widgets/desktop/secondary_button.dart';
import '../../widgets/stack_text_field.dart';
class RequestDesktopAuthDialog extends ConsumerStatefulWidget {
const RequestDesktopAuthDialog({
super.key,
this.title,
});
final String? title;
@override
ConsumerState<RequestDesktopAuthDialog> createState() =>
_RequestDesktopAuthDialogState();
}
class _RequestDesktopAuthDialogState
extends ConsumerState<RequestDesktopAuthDialog> {
late final TextEditingController passwordController;
late final FocusNode passwordFocusNode;
bool continueEnabled = false;
bool hidePassword = true;
bool _lock = false;
Future<void> _auth() async {
if (_lock) {
return;
}
_lock = true;
try {
final verified = await showLoading(
whileFuture: ref
.read(storageCryptoHandlerProvider)
.verifyPassphrase(passwordController.text),
context: context,
message: "Checking...",
rootNavigator: true,
delay: const Duration(milliseconds: 1000),
);
if (verified == true) {
if (mounted) {
Navigator.of(context, rootNavigator: true).pop("verified success");
}
} else {
if (mounted) {
Navigator.of(context, rootNavigator: true).pop();
await Future<void>.delayed(const Duration(milliseconds: 300));
if (mounted) {
unawaited(
showFloatingFlushBar(
type: FlushBarType.warning,
message: "Invalid passphrase!",
context: context,
),
);
}
}
}
} finally {
_lock = false;
}
}
@override
void initState() {
passwordController = TextEditingController();
passwordFocusNode = FocusNode();
super.initState();
}
@override
void dispose() {
passwordController.dispose();
passwordFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return DesktopDialog(
maxWidth: 579,
maxHeight: double.infinity,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
DesktopDialogCloseButton(
onPressedOverride: Navigator.of(
context,
rootNavigator: true,
).pop,
),
],
),
const SizedBox(
height: 12,
),
SvgPicture.asset(
Assets.svg.keys,
width: 100,
height: 58,
),
const SizedBox(
height: 55,
),
if (widget.title != null)
Text(
widget.title!,
style: STextStyles.desktopH2(context),
),
if (widget.title != null)
const SizedBox(
height: 16,
),
Text(
"Enter your password",
style: STextStyles.desktopTextMedium(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textDark3,
),
),
const SizedBox(
height: 24,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
key: const Key("enterPasswordUnlockWalletKeysDesktopFieldKey"),
focusNode: passwordFocusNode,
controller: passwordController,
style: STextStyles.desktopTextMedium(context).copyWith(
height: 2,
),
obscureText: hidePassword,
enableSuggestions: false,
autocorrect: false,
autofocus: true,
onSubmitted: (_) {
if (continueEnabled) {
_auth();
}
},
decoration: standardInputDecoration(
"Enter password",
passwordFocusNode,
context,
).copyWith(
suffixIcon: UnconstrainedBox(
child: SizedBox(
height: 70,
child: Row(
children: [
GestureDetector(
key: const Key(
"enterUnlockWalletKeysDesktopFieldShowPasswordButtonKey",
),
onTap: () async {
setState(() {
hidePassword = !hidePassword;
});
},
child: Container(
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(1000),
),
height: 32,
width: 32,
child: Center(
child: SvgPicture.asset(
hidePassword
? Assets.svg.eye
: Assets.svg.eyeSlash,
color: Theme.of(context)
.extension<StackColors>()!
.textDark3,
width: 24,
height: 19,
),
),
),
),
const SizedBox(
width: 10,
),
],
),
),
),
),
onChanged: (newValue) {
setState(() {
continueEnabled = newValue.isNotEmpty;
});
},
),
),
),
const SizedBox(
height: 55,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
),
child: Row(
children: [
Expanded(
child: SecondaryButton(
label: "Cancel",
onPressed: Navigator.of(
context,
rootNavigator: true,
).pop,
),
),
const SizedBox(
width: 16,
),
Expanded(
child: PrimaryButton(
label: "Continue",
enabled: continueEnabled,
onPressed: continueEnabled ? _auth : null,
),
),
],
),
),
const SizedBox(
height: 32,
),
],
),
);
}
}

View file

@ -198,6 +198,7 @@ import 'wallets/crypto_currency/crypto_currency.dart';
import 'wallets/crypto_currency/intermediate/frost_currency.dart';
import 'wallets/models/tx_data.dart';
import 'wallets/wallet/wallet.dart';
import 'wallets/wallet/wallet_mixin_interfaces/extended_keys_interface.dart';
import 'widgets/choose_coin_view.dart';
import 'widgets/frost_scaffold.dart';
@ -908,11 +909,12 @@ class RouteGenerator {
);
case XPubView.routeName:
if (args is String) {
if (args is (String, ({List<XPub> xpubs, String fingerprint}))) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => XPubView(
walletId: args,
walletId: args.$1,
xpubData: args.$2,
),
settings: RouteSettings(
name: settings.name,
@ -1275,6 +1277,63 @@ class RouteGenerator {
name: settings.name,
),
);
} else if (args is ({
String walletId,
List<String> mnemonic,
({List<XPriv> xprivs, String fingerprint})? xprivData,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => WalletBackupView(
walletId: args.walletId,
mnemonic: args.mnemonic,
xprivData: args.xprivData,
),
settings: RouteSettings(
name: settings.name,
),
);
} else if (args is ({
String walletId,
List<String> mnemonic,
({List<XPriv> xprivs, String fingerprint})? xprivData,
({
String myName,
String config,
String keys,
({String config, String keys})? prevGen,
})? frostWalletData,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => WalletBackupView(
walletId: args.walletId,
mnemonic: args.mnemonic,
frostWalletData: args.frostWalletData,
xprivData: args.xprivData,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case MobileXPrivsView.routeName:
if (args is ({
String walletId,
({List<XPriv> xprivs, String fingerprint}) xprivData,
})) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => MobileXPrivsView(
walletId: args.walletId,
xprivData: args.xprivData,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
@ -2293,26 +2352,51 @@ class RouteGenerator {
case WalletKeysDesktopPopup.routeName:
if (args is ({
List<String> mnemonic,
String walletId,
({String keys, String config})? frostData
})) {
return FadePageRoute(
WalletKeysDesktopPopup(
words: args.mnemonic,
walletId: args.walletId,
frostData: args.frostData,
),
RouteSettings(
name: settings.name,
),
);
// return getRoute(
// shouldUseMaterialRoute: useMaterialPageRoute,
// builder: (_) => WalletKeysDesktopPopup(
// words: args,
// ),
// settings: RouteSettings(
// name: settings.name,
// ),
// );
} else if (args is ({
List<String> mnemonic,
String walletId,
({String keys, String config})? frostData,
({List<XPriv> xprivs, String fingerprint})? xprivData,
})) {
return FadePageRoute(
WalletKeysDesktopPopup(
words: args.mnemonic,
walletId: args.walletId,
frostData: args.frostData,
xprivData: args.xprivData,
),
RouteSettings(
name: settings.name,
),
);
} else if (args is ({
List<String> mnemonic,
String walletId,
({List<XPriv> xprivs, String fingerprint})? xprivData,
})) {
return FadePageRoute(
WalletKeysDesktopPopup(
words: args.mnemonic,
walletId: args.walletId,
xprivData: args.xprivData,
),
RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");

View file

@ -9,6 +9,7 @@
*/
import 'package:decimal/decimal.dart';
import '../../models/exchange/response_objects/estimate.dart';
import '../../models/exchange/response_objects/range.dart';
import '../../models/exchange/response_objects/trade.dart';
@ -17,6 +18,7 @@ import '../../models/isar/exchange_cache/pair.dart';
import 'change_now/change_now_exchange.dart';
import 'exchange_response.dart';
import 'majestic_bank/majestic_bank_exchange.dart';
import 'nanswap/nanswap_exchange.dart';
import 'simpleswap/simpleswap_exchange.dart';
import 'trocador/trocador_exchange.dart';
@ -33,6 +35,8 @@ abstract class Exchange {
return MajesticBankExchange.instance;
case TrocadorExchange.exchangeName:
return TrocadorExchange.instance;
case NanswapExchange.exchangeName:
return NanswapExchange.instance;
default:
final split = name.split(" ");
if (split.length >= 2) {
@ -45,6 +49,8 @@ abstract class Exchange {
String get name;
bool get supportsRefundAddress => true;
Future<ExchangeResponse<List<Currency>>> getAllCurrencies(bool fixedRate);
Future<ExchangeResponse<List<Currency>>> getPairedCurrencies(
@ -97,6 +103,7 @@ abstract class Exchange {
static List<Exchange> get exchangesWithTorSupport => [
MajesticBankExchange.instance,
TrocadorExchange.instance,
NanswapExchange.instance, // Maybe??
];
/// List of exchange names which support Tor.

View file

@ -23,6 +23,7 @@ import '../../utilities/prefs.dart';
import '../../utilities/stack_file_system.dart';
import 'change_now/change_now_exchange.dart';
import 'majestic_bank/majestic_bank_exchange.dart';
import 'nanswap/nanswap_exchange.dart';
import 'trocador/trocador_exchange.dart';
class ExchangeDataLoadingService {
@ -170,6 +171,7 @@ class ExchangeDataLoadingService {
final futures = [
loadMajesticBankCurrencies(),
loadTrocadorCurrencies(),
loadNanswapCurrencies(),
];
// If using Tor, don't load data for exchanges which don't support Tor.
@ -382,6 +384,31 @@ class ExchangeDataLoadingService {
}
}
Future<void> loadNanswapCurrencies() async {
if (_isar == null) {
await initDB();
}
final responseCurrencies =
await NanswapExchange.instance.getAllCurrencies(false);
if (responseCurrencies.value != null) {
await isar.writeTxn(() async {
final idsToDelete = await isar.currencies
.where()
.exchangeNameEqualTo(NanswapExchange.exchangeName)
.idProperty()
.findAll();
await isar.currencies.deleteAll(idsToDelete);
await isar.currencies.putAll(responseCurrencies.value!);
});
} else {
Logging.instance.log(
"loadNanswapCurrencies: $responseCurrencies",
level: LogLevel.Warning,
);
}
}
// Future<void> loadMajesticBankPairs() async {
// final exchange = MajesticBankExchange.instance;
//

View file

@ -46,6 +46,9 @@ class MajesticBankExchange extends Exchange {
"XMR": "Monero",
};
@override
bool get supportsRefundAddress => false;
@override
Future<ExchangeResponse<Trade>> createTrade({
required String from,

View file

@ -0,0 +1,43 @@
class NCurrency {
final String id;
final String ticker;
final String name;
final String image;
final String network;
final bool hasExternalId;
final bool feeLess;
NCurrency({
required this.id,
required this.ticker,
required this.name,
required this.image,
required this.network,
required this.hasExternalId,
required this.feeLess,
});
factory NCurrency.fromJson(Map<String, dynamic> json) {
return NCurrency(
id: json["id"] as String,
ticker: json['ticker'] as String,
name: json['name'] as String,
image: json['image'] as String,
network: json['network'] as String,
hasExternalId: json['hasExternalId'] as bool,
feeLess: json['feeless'] as bool,
);
}
@override
String toString() {
return 'NCurrency {'
'ticker: $ticker, '
'name: $name, '
'image: $image, '
'network: $network, '
'hasExternalId: $hasExternalId, '
'feeless: $feeLess'
'}';
}
}

View file

@ -0,0 +1,32 @@
class NEstimate {
final String from;
final String to;
final num amountFrom;
final num amountTo;
NEstimate({
required this.from,
required this.to,
required this.amountFrom,
required this.amountTo,
});
factory NEstimate.fromJson(Map<String, dynamic> json) {
return NEstimate(
from: json['from'] as String,
to: json['to'] as String,
amountFrom: json['amountFrom'] as num,
amountTo: json['amountTo'] as num,
);
}
@override
String toString() {
return 'NEstimate {'
'from: $from, '
'to: $to, '
'amountFrom: $amountFrom, '
'amountTo: $amountTo '
'}';
}
}

View file

@ -0,0 +1,81 @@
class NTrade {
final String id;
final String from;
final String to;
final num expectedAmountFrom;
final num expectedAmountTo;
final String payinAddress;
final String payoutAddress;
final String? payinExtraId;
final String? fullLink;
final String? status;
final String? payinHash;
final String? payoutHash;
final num? fromAmount;
final num? toAmount;
final String? fromNetwork;
final String? toNetwork;
NTrade({
required this.id,
required this.from,
required this.to,
required this.expectedAmountFrom,
required this.expectedAmountTo,
required this.payinAddress,
required this.payoutAddress,
this.payinExtraId,
this.fullLink,
this.status,
this.payinHash,
this.payoutHash,
this.fromAmount,
this.toAmount,
this.fromNetwork,
this.toNetwork,
});
factory NTrade.fromJson(Map<String, dynamic> json) {
return NTrade(
id: json['id'] as String,
from: json['from'] as String,
to: json['to'] as String,
expectedAmountFrom: num.parse(json['expectedAmountFrom'].toString()),
expectedAmountTo: json['expectedAmountTo'] as num,
payinAddress: json['payinAddress'] as String,
payoutAddress: json['payoutAddress'] as String,
fullLink: json['fullLink'] as String?,
payinExtraId: json['payinExtraId'] as String?,
status: json['status'] as String?,
payinHash: json['payinHash'] as String?,
payoutHash: json['payoutHash'] as String?,
fromAmount: json['fromAmount'] as num?,
toAmount: json['toAmount'] as num?,
fromNetwork: json['fromNetwork'] as String?,
toNetwork: json['toNetwork'] as String?,
);
}
@override
String toString() {
return 'NTrade {'
' id: $id, '
' from: $from, '
' to: $to, '
' expectedAmountFrom: $expectedAmountFrom, '
' expectedAmountTo: $expectedAmountTo, '
' payinAddress: $payinAddress, '
' payoutAddress: $payoutAddress, '
' fullLink: $fullLink, '
' payinExtraId: $payinExtraId, '
' status: $status, '
' payinHash: $payinHash, '
' payoutHash: $payoutHash '
' fromAmount: $fromAmount, '
' toAmount: $toAmount, '
' fromNetwork: $fromNetwork, '
' toNetwork: $toNetwork, '
'}';
}
}

View file

@ -0,0 +1,517 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../../../exceptions/exchange/exchange_exception.dart';
import '../../../external_api_keys.dart';
import '../../../networking/http.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/prefs.dart';
import '../../tor_service.dart';
import '../exchange_response.dart';
import 'api_response_models/n_currency.dart';
import 'api_response_models/n_estimate.dart';
import 'api_response_models/n_trade.dart';
class NanswapAPI {
NanswapAPI._();
static const authority = "api.nanswap.com";
static const version = "v1";
static NanswapAPI? _instance;
static NanswapAPI get instance => _instance ??= NanswapAPI._();
final _client = HTTP();
Uri _buildUri({required String endpoint, Map<String, String>? params}) {
return Uri.https(authority, "/$version/$endpoint", params);
}
Future<dynamic> _makeGetRequest(Uri uri) async {
int code = -1;
try {
final response = await _client.get(
url: uri,
headers: {
'Accept': 'application/json',
},
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null,
);
code = response.code;
final parsed = jsonDecode(response.body);
return parsed;
} catch (e, s) {
Logging.instance.log(
"NanswapAPI._makeRequest($uri) HTTP:$code threw: $e\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
Future<dynamic> _makePostRequest(
Uri uri,
Map<String, dynamic> body,
) async {
int code = -1;
try {
final response = await _client.post(
url: uri,
headers: {
'nanswap-api-key': kNanswapApiKey,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: jsonEncode(body),
proxyInfo: Prefs.instance.useTor
? TorService.sharedInstance.getProxyInfo()
: null,
);
code = response.code;
final data = response.body;
final parsed = jsonDecode(data);
return parsed;
} catch (e, s) {
Logging.instance.log(
"NanswapAPI._makePostRequest($uri) HTTP:$code threw: $e\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
// ============= API ===================================================
// GET List of supported currencies
// https://api.nanswap.com/v1/all-currencies
//
// Returns a Key => Value map of available currencies.
//
// The Key is the ticker, that can be used in the from and to params of the /get-estimate, /get-limit, /create-order.
//
// The Value is the currency info:
//
// name
//
// logo
//
// network Network of the crypto.
//
// hasExternalId Boolean. If the crypto require a memo/id.
//
// feeless Boolean. If crypto has 0 network fees.
//
// HEADERS
// Accept
//
// application/json
Future<ExchangeResponse<List<NCurrency>>> getSupportedCurrencies() async {
final uri = _buildUri(
endpoint: "all-currencies",
);
try {
final json = await _makeGetRequest(uri);
final List<NCurrency> result = [];
for (final key in (json as Map).keys) {
final _map = json[key] as Map;
_map["id"] = key;
result.add(
NCurrency.fromJson(
Map<String, dynamic>.from(_map),
),
);
}
return ExchangeResponse(value: result);
} catch (e, s) {
Logging.instance.log(
"Nanswap.getSupportedCurrencies() exception: $e\n$s",
level: LogLevel.Error,
);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
// GET Get estimate
// https://api.nanswap.com/v1/get-estimate?from=XNO&to=BAN&amount=10
//
// Get estimated exchange amount.
// HEADERS
// Accept
//
// application/json
// PARAMS
//
// from
// XNO
// Ticker from
//
// to
// BAN
// Ticker to
//
// amount
// 10
// Amount from
Future<ExchangeResponse<NEstimate>> getEstimate({
required String amountFrom,
required String from,
required String to,
}) async {
final uri = _buildUri(
endpoint: "get-estimate",
params: {
"to": to.toUpperCase(),
"from": from.toUpperCase(),
"amount": amountFrom,
},
);
try {
final json = await _makeGetRequest(uri);
try {
final map = Map<String, dynamic>.from(json as Map);
// not sure why the api responds without these sometimes...
map["to"] ??= to.toUpperCase();
map["from"] ??= from.toUpperCase();
return ExchangeResponse(
value: NEstimate.fromJson(
map,
),
);
} catch (_) {
Logging.instance.log(
"Nanswap.getEstimate() response was: $json",
level: LogLevel.Error,
);
rethrow;
}
} catch (e, s) {
Logging.instance.log(
"Nanswap.getEstimate() exception: $e\n$s",
level: LogLevel.Error,
);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
// GET Get estimate reverse
// https://api.nanswap.com/v1/get-estimate-reverse?from=XNO&to=BAN&amount=1650
//
// (Only available for feeless crypto)
//
// Get estimate but reversed, it takes toAmount and returns the fromAmount
// estimation. Allows to let user input directly their toAmount wanted.
// HEADERS
// Accept
//
// application/json
// PARAMS
// from
// XNO
// Ticker from
//
// to
// BAN
// Ticker to
//
// amount
// 1650
// Amount to
Future<ExchangeResponse<NEstimate>> getEstimateReversed({
required String amountTo,
required String from,
required String to,
}) async {
final uri = _buildUri(
endpoint: "get-estimate-reverse",
params: {
"to": to.toUpperCase(),
"from": from.toUpperCase(),
"amount": amountTo,
},
);
try {
final json = await _makeGetRequest(uri);
final map = Map<String, dynamic>.from(json as Map);
// not sure why the api responds without these sometimes...
map["to"] ??= to.toUpperCase();
map["from"] ??= from.toUpperCase();
return ExchangeResponse(
value: NEstimate.fromJson(
map,
),
);
} catch (e, s) {
Logging.instance.log(
"Nanswap.getEstimateReverse() exception: $e\n$s",
level: LogLevel.Error,
);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
// GET Get order limit amount
// https://api.nanswap.com/v1/get-limits?from=XNO&to=BAN
//
// Returns minimum and maximum from amount for a given pair. Maximum amount depends of current liquidity.
// HEADERS
// Accept
//
// application/json
// PARAMS
// from
// XNO
// Ticker from
//
// to
// BAN
// Ticker to
Future<ExchangeResponse<({num minFrom, num maxFrom})>> getOrderLimits({
required String from,
required String to,
}) async {
final uri = _buildUri(
endpoint: "get-limits",
params: {
"to": to.toUpperCase(),
"from": from.toUpperCase(),
},
);
try {
final json = await _makeGetRequest(uri);
return ExchangeResponse(
value: (
minFrom: json["min"] as num,
maxFrom: json["max"] as num,
),
);
} catch (e, s) {
Logging.instance.log(
"Nanswap.getOrderLimits() exception: $e\n$s",
level: LogLevel.Error,
);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
// POST Create a new order
// https://api.nanswap.com/v1/create-order
//
// Create a new order and returns order data. You need to send the request body as JSON.
// A valid API key is required in nanswap-api-key header for this request.
// You can get one at https://nanswap.com/API
// Request:
//
// * from ticker of currency you want to exchange
// * to ticker of currency you want to receive
// * amount The amount you want to send
// * toAddress The address that will recieve the exchanged funds
// * extraId (optional) Memo/Id of the toAddress
//
// * itemName (optional) An item name that will be displayed on transaction
// page. Can be used by merchant to provide a better UX to users. Max 128 char.
// * maxDurationSeconds (optional) Maximum seconds after what transaction
// expires. Min: 30s Max: 259200s. Default to 72h or 5min if itemName is set
// Reponse:
//
// * id Order id.
// * from ticker of currency you want to exchange
// * to ticker of currency you want to receive
// * expectedAmountFrom The amount you want to send
// * expectedAmountTo Estimated value that you will get based on the field expectedAmountFrom
// * payinAddress Nanswap's address you need to send the funds to
// * payinExtraId If present, the extra/memo id required for the payinAddress
// * payoutAddress The address that will recieve the exchanged funds
// * fullLink URL of the transaction
// AUTHORIZATIONAPI Key
// Key
//
// nanswap-api-key
// Value
//
// <value>
// HEADERS
// nanswap-api-key
//
// API_KEY
//
// (Required)
// Content-Type
//
// application/json
// Accept
//
// application/json
Future<ExchangeResponse<NTrade>> createOrder({
required String from,
required String to,
required num fromAmount,
required String toAddress,
String? extraIdOrMemo,
}) async {
final uri = _buildUri(
endpoint: "create-order",
);
final body = {
"from": from.toUpperCase(),
"to": to.toUpperCase(),
"amount": fromAmount,
"toAddress": toAddress,
};
if (extraIdOrMemo != null) {
body["extraId"] = extraIdOrMemo;
}
try {
final json = await _makePostRequest(uri, body);
try {
return ExchangeResponse(
value: NTrade.fromJson(
Map<String, dynamic>.from(json as Map),
),
);
} catch (_) {
debugPrint(json.toString());
rethrow;
}
} catch (e, s) {
Logging.instance.log(
"Nanswap.createOrder() exception: $e\n$s",
level: LogLevel.Error,
);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
// GET Get order id data
// https://api.nanswap.com/v1/get-order?id=zYkxDxfmYRM
//
// Returns data of an order id.
// Response:
//
// id Order id.
//
// status Order status, can be one of the following : [waiting, exchanging, sending, completed, error]
//
// from ticker of currency you want to exchange
//
// fromNetwork network of the currency you want to exchange.
//
// to ticker of currency you want to receive
//
// toNetwork network of the currency you want to receive.
//
// expectedAmountFrom The amount you want to send
//
// expectedAmountTo Estimated value that you will get based on the field expectedAmountFrom
//
// amountFrom From Amount Exchanged
//
// amountTo To Amount Exchanged
//
// payinAddress Nanswap's address you need to send the funds to
//
// payinExtraId If present, the extra/memo id required for the payinAddress
//
// payoutAddress The address that will recieve the exchanged funds
//
// payinHash Hash of the transaction you sent us
//
// senderAddress Address which sent us the funds
//
// payoutHash Hash of the transaction we sent to you
//
// HEADERS
// Accept
//
// application/json
// PARAMS
// id
//
// zYkxDxfmYRM
//
// The order id
Future<ExchangeResponse<NTrade>> getOrder({required String id}) async {
final uri = _buildUri(
endpoint: "get-order",
params: {
"id": id,
},
);
try {
final json = await _makeGetRequest(uri);
try {
return ExchangeResponse(
value: NTrade.fromJson(
Map<String, dynamic>.from(json as Map),
),
);
} catch (_) {
debugPrint(json.toString());
rethrow;
}
} catch (e, s) {
Logging.instance.log(
"Nanswap.getOrder($id) exception: $e\n$s",
level: LogLevel.Error,
);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
}

View file

@ -0,0 +1,461 @@
import 'package:decimal/decimal.dart';
import 'package:uuid/uuid.dart';
import '../../../app_config.dart';
import '../../../exceptions/exchange/exchange_exception.dart';
import '../../../models/exchange/response_objects/estimate.dart';
import '../../../models/exchange/response_objects/range.dart';
import '../../../models/exchange/response_objects/trade.dart';
import '../../../models/isar/exchange_cache/currency.dart';
import '../../../models/isar/exchange_cache/pair.dart';
import '../exchange.dart';
import '../exchange_response.dart';
import 'api_response_models/n_estimate.dart';
import 'nanswap_api.dart';
class NanswapExchange extends Exchange {
NanswapExchange._();
static NanswapExchange? _instance;
static NanswapExchange get instance => _instance ??= NanswapExchange._();
static const exchangeName = "Nanswap";
static const filter = ["BTC", "BAN", "XNO"];
@override
bool get supportsRefundAddress => false;
@override
Future<ExchangeResponse<Trade>> createTrade({
required String from,
required String to,
required bool fixedRate,
required Decimal amount,
required String addressTo,
String? extraId,
required String addressRefund,
required String refundExtraId,
Estimate? estimate,
required bool reversed,
}) async {
try {
if (fixedRate) {
throw ExchangeException(
"Nanswap fixedRate not available",
ExchangeExceptionType.generic,
);
}
if (refundExtraId.isNotEmpty) {
throw ExchangeException(
"Nanswap refundExtraId not available",
ExchangeExceptionType.generic,
);
}
if (addressRefund.isNotEmpty) {
throw ExchangeException(
"Nanswap addressRefund not available",
ExchangeExceptionType.generic,
);
}
if (reversed) {
throw ExchangeException(
"Nanswap reversed not available",
ExchangeExceptionType.generic,
);
}
final response = await NanswapAPI.instance.createOrder(
from: from,
to: to,
fromAmount: amount.toDouble(),
toAddress: addressTo,
extraIdOrMemo: extraId,
);
if (response.exception != null) {
return ExchangeResponse(
exception: response.exception,
);
}
final t = response.value!;
print(t);
return ExchangeResponse(
value: Trade(
uuid: const Uuid().v1(),
tradeId: t.id,
rateType: "estimated",
direction: "normal",
timestamp: DateTime.now(),
updatedAt: DateTime.now(),
payInCurrency: from,
payInAmount: t.expectedAmountFrom.toString(),
payInAddress: t.payinAddress,
payInNetwork: t.toNetwork ?? t.to,
payInExtraId: t.payinExtraId ?? "",
payInTxid: t.payinHash ?? "",
payOutCurrency: to,
payOutAmount: t.expectedAmountTo.toString(),
payOutAddress: t.payoutAddress,
payOutNetwork: t.fromNetwork ?? t.from,
payOutExtraId: "",
payOutTxid: t.payoutHash ?? "",
refundAddress: "",
refundExtraId: "",
status: "waiting",
exchangeName: exchangeName,
),
);
} on ExchangeException catch (e) {
return ExchangeResponse(
exception: e,
);
} catch (e) {
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
@override
Future<ExchangeResponse<List<Currency>>> getAllCurrencies(
bool fixedRate,
) async {
try {
if (fixedRate) {
throw ExchangeException(
"Nanswap fixedRate not available",
ExchangeExceptionType.generic,
);
}
final response = await NanswapAPI.instance.getSupportedCurrencies();
if (response.exception != null) {
return ExchangeResponse(
exception: response.exception,
);
}
return ExchangeResponse(
value: response.value!
.where((e) => filter.contains(e.id))
.map(
(e) => Currency(
exchangeName: exchangeName,
ticker: e.id,
name: e.name,
network: e.network,
image: e.image,
isFiat: false,
rateType: SupportedRateType.estimated,
isStackCoin: AppConfig.isStackCoin(e.id),
tokenContract: null,
isAvailable: true,
),
)
.toList(),
);
} on ExchangeException catch (e) {
return ExchangeResponse(
exception: e,
);
} catch (e) {
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
@override
Future<ExchangeResponse<List<Pair>>> getAllPairs(bool fixedRate) async {
throw UnimplementedError();
}
@override
Future<ExchangeResponse<List<Estimate>>> getEstimates(
String from,
String to,
Decimal amount,
bool fixedRate,
bool reversed,
) async {
try {
if (fixedRate) {
throw ExchangeException(
"Nanswap fixedRate not available",
ExchangeExceptionType.generic,
);
}
final ExchangeResponse<NEstimate> response;
if (reversed) {
response = await NanswapAPI.instance.getEstimateReversed(
from: from,
to: to,
amountTo: amount.toString(),
);
} else {
response = await NanswapAPI.instance.getEstimate(
from: from,
to: to,
amountFrom: amount.toString(),
);
}
if (response.exception != null) {
return ExchangeResponse(
exception: response.exception,
);
}
final t = response.value!;
return ExchangeResponse(
value: [
Estimate(
estimatedAmount: Decimal.parse(
(reversed ? t.amountFrom : t.amountTo).toString(),
),
fixedRate: fixedRate,
reversed: reversed,
exchangeProvider: exchangeName,
),
],
);
} on ExchangeException catch (e) {
return ExchangeResponse(
exception: e,
);
} catch (e) {
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
@override
Future<ExchangeResponse<List<Currency>>> getPairedCurrencies(
String forCurrency,
bool fixedRate,
) async {
try {
if (fixedRate) {
throw ExchangeException(
"Nanswap fixedRate not available",
ExchangeExceptionType.generic,
);
}
final response = await getAllCurrencies(
fixedRate,
);
if (response.exception != null) {
return ExchangeResponse(
exception: response.exception,
);
}
return ExchangeResponse(
value: response.value!..removeWhere((e) => e.ticker == forCurrency),
);
} on ExchangeException catch (e) {
return ExchangeResponse(
exception: e,
);
} catch (e) {
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
@override
Future<ExchangeResponse<List<Pair>>> getPairsFor(
String currency,
bool fixedRate,
) async {
throw UnsupportedError("Not used");
}
@override
Future<ExchangeResponse<Range>> getRange(
String from,
String to,
bool fixedRate,
) async {
try {
if (fixedRate) {
throw ExchangeException(
"Nanswap fixedRate not available",
ExchangeExceptionType.generic,
);
}
final response = await NanswapAPI.instance.getOrderLimits(
from: from,
to: to,
);
if (response.exception != null) {
return ExchangeResponse(
exception: response.exception,
);
}
final t = response.value!;
return ExchangeResponse(
value: Range(
min: Decimal.parse(t.minFrom.toString()),
max: Decimal.parse(t.maxFrom.toString()),
),
);
} on ExchangeException catch (e) {
return ExchangeResponse(
exception: e,
);
} catch (e) {
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
@override
Future<ExchangeResponse<Trade>> getTrade(String tradeId) async {
try {
final response = await NanswapAPI.instance.getOrder(
id: tradeId,
);
if (response.exception != null) {
return ExchangeResponse(
exception: response.exception,
);
}
final t = response.value!;
return ExchangeResponse(
value: Trade(
uuid: const Uuid().v1(),
tradeId: t.id,
rateType: "estimated",
direction: "normal",
timestamp: DateTime.now(),
updatedAt: DateTime.now(),
payInCurrency: t.from,
payInAmount: t.expectedAmountFrom.toString(),
payInAddress: t.payinAddress,
payInNetwork: t.toNetwork ?? t.to,
payInExtraId: t.payinExtraId ?? "",
payInTxid: t.payinHash ?? "",
payOutCurrency: t.to,
payOutAmount: t.expectedAmountTo.toString(),
payOutAddress: t.payoutAddress,
payOutNetwork: t.fromNetwork ?? t.from,
payOutExtraId: "",
payOutTxid: t.payoutHash ?? "",
refundAddress: "",
refundExtraId: "",
status: t.status ?? "unknown",
exchangeName: exchangeName,
),
);
} on ExchangeException catch (e) {
return ExchangeResponse(
exception: e,
);
} catch (e) {
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
@override
Future<ExchangeResponse<List<Trade>>> getTrades() async {
// TODO: implement getTrades
throw UnimplementedError();
}
@override
String get name => exchangeName;
@override
Future<ExchangeResponse<Trade>> updateTrade(Trade trade) async {
try {
final response = await NanswapAPI.instance.getOrder(
id: trade.tradeId,
);
if (response.exception != null) {
return ExchangeResponse(
exception: response.exception,
);
}
final t = response.value!;
return ExchangeResponse(
value: Trade(
uuid: trade.uuid,
tradeId: t.id,
rateType: trade.rateType,
direction: trade.rateType,
timestamp: trade.timestamp,
updatedAt: DateTime.now(),
payInCurrency: t.from,
payInAmount: t.expectedAmountFrom.toString(),
payInAddress: t.payinAddress,
payInNetwork: t.toNetwork ?? trade.payInNetwork,
payInExtraId: t.payinExtraId ?? trade.payInExtraId,
payInTxid: t.payinHash ?? trade.payInTxid,
payOutCurrency: t.to,
payOutAmount: t.expectedAmountTo.toString(),
payOutAddress: t.payoutAddress,
payOutNetwork: t.fromNetwork ?? trade.payOutNetwork,
payOutExtraId: trade.payOutExtraId,
payOutTxid: t.payoutHash ?? trade.payOutTxid,
refundAddress: trade.refundAddress,
refundExtraId: trade.refundExtraId,
status: t.status ?? "unknown",
exchangeName: exchangeName,
),
);
} on ExchangeException catch (e) {
return ExchangeResponse(
exception: e,
);
} catch (e) {
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
}

View file

@ -9,8 +9,10 @@
*/
import 'package:flutter/material.dart';
import '../services/exchange/change_now/change_now_exchange.dart';
import '../services/exchange/majestic_bank/majestic_bank_exchange.dart';
import '../services/exchange/nanswap/nanswap_exchange.dart';
import '../services/exchange/simpleswap/simpleswap_exchange.dart';
import '../services/exchange/trocador/trocador_exchange.dart';
@ -45,6 +47,7 @@ class _EXCHANGE {
String get majesticBankBlue => "${_path}mb_blue.svg";
String get majesticBankGreen => "${_path}mb_green.svg";
String get trocador => "${_path}trocador.svg";
String get nanswap => "${_path}nanswap.svg";
String getIconFor({required String exchangeName}) {
switch (exchangeName) {
@ -56,6 +59,8 @@ class _EXCHANGE {
return majesticBankBlue;
case TrocadorExchange.exchangeName:
return trocador;
case NanswapExchange.exchangeName:
return nanswap;
default:
throw ArgumentError("Invalid exchange name passed to "
"Assets.exchange.getIconFor()");

View file

@ -11,18 +11,19 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:uuid/uuid.dart';
import '../app_config.dart';
import '../db/hive/db.dart';
import '../services/event_bus/events/global/tor_status_changed_event.dart';
import '../services/event_bus/global_event_bus.dart';
import '../app_config.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import '../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart';
import 'amount/amount_unit.dart';
import 'constants.dart';
import 'enums/backup_frequency_type.dart';
import 'enums/languages_enum.dart';
import 'enums/sync_type_enum.dart';
import '../wallets/crypto_currency/crypto_currency.dart';
import '../wallets/wallet/wallet_mixin_interfaces/cash_fusion_interface.dart';
import 'package:uuid/uuid.dart';
class Prefs extends ChangeNotifier {
Prefs._();
@ -69,6 +70,7 @@ class Prefs extends ChangeNotifier {
await _setMaxDecimals();
_useTor = await _getUseTor();
_fusionServerInfo = await _getFusionServerInfo();
_autoPin = await _getAutoPin();
_initialized = true;
}
@ -1103,4 +1105,30 @@ class Prefs extends ChangeNotifier {
return actualMap;
}
// Automatic PIN entry.
bool _autoPin = false;
bool get autoPin => _autoPin;
set autoPin(bool autoPin) {
if (_autoPin != autoPin) {
DB.instance.put<dynamic>(
boxName: DB.boxNamePrefs,
key: "autoPin",
value: autoPin,
);
_autoPin = autoPin;
notifyListeners();
}
}
Future<bool> _getAutoPin() async {
return await DB.instance.get<dynamic>(
boxName: DB.boxNamePrefs,
key: "autoPin",
) as bool? ??
false;
}
}

View file

@ -11,9 +11,24 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../themes/stack_colors.dart';
import 'logger.dart';
import '../widgets/custom_loading_overlay.dart';
import 'logger.dart';
Future<T> minWaitFuture<T>(
Future<T> future, {
required Duration delay,
}) async {
final results = await Future.wait(
[
future,
Future<dynamic>.delayed(delay),
],
);
return results.first as T;
}
Future<T?> showLoading<T>({
required Future<T> whileFuture,
@ -23,6 +38,7 @@ Future<T?> showLoading<T>({
bool rootNavigator = false,
bool opaqueBG = false,
void Function(Exception)? onException,
Duration? delay,
}) async {
unawaited(
showDialog<void>(
@ -49,7 +65,11 @@ Future<T?> showLoading<T>({
T? result;
try {
if (delay != null) {
result = await minWaitFuture(whileFuture, delay: delay);
} else {
result = await whileFuture;
}
} catch (e, s) {
Logging.instance.log(
"showLoading caught: $e\n$s",

View file

@ -4,7 +4,6 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:solana/solana.dart';
import '../networking/http.dart';
import '../pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart';
@ -15,6 +14,7 @@ import '../wallets/crypto_currency/crypto_currency.dart';
import '../wallets/crypto_currency/interfaces/electrumx_currency_interface.dart';
import '../wallets/crypto_currency/intermediate/cryptonote_currency.dart';
import '../wallets/crypto_currency/intermediate/nano_currency.dart';
import '../wallets/wallet/impl/solana_wallet.dart';
import 'connection_check/electrum_connection_check.dart';
import 'logger.dart';
import 'test_epic_box_connection.dart';
@ -210,14 +210,20 @@ Future<bool> testNodeConnection({
case Solana():
try {
RpcClient rpcClient;
if (formData.host!.startsWith("http") ||
formData.host!.startsWith("https")) {
rpcClient = RpcClient("${formData.host}:${formData.port}");
} else {
rpcClient = RpcClient("http://${formData.host}:${formData.port}");
}
await rpcClient.getEpochInfo().then((value) => testPassed = true);
final rpcClient = SolanaWallet.createRpcClient(
formData.host!,
formData.port!,
formData.useSSL ?? false,
ref.read(prefsChangeNotifierProvider),
ref.read(pTorService),
);
final health = await rpcClient.getHealth();
Logging.instance.log(
"Solana testNodeConnection \"health=$health\"",
level: LogLevel.Info,
);
return true;
} catch (_) {
testPassed = false;
}

View file

@ -55,8 +55,6 @@ class Bitcoincash extends Bip39HDCurrency with ElectrumXCurrencyInterface {
@override
int get maxUnusedAddressGap => 50;
@override
int get maxNumberOfIndexesToCheck => 10000000;
@override
// change this to change the number of confirms a tx needs in order to show as confirmed

View file

@ -50,8 +50,6 @@ class Ecash extends Bip39HDCurrency with ElectrumXCurrencyInterface {
@override
int get maxUnusedAddressGap => 50;
@override
int get maxNumberOfIndexesToCheck => 10000000;
@override
// change this to change the number of confirms a tx needs in order to show as confirmed

View file

@ -46,8 +46,7 @@ class Solana extends Bip39Currency {
switch (network) {
case CryptoCurrencyNetwork.main:
return NodeModel(
host:
"https://api.mainnet-beta.solana.com/", // TODO: Change this to stack wallet one
host: "https://solana.stackwallet.com",
port: 443,
name: DefaultNodes.defaultName,
id: DefaultNodes.buildId(this),
@ -70,9 +69,13 @@ class Solana extends Bip39Currency {
@override
bool validateAddress(String address) {
try {
return isPointOnEd25519Curve(
Ed25519HDPublicKey.fromBase58(address).toByteArray(),
);
} catch (_) {
return false;
}
}
@override

View file

@ -1,6 +1,7 @@
import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib;
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/enums/derive_path_type_enum.dart';
@ -16,7 +17,6 @@ abstract class Bip39HDCurrency extends Bip39Currency {
List<DerivePathType> get supportedDerivationPathTypes;
int get maxUnusedAddressGap => 50;
int get maxNumberOfIndexesToCheck => 10000;
String constructDerivePath({
required DerivePathType derivePathType,

View file

@ -8,12 +8,14 @@ import '../intermediate/bip39_hd_wallet.dart';
import '../wallet_mixin_interfaces/coin_control_interface.dart';
import '../wallet_mixin_interfaces/cpfp_interface.dart';
import '../wallet_mixin_interfaces/electrumx_interface.dart';
import '../wallet_mixin_interfaces/extended_keys_interface.dart';
import '../wallet_mixin_interfaces/paynym_interface.dart';
import '../wallet_mixin_interfaces/rbf_interface.dart';
class BitcoinWallet<T extends PaynymCurrencyInterface> extends Bip39HDWallet<T>
with
ElectrumXInterface<T>,
ExtendedKeysInterface<T>,
CoinControlInterface,
PaynymInterface<T>,
RbfInterface<T>,

View file

@ -19,11 +19,13 @@ import '../wallet_mixin_interfaces/bcash_interface.dart';
import '../wallet_mixin_interfaces/cash_fusion_interface.dart';
import '../wallet_mixin_interfaces/coin_control_interface.dart';
import '../wallet_mixin_interfaces/electrumx_interface.dart';
import '../wallet_mixin_interfaces/extended_keys_interface.dart';
class BitcoincashWallet<T extends ElectrumXCurrencyInterface>
extends Bip39HDWallet<T>
with
ElectrumXInterface<T>,
ExtendedKeysInterface<T>,
BCashInterface<T>,
CoinControlInterface<T>,
CashFusionInterface<T> {

View file

@ -12,9 +12,10 @@ import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
import '../intermediate/bip39_hd_wallet.dart';
import '../wallet_mixin_interfaces/coin_control_interface.dart';
import '../wallet_mixin_interfaces/electrumx_interface.dart';
import '../wallet_mixin_interfaces/extended_keys_interface.dart';
class DashWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
with ElectrumXInterface<T>, CoinControlInterface {
with ElectrumXInterface<T>, ExtendedKeysInterface<T>, CoinControlInterface {
DashWallet(CryptoCurrencyNetwork network) : super(Dash(network) as T);
@override

View file

@ -13,9 +13,11 @@ import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
import '../intermediate/bip39_hd_wallet.dart';
import '../wallet_mixin_interfaces/coin_control_interface.dart';
import '../wallet_mixin_interfaces/electrumx_interface.dart';
import '../wallet_mixin_interfaces/extended_keys_interface.dart';
class DogecoinWallet<T extends ElectrumXCurrencyInterface>
extends Bip39HDWallet<T> with ElectrumXInterface<T>, CoinControlInterface {
extends Bip39HDWallet<T>
with ElectrumXInterface<T>, ExtendedKeysInterface<T>, CoinControlInterface {
DogecoinWallet(CryptoCurrencyNetwork network) : super(Dogecoin(network) as T);
@override

View file

@ -19,10 +19,12 @@ import '../wallet_mixin_interfaces/bcash_interface.dart';
import '../wallet_mixin_interfaces/cash_fusion_interface.dart';
import '../wallet_mixin_interfaces/coin_control_interface.dart';
import '../wallet_mixin_interfaces/electrumx_interface.dart';
import '../wallet_mixin_interfaces/extended_keys_interface.dart';
class EcashWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
with
ElectrumXInterface<T>,
ExtendedKeysInterface<T>,
BCashInterface<T>,
CoinControlInterface<T>,
CashFusionInterface<T> {

View file

@ -22,6 +22,7 @@ import '../../models/tx_data.dart';
import '../intermediate/bip39_hd_wallet.dart';
import '../wallet_mixin_interfaces/coin_control_interface.dart';
import '../wallet_mixin_interfaces/electrumx_interface.dart';
import '../wallet_mixin_interfaces/extended_keys_interface.dart';
import '../wallet_mixin_interfaces/lelantus_interface.dart';
import '../wallet_mixin_interfaces/spark_interface.dart';
@ -30,6 +31,7 @@ const sparkStartBlock = 819300; // (approx 18 Jan 2024)
class FiroWallet<T extends ElectrumXCurrencyInterface> extends Bip39HDWallet<T>
with
ElectrumXInterface<T>,
ExtendedKeysInterface<T>,
LelantusInterface<T>,
SparkInterface<T>,
CoinControlInterface<T> {

View file

@ -15,6 +15,7 @@ import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
import '../intermediate/bip39_hd_wallet.dart';
import '../wallet_mixin_interfaces/coin_control_interface.dart';
import '../wallet_mixin_interfaces/electrumx_interface.dart';
import '../wallet_mixin_interfaces/extended_keys_interface.dart';
import '../wallet_mixin_interfaces/ordinals_interface.dart';
import '../wallet_mixin_interfaces/rbf_interface.dart';
@ -22,6 +23,7 @@ class LitecoinWallet<T extends ElectrumXCurrencyInterface>
extends Bip39HDWallet<T>
with
ElectrumXInterface<T>,
ExtendedKeysInterface<T>,
CoinControlInterface<T>,
RbfInterface<T>,
OrdinalsInterface<T> {

View file

@ -19,10 +19,14 @@ import '../../models/tx_data.dart';
import '../intermediate/bip39_hd_wallet.dart';
import '../wallet_mixin_interfaces/coin_control_interface.dart';
import '../wallet_mixin_interfaces/electrumx_interface.dart';
import '../wallet_mixin_interfaces/extended_keys_interface.dart';
class ParticlWallet<T extends ElectrumXCurrencyInterface>
extends Bip39HDWallet<T>
with ElectrumXInterface<T>, CoinControlInterface<T> {
with
ElectrumXInterface<T>,
ExtendedKeysInterface<T>,
CoinControlInterface<T> {
@override
int get isarTransactionVersion => 2;

View file

@ -1,4 +1,5 @@
import 'package:isar/isar.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../models/isar/models/blockchain_data/transaction.dart';
import '../../../models/isar/models/blockchain_data/v2/input_v2.dart';
@ -11,10 +12,14 @@ import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
import '../intermediate/bip39_hd_wallet.dart';
import '../wallet_mixin_interfaces/coin_control_interface.dart';
import '../wallet_mixin_interfaces/electrumx_interface.dart';
import '../wallet_mixin_interfaces/extended_keys_interface.dart';
class PeercoinWallet<T extends ElectrumXCurrencyInterface>
extends Bip39HDWallet<T>
with ElectrumXInterface<T>, CoinControlInterface<T> {
with
ElectrumXInterface<T>,
ExtendedKeysInterface<T>,
CoinControlInterface<T> {
@override
int get isarTransactionVersion => 2;

View file

@ -18,6 +18,7 @@ import '../../../services/node_service.dart';
import '../../../services/tor_service.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/logger.dart';
import '../../../utilities/prefs.dart';
import '../../crypto_currency/crypto_currency.dart';
import '../../models/tx_data.dart';
import '../intermediate/bip39_wallet.dart';
@ -245,14 +246,15 @@ class SolanaWallet extends Bip39Wallet<Solana> {
}
@override
Future<bool> pingCheck() {
Future<bool> pingCheck() async {
String? health;
try {
_checkClient();
_rpcClient?.getHealth();
return Future.value(true);
health = await _rpcClient?.getHealth();
return health != null;
} catch (e, s) {
Logging.instance.log(
"$runtimeType Solana pingCheck failed: $e\n$s",
"$runtimeType Solana pingCheck failed \"health=$health\": $e\n$s",
level: LogLevel.Error,
);
return Future.value(false);
@ -453,32 +455,67 @@ class SolanaWallet extends Bip39Wallet<Solana> {
}
@override
Future<bool> updateUTXOs() {
Future<bool> updateUTXOs() async {
// No UTXOs in Solana
return Future.value(false);
return false;
}
/// Make sure the Solana RpcClient uses Tor if it's enabled.
///
void _checkClient() async {
void _checkClient() {
final node = getCurrentNode();
_rpcClient = createRpcClient(
node.host,
node.port,
node.useSSL,
prefs,
TorService.sharedInstance,
);
}
// static helper function for building a sol rpc client
static RpcClient createRpcClient(
final String host,
final int port,
final bool useSSL,
final Prefs prefs,
final TorService torService,
) {
HttpClient? httpClient;
if (prefs.useTor) {
// Make proxied HttpClient.
final ({InternetAddress host, int port}) proxyInfo =
TorService.sharedInstance.getProxyInfo();
final proxyInfo = torService.getProxyInfo();
final proxySettings = ProxySettings(proxyInfo.host, proxyInfo.port);
httpClient = HttpClient();
SocksTCPClient.assignToHttpClient(httpClient, [proxySettings]);
}
_rpcClient = RpcClient(
"${getCurrentNode().host}:${getCurrentNode().port}",
final regex = RegExp("^(http|https)://");
String editedHost;
if (host.startsWith(regex)) {
editedHost = host.replaceFirst(regex, "");
} else {
editedHost = host;
}
while (editedHost.endsWith("/")) {
editedHost = editedHost.substring(0, editedHost.length - 1);
}
final uri = Uri(
scheme: useSSL ? "https" : "http",
host: editedHost,
port: port,
);
return RpcClient(
uri.toString(),
timeout: const Duration(seconds: 30),
customHeaders: {},
httpClient: httpClient,
);
return;
}
}

View file

@ -1,3 +1,5 @@
import 'dart:typed_data';
import 'package:bip39/bip39.dart' as bip39;
import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib;
import 'package:isar/isar.dart';
@ -6,6 +8,7 @@ import '../../../models/balance.dart';
import '../../../models/isar/models/blockchain_data/address.dart';
import '../../../utilities/amount/amount.dart';
import '../../../utilities/enums/derive_path_type_enum.dart';
import '../../../utilities/extensions/extensions.dart';
import '../../crypto_currency/intermediate/bip39_hd_currency.dart';
import '../wallet_mixin_interfaces/multi_address_interface.dart';
import 'bip39_wallet.dart';
@ -28,6 +31,22 @@ abstract class Bip39HDWallet<T extends Bip39HDCurrency> extends Bip39Wallet<T>
return coinlib.HDPrivateKey.fromSeed(seed);
}
Future<String> getPrivateKeyWIF(Address address) async {
final keys =
(await getRootHDNode()).derivePath(address.derivationPath!.value);
final List<int> data = [
cryptoCurrency.networkParams.wifPrefix,
...keys.privateKey.data,
if (keys.privateKey.compressed) 1,
];
final checksum =
coinlib.sha256DoubleHash(Uint8List.fromList(data)).sublist(0, 4);
data.addAll(checksum);
return Uint8List.fromList(data).toBase58Encoded;
}
Future<Address> generateNextReceivingAddress({
required DerivePathType derivePathType,
}) async {

View file

@ -937,7 +937,6 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
int highestIndexWithHistory = 0;
for (int index = 0;
index < cryptoCurrency.maxNumberOfIndexesToCheck &&
gapCounter < cryptoCurrency.maxUnusedAddressGap;
index += txCountBatchSize) {
Logging.instance.log(
@ -1017,10 +1016,7 @@ mixin ElectrumXInterface<T extends ElectrumXCurrencyInterface>
final List<Address> addressArray = [];
int gapCounter = 0;
int index = 0;
for (;
index < cryptoCurrency.maxNumberOfIndexesToCheck &&
gapCounter < cryptoCurrency.maxUnusedAddressGap;
index++) {
for (; gapCounter < cryptoCurrency.maxUnusedAddressGap; index++) {
Logging.instance.log(
"index: $index, \t GapCounter chain=$chain ${type.name}: $gapCounter",
level: LogLevel.Info,

View file

@ -0,0 +1,87 @@
import '../../crypto_currency/interfaces/electrumx_currency_interface.dart';
import 'electrumx_interface.dart';
typedef XPub = ({String path, String xpub});
typedef XPriv = ({String path, String xpriv});
mixin ExtendedKeysInterface<T extends ElectrumXCurrencyInterface>
on ElectrumXInterface<T> {
Future<({List<XPub> xpubs, String fingerprint})> getXPubs() async {
final paths = cryptoCurrency.supportedDerivationPathTypes.map(
(e) => (
path: e,
addressType: e.getAddressType(),
),
);
final master = await getRootHDNode();
final fingerprint = master.fingerprint.toRadixString(16);
final futures = paths.map((e) async {
String path = cryptoCurrency.constructDerivePath(
derivePathType: e.path,
chain: 0,
index: 0,
);
// trim chain and address index
path = path.substring(0, path.lastIndexOf("'") + 1);
final node = master.derivePath(path);
return (
path: path,
xpub: node.hdPublicKey.encode(
cryptoCurrency.networkParams.pubHDPrefix,
// 0x04b24746,
),
);
});
return (
fingerprint: fingerprint,
xpubs: await Future.wait(futures),
);
}
Future<({List<XPriv> xprivs, String fingerprint})> getXPrivs() async {
final paths = cryptoCurrency.supportedDerivationPathTypes.map(
(e) => (
path: e,
addressType: e.getAddressType(),
),
);
final master = await getRootHDNode();
final fingerprint = master.fingerprint.toRadixString(16);
final futures = paths.map((e) async {
String path = cryptoCurrency.constructDerivePath(
derivePathType: e.path,
chain: 0,
index: 0,
);
// trim chain and address index
path = path.substring(0, path.lastIndexOf("'") + 1);
final node = master.derivePath(path);
return (
path: path,
xpriv: node.encode(
cryptoCurrency.networkParams.privHDPrefix,
),
);
});
return (
fingerprint: fingerprint,
xprivs: [
(
path: "Master",
xpriv: master.encode(
cryptoCurrency.networkParams.privHDPrefix,
),
),
...(await Future.wait(futures)),
],
);
}
}

View file

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/isar/models/isar_models.dart';
import '../pages/pinpad_views/pinpad_dialog.dart';
import '../pages_desktop_specific/password/request_desktop_auth_dialog.dart';
import '../providers/global/wallets_provider.dart';
import '../utilities/show_loading.dart';
import '../utilities/text_styles.dart';
import '../utilities/util.dart';
import '../wallets/wallet/intermediate/bip39_hd_wallet.dart';
import 'custom_buttons/blue_text_button.dart';
import 'detail_item.dart';
class AddressPrivateKey extends ConsumerStatefulWidget {
/// The [walletId] MUST be the id of a [Bip39HDWallet]!
const AddressPrivateKey({
super.key,
required this.walletId,
required this.address,
});
final String walletId;
final Address address;
@override
ConsumerState<AddressPrivateKey> createState() => _AddressPrivateKeyState();
}
class _AddressPrivateKeyState extends ConsumerState<AddressPrivateKey> {
String? _private;
bool _lock = false;
Future<void> _loadPrivKey() async {
// sanity check that should never actually fail in practice.
// Big problems if it actually does though so we check and crash if it fails.
assert(widget.walletId == widget.address.walletId);
if (_lock) {
return;
}
_lock = true;
try {
final verified = await showDialog<String?>(
context: context,
builder: (context) => Util.isDesktop
? const RequestDesktopAuthDialog(title: "Show WIF private key")
: const PinpadDialog(
biometricsAuthenticationTitle: "Show WIF private key",
biometricsLocalizedReason:
"Authenticate to show WIF private key",
biometricsCancelButtonString: "CANCEL",
),
barrierDismissible: !Util.isDesktop,
);
if (verified == "verified success" && mounted) {
final wallet =
ref.read(pWallets).getWallet(widget.walletId) as Bip39HDWallet;
_private = await showLoading(
whileFuture: wallet.getPrivateKeyWIF(widget.address),
context: context,
message: "Loading...",
delay: const Duration(milliseconds: 800),
rootNavigator: Util.isDesktop,
);
if (context.mounted) {
setState(() {});
} else {
_private == null;
}
}
} finally {
_lock = false;
}
}
@override
Widget build(BuildContext context) {
return DetailItemBase(
button: CustomTextButton(
text: "Show",
onTap: _loadPrivKey,
enabled: _private == null,
),
title: Text(
"Private key (WIF)",
style: STextStyles.itemSubtitle(context),
),
detail: SelectableText(
_private ?? "*" * 52, // 52 is approx length
style: STextStyles.w500_14(context),
),
);
}
}

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import '../themes/stack_colors.dart';
import '../utilities/text_styles.dart';
import '../utilities/util.dart';
@ -15,6 +16,8 @@ class DetailItem extends StatelessWidget {
this.showEmptyDetail = true,
this.horizontal = false,
this.disableSelectableText = false,
this.borderColor,
this.expandDetail = false,
});
final String title;
@ -24,25 +27,83 @@ class DetailItem extends StatelessWidget {
final bool horizontal;
final bool disableSelectableText;
final Color? overrideDetailTextColor;
final Color? borderColor;
final bool expandDetail;
@override
Widget build(BuildContext context) {
final TextStyle detailStyle;
TextStyle detailStyle = STextStyles.w500_14(context);
String _detail = detail;
if (overrideDetailTextColor != null) {
detailStyle = STextStyles.w500_14(context).copyWith(
color: overrideDetailTextColor,
);
} else {
detailStyle = STextStyles.w500_14(context);
}
if (detail.isEmpty && showEmptyDetail) {
_detail = "$title will appear here";
detailStyle = detailStyle.copyWith(
color: Theme.of(context).extension<StackColors>()!.textSubtitle3,
);
}
return DetailItemBase(
horizontal: horizontal,
borderColor: borderColor,
expandDetail: expandDetail,
title: disableSelectableText
? Text(
title,
style: STextStyles.itemSubtitle(context),
)
: SelectableText(
title,
style: STextStyles.itemSubtitle(context),
),
detail: disableSelectableText
? Text(
_detail,
style: detailStyle,
)
: SelectableText(
_detail,
style: detailStyle,
),
);
}
}
class DetailItemBase extends StatelessWidget {
const DetailItemBase({
super.key,
required this.title,
required this.detail,
this.button,
this.horizontal = false,
this.borderColor,
this.expandDetail = false,
});
final Widget title;
final Widget detail;
final Widget? button;
final bool horizontal;
final Color? borderColor;
final bool expandDetail;
@override
Widget build(BuildContext context) {
return ConditionalParent(
condition: !Util.isDesktop,
condition: !Util.isDesktop || borderColor != null,
builder: (child) => RoundedWhiteContainer(
padding: Util.isDesktop
? const EdgeInsets.all(16)
: const EdgeInsets.all(12),
borderColor: borderColor,
child: child,
),
child: ConditionalParent(
condition: Util.isDesktop,
condition: Util.isDesktop && borderColor == null,
builder: (child) => Padding(
padding: const EdgeInsets.all(16),
child: child,
@ -51,23 +112,15 @@ class DetailItem extends StatelessWidget {
? Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
disableSelectableText
? Text(
title,
style: STextStyles.itemSubtitle(context),
)
: SelectableText(
title,
style: STextStyles.itemSubtitle(context),
if (expandDetail)
const SizedBox(
width: 16,
),
disableSelectableText
? Text(
detail,
style: detailStyle,
)
: SelectableText(
detail,
style: detailStyle,
ConditionalParent(
condition: expandDetail,
builder: (child) => Expanded(child: child),
child: detail,
),
],
)
@ -77,47 +130,17 @@ class DetailItem extends StatelessWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
disableSelectableText
? Text(
title,
style: STextStyles.itemSubtitle(context),
)
: SelectableText(
title,
style: STextStyles.itemSubtitle(context),
),
button ?? Container(),
],
),
const SizedBox(
height: 5,
),
detail.isEmpty && showEmptyDetail
? disableSelectableText
? Text(
"$title will appear here",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle3,
),
)
: SelectableText(
"$title will appear here",
style: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle3,
),
)
: disableSelectableText
? Text(
detail,
style: detailStyle,
)
: SelectableText(
detail,
style: detailStyle,
ConditionalParent(
condition: expandDetail,
builder: (child) => Expanded(child: child),
child: detail,
),
],
),

View file

@ -11,7 +11,10 @@ class QR extends StatelessWidget {
@override
Widget build(BuildContext context) {
return QrImageView(
return SizedBox(
width: size,
height: size,
child: QrImageView(
data: data,
size: size,
padding: padding ?? const EdgeInsets.all(10),
@ -22,6 +25,7 @@ class QR extends StatelessWidget {
// foregroundColor: Theme.of(context)
// .extension<StackColors>()!
// .accentColorDark,
),
);
}
}