Merge pull request #620 from cypherstack/nano_rep_change

bug fixes and ability to change wallet representative in banano and nano wallets
This commit is contained in:
julian-CStack 2023-07-21 15:21:48 -06:00 committed by GitHub
commit d8ad3cecb5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 674 additions and 33 deletions

View file

@ -142,7 +142,7 @@ class _AddCustomTokenViewState extends ConsumerState<AddCustomTokenView> {
context: context, context: context,
message: "Looking up contract", message: "Looking up contract",
); );
currentToken = response.value; currentToken = response!.value;
if (currentToken != null) { if (currentToken != null) {
nameController.text = currentToken!.name; nameController.text = currentToken!.name;
symbolController.text = currentToken!.symbol; symbolController.text = currentToken!.symbol;
@ -157,7 +157,7 @@ class _AddCustomTokenViewState extends ConsumerState<AddCustomTokenView> {
context: context, context: context,
builder: (context) => StackOkDialog( builder: (context) => StackOkDialog(
title: "Failed to look up token", title: "Failed to look up token",
message: response.exception?.message, message: response!.exception?.message,
), ),
), ),
); );

View file

@ -164,7 +164,7 @@ class _InstallThemeFromFileDialogState
); );
if (mounted) { if (mounted) {
Navigator.of(context).pop(); Navigator.of(context).pop();
if (!result) { if (!result!) {
unawaited( unawaited(
showDialog( showDialog(
context: context, context: context,

View file

@ -72,11 +72,11 @@ class _StackThemeCardState extends ConsumerState<StackThemeCard> {
} }
Future<void> _downloadPressed() async { Future<void> _downloadPressed() async {
final result = await showLoading( final result = (await showLoading(
whileFuture: _downloadAndInstall(), whileFuture: _downloadAndInstall(),
context: context, context: context,
message: "Downloading and installing theme...", message: "Downloading and installing theme...",
); ))!;
if (mounted) { if (mounted) {
final message = result final message = result

View file

@ -20,11 +20,12 @@ import 'package:stackwallet/pages/home_view/home_view.dart';
import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/xpub_view.dart';
import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart'; import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart';
import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/route_generator.dart';
@ -231,7 +232,7 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
.mnemonic; .mnemonic;
if (mounted) { if (mounted) {
Navigator.push( await Navigator.push(
context, context,
RouteGenerator.getRoute( RouteGenerator.getRoute(
shouldUseMaterialRoute: shouldUseMaterialRoute:
@ -305,6 +306,25 @@ class _WalletSettingsViewState extends ConsumerState<WalletSettingsView> {
); );
}, },
), ),
if (coin == Coin.nano || coin == Coin.banano)
const SizedBox(
height: 8,
),
if (coin == Coin.nano || coin == Coin.banano)
Consumer(
builder: (_, ref, __) {
return SettingsListButton(
iconAssetName: Assets.svg.eye,
title: "Change representative",
onPressed: () {
Navigator.of(context).pushNamed(
ChangeRepresentativeView.routeName,
arguments: widget.walletId,
);
},
);
},
),
const SizedBox( const SizedBox(
height: 8, height: 8,
), ),
@ -434,18 +454,20 @@ class _EpiBoxInfoFormState extends ConsumerState<EpicBoxInfoForm> {
TextButton( TextButton(
onPressed: () async { onPressed: () async {
try { try {
wallet.updateEpicboxConfig( await wallet.updateEpicboxConfig(
hostController.text, hostController.text,
int.parse(portController.text), int.parse(portController.text),
); );
showFloatingFlushBar( if (mounted) {
await showFloatingFlushBar(
context: context, context: context,
message: "Epicbox info saved!", message: "Epicbox info saved!",
type: FlushBarType.success, type: FlushBarType.success,
); );
wallet.refresh(); }
unawaited(wallet.refresh());
} catch (e) { } catch (e) {
showFloatingFlushBar( await showFloatingFlushBar(
context: context, context: context,
message: "Failed to save epicbox info: $e", message: "Failed to save epicbox info: $e",
type: FlushBarType.warning, type: FlushBarType.warning,

View file

@ -0,0 +1,367 @@
/*
* 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:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/notifications/show_flush_bar.dart';
import 'package:stackwallet/providers/global/wallets_provider.dart';
import 'package:stackwallet/services/coins/banano/banano_wallet.dart';
import 'package:stackwallet/services/coins/nano/nano_wallet.dart';
import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/clipboard_interface.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/show_loading.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/util.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog.dart';
import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart';
import 'package:stackwallet/widgets/desktop/primary_button.dart';
import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
import 'package:stackwallet/widgets/loading_indicator.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/stack_text_field.dart';
import 'package:stackwallet/widgets/textfield_icon_button.dart';
class ChangeRepresentativeView extends ConsumerStatefulWidget {
const ChangeRepresentativeView({
Key? key,
required this.walletId,
this.clipboardInterface = const ClipboardWrapper(),
}) : super(key: key);
final String walletId;
final ClipboardInterface clipboardInterface;
static const String routeName = "/changeRepresentative";
@override
ConsumerState<ChangeRepresentativeView> createState() => _XPubViewState();
}
class _XPubViewState extends ConsumerState<ChangeRepresentativeView> {
final _textController = TextEditingController();
final _textFocusNode = FocusNode();
final bool isDesktop = Util.isDesktop;
late ClipboardInterface _clipboardInterface;
String? representative;
Future<String> loadRepresentative() async {
final manager =
ref.read(walletsChangeNotifierProvider).getManager(widget.walletId);
if (manager.coin == Coin.nano) {
return (manager.wallet as NanoWallet).getCurrentRepresentative();
} else if (manager.coin == Coin.banano) {
return (manager.wallet as BananoWallet).getCurrentRepresentative();
}
throw Exception("Unsupported wallet attempted to show representative!");
}
Future<void> _save() async {
final manager =
ref.read(walletsChangeNotifierProvider).getManager(widget.walletId);
final changeFuture = manager.coin == Coin.nano
? (manager.wallet as NanoWallet).changeRepresentative
: (manager.wallet as BananoWallet).changeRepresentative;
final result = await showLoading(
whileFuture: changeFuture(_textController.text),
context: context,
message: "Updating representative...",
onException: (ex) {
String msg = ex.toString();
while (msg.isNotEmpty && msg.startsWith("Exception:")) {
msg = msg.substring(10).trim();
}
showFloatingFlushBar(
type: FlushBarType.warning,
message: msg,
context: context,
);
});
if (mounted) {
if (result != null && result) {
setState(() {
representative = _textController.text;
_textController.text = "";
});
await showFloatingFlushBar(
type: FlushBarType.success,
message: "Representative changed",
context: context,
);
}
}
}
@override
void initState() {
_clipboardInterface = widget.clipboardInterface;
super.initState();
}
@override
void dispose() {
_textController.dispose();
_textFocusNode.dispose();
super.dispose();
}
Future<void> _copy() async {
await _clipboardInterface
.setData(ClipboardData(text: representative ?? ""));
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(
child: SafeArea(
child: Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: AppBarBackButton(
onPressed: () async {
Navigator.of(context).pop();
},
),
title: Text(
"Wallet representative",
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 (representative != null) {
_copy();
}
},
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.only(
top: 12,
left: 16,
right: 16,
),
child: child,
),
),
),
),
child: ConditionalParent(
condition: isDesktop,
builder: (child) => DesktopDialog(
maxWidth: 600,
maxHeight: double.infinity,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(
left: 32,
),
child: Text(
"${ref.watch(walletsChangeNotifierProvider.select((value) => value.getManager(widget.walletId).walletName))} xPub",
style: STextStyles.desktopH2(context),
),
),
DesktopDialogCloseButton(
onPressedOverride: Navigator.of(
context,
rootNavigator: true,
).pop,
),
],
),
AnimatedSize(
duration: const Duration(
milliseconds: 150,
),
child: Padding(
padding: const EdgeInsets.fromLTRB(32, 0, 32, 32),
child: child,
),
),
],
),
),
child: Column(
children: [
if (isDesktop) const SizedBox(height: 44),
ConditionalParent(
condition: !isDesktop,
builder: (child) => Expanded(
child: child,
),
child: FutureBuilder(
future: loadRepresentative(),
builder: (context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.hasData) {
representative = snapshot.data!;
}
const height = 600.0;
Widget child;
if (representative == null) {
child = const SizedBox(
key: Key("loadingRepresentative"),
height: height,
child: Center(
child: LoadingIndicator(
width: 100,
),
),
);
} else {
child = Column(
children: [
RoundedWhiteContainer(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
representative!,
style: STextStyles.itemSubtitle(context),
),
],
),
),
const SizedBox(
height: 24,
),
ClipRRect(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
child: TextField(
autocorrect: Util.isDesktop ? false : true,
enableSuggestions: Util.isDesktop ? false : true,
controller: _textController,
style: isDesktop
? STextStyles.desktopTextExtraSmall(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textFieldActiveText,
height: 1.8,
)
: STextStyles.field(context),
focusNode: _textFocusNode,
decoration: standardInputDecoration(
"Enter new representative",
_textFocusNode,
context,
desktopMed: isDesktop,
).copyWith(
contentPadding: isDesktop
? const EdgeInsets.only(
left: 16,
top: 11,
bottom: 12,
right: 5,
)
: null,
suffixIcon: _textController.text.isNotEmpty
? Padding(
padding: const EdgeInsets.only(right: 0),
child: UnconstrainedBox(
child: Row(
children: [
TextFieldIconButton(
child: const XIcon(),
onTap: () async {
setState(() {
_textController.text = "";
});
},
),
],
),
),
)
: null,
),
),
),
if (!isDesktop) const Spacer(),
PrimaryButton(
label: "Save",
onPressed: _save,
),
if (!isDesktop)
const SizedBox(
height: 16,
),
],
);
}
return AnimatedSwitcher(
duration: const Duration(
milliseconds: 200,
),
child: child,
);
},
),
),
],
),
),
);
}
}

View file

@ -100,7 +100,7 @@ class _MyTokenSelectItemState extends ConsumerState<MyTokenSelectItem> {
message: "Loading ${widget.token.name}", message: "Loading ${widget.token.name}",
); );
if (!success) { if (!success!) {
return; return;
} }

View file

@ -13,7 +13,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/xpub_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart';
import 'package:stackwallet/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart'; import 'package:stackwallet/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart';
import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart';
import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/providers.dart';

View file

@ -125,7 +125,7 @@ class _CreatePasswordViewState extends ConsumerState<CreatePasswordView> {
} }
} }
if (!widget.restoreFromSWB) { if (!widget.restoreFromSWB && mounted) {
unawaited(showFloatingFlushBar( unawaited(showFloatingFlushBar(
type: FlushBarType.success, type: FlushBarType.success,
message: "Your password is set up", message: "Your password is set up",

View file

@ -84,10 +84,10 @@ class _DesktopLoginViewState extends ConsumerState<DesktopLoginView> {
unawaited( unawaited(
showDialog( showDialog(
context: context, context: context,
builder: (context) => Column( builder: (context) => const Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: const [ children: [
LoadingIndicator( LoadingIndicator(
width: 200, width: 200,
height: 200, height: 200,

View file

@ -103,14 +103,15 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/support_vi
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart';
import 'package:stackwallet/pages/settings_views/global_settings_view/xpub_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart';
import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart';
import 'package:stackwallet/pages/stack_privacy_calls.dart'; import 'package:stackwallet/pages/stack_privacy_calls.dart';
import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; import 'package:stackwallet/pages/token_view/my_tokens_view.dart';
import 'package:stackwallet/pages/token_view/token_contract_details_view.dart'; import 'package:stackwallet/pages/token_view/token_contract_details_view.dart';
@ -564,6 +565,20 @@ class RouteGenerator {
} }
return _routeError("${settings.name} invalid args: ${args.toString()}"); return _routeError("${settings.name} invalid args: ${args.toString()}");
case ChangeRepresentativeView.routeName:
if (args is String) {
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => ChangeRepresentativeView(
walletId: args,
),
settings: RouteSettings(
name: settings.name,
),
);
}
return _routeError("${settings.name} invalid args: ${args.toString()}");
case AppearanceSettingsView.routeName: case AppearanceSettingsView.routeName:
return getRoute( return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute, shouldUseMaterialRoute: useMaterialPageRoute,

View file

@ -14,9 +14,9 @@ import 'package:stackwallet/services/event_bus/events/global/node_connection_sta
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/nano_api.dart';
import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
@ -32,8 +32,7 @@ const int MINIMUM_CONFIRMATIONS = 1;
const String DEFAULT_REPRESENTATIVE = const String DEFAULT_REPRESENTATIVE =
"ban_1ka1ium4pfue3uxtntqsrib8mumxgazsjf58gidh1xeo5te3whsq8z476goo"; "ban_1ka1ium4pfue3uxtntqsrib8mumxgazsjf58gidh1xeo5te3whsq8z476goo";
class BananoWallet extends CoinServiceAPI class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB {
with WalletCache, WalletDB, CoinControlInterface {
BananoWallet({ BananoWallet({
required String walletId, required String walletId,
required String walletName, required String walletName,
@ -925,4 +924,51 @@ class BananoWallet extends CoinServiceAPI
); );
await updateCachedChainHeight(height ?? 0); await updateCachedChainHeight(height ?? 0);
} }
Future<String> getCurrentRepresentative() async {
final serverURI = Uri.parse(getCurrentNode().host);
final address = await currentReceivingAddress;
final response = await NanoAPI.getAccountInfo(
server: serverURI,
representative: true,
account: address,
);
return response.accountInfo?.representative ?? DEFAULT_REPRESENTATIVE;
}
Future<bool> changeRepresentative(String newRepresentative) async {
try {
final serverURI = Uri.parse(getCurrentNode().host);
final balance = this.balance.spendable.raw.toString();
final String privateKey = await getPrivateKeyFromMnemonic();
final address = await currentReceivingAddress;
final response = await NanoAPI.getAccountInfo(
server: serverURI,
representative: true,
account: address,
);
if (response.accountInfo == null) {
throw response.exception ?? Exception("Failed to get account info");
}
final work = await requestWork(response.accountInfo!.frontier);
return await NanoAPI.changeRepresentative(
server: serverURI,
accountType: NanoAccountType.BANANO,
account: address,
newRepresentative: newRepresentative,
previousBlock: response.accountInfo!.frontier,
balance: balance,
privateKey: privateKey,
work: work!,
);
} catch (_) {
rethrow;
}
}
} }

View file

@ -24,9 +24,9 @@ import 'package:stackwallet/services/event_bus/events/global/node_connection_sta
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/coin_control_interface.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/nano_api.dart';
import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/amount/amount.dart';
@ -42,8 +42,7 @@ const int MINIMUM_CONFIRMATIONS = 1;
const String DEFAULT_REPRESENTATIVE = const String DEFAULT_REPRESENTATIVE =
"nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579"; "nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579";
class NanoWallet extends CoinServiceAPI class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB {
with WalletCache, WalletDB, CoinControlInterface {
NanoWallet({ NanoWallet({
required String walletId, required String walletId,
required String walletName, required String walletName,
@ -937,4 +936,51 @@ class NanoWallet extends CoinServiceAPI
); );
await updateCachedChainHeight(height ?? 0); await updateCachedChainHeight(height ?? 0);
} }
Future<String> getCurrentRepresentative() async {
final serverURI = Uri.parse(getCurrentNode().host);
final address = await currentReceivingAddress;
final response = await NanoAPI.getAccountInfo(
server: serverURI,
representative: true,
account: address,
);
return response.accountInfo?.representative ?? DEFAULT_REPRESENTATIVE;
}
Future<bool> changeRepresentative(String newRepresentative) async {
try {
final serverURI = Uri.parse(getCurrentNode().host);
final balance = this.balance.spendable.raw.toString();
final String privateKey = await getPrivateKeyFromMnemonic();
final address = await currentReceivingAddress;
final response = await NanoAPI.getAccountInfo(
server: serverURI,
representative: true,
account: address,
);
if (response.accountInfo == null) {
throw response.exception ?? Exception("Failed to get account info");
}
final work = await requestWork(response.accountInfo!.frontier);
return await NanoAPI.changeRepresentative(
server: serverURI,
accountType: NanoAccountType.NANO,
account: address,
newRepresentative: newRepresentative,
previousBlock: response.accountInfo!.frontier,
balance: balance,
privateKey: privateKey,
work: work!,
);
} catch (_) {
rethrow;
}
}
} }

View file

@ -10,7 +10,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:decimal/decimal.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart'; import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart';
import 'package:stackwallet/dto/ethereum/eth_token_tx_extra_dto.dart'; import 'package:stackwallet/dto/ethereum/eth_token_tx_extra_dto.dart';
@ -431,10 +430,10 @@ abstract class EthereumAPI {
final map = json["data"].first as Map; final map = json["data"].first as Map;
final balance = final balance =
Decimal.tryParse(map["balance"].toString()) ?? Decimal.zero; BigInt.tryParse(map["units"].toString()) ?? BigInt.zero;
return EthereumResponse( return EthereumResponse(
Amount.fromDecimal(balance, fractionDigits: map["decimals"] as int), Amount(rawValue: balance, fractionDigits: map["decimals"] as int),
null, null,
); );
} else { } else {

130
lib/services/nano_api.dart Normal file
View file

@ -0,0 +1,130 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:nanodart/nanodart.dart';
class NanoAPI {
static Future<
({
NAccountInfo? accountInfo,
Exception? exception,
})> getAccountInfo({
required Uri server,
required bool representative,
required String account,
}) async {
NAccountInfo? accountInfo;
Exception? exception;
try {
final response = await http.post(
server,
headers: {
"Content-Type": "application/json",
},
body: jsonEncode({
"action": "account_info",
"representative": "true",
"account": account,
}),
);
final map = jsonDecode(response.body);
if (map is Map && map["error"] != null) {
throw Exception(map["error"].toString());
}
accountInfo = NAccountInfo(
frontier: map["frontier"] as String,
representative: map["representative"] as String,
);
} on Exception catch (e) {
exception = e;
} catch (e) {
exception = Exception(e.toString());
}
return (accountInfo: accountInfo, exception: exception);
}
static Future<bool> changeRepresentative({
required Uri server,
required int accountType,
required String account,
required String newRepresentative,
required String previousBlock,
required String balance,
required String privateKey,
required String work,
}) async {
Map<String, String> block = {
"type": "state",
"account": account,
"previous": previousBlock,
"representative": newRepresentative,
"balance": balance,
"link":
"0000000000000000000000000000000000000000000000000000000000000000",
"work": work,
};
final String hash;
try {
hash = NanoBlocks.computeStateHash(
accountType,
account,
previousBlock,
newRepresentative,
BigInt.parse(balance),
block["link"] as String,
);
} catch (e) {
if (e is RangeError) {
throw Exception("Invalid representative format");
}
rethrow;
}
final signature = NanoSignatures.signBlock(hash, privateKey);
block["signature"] = signature;
final map = await postBlock(server: server, block: block);
if (map is Map && map["error"] != null) {
throw Exception(map["error"].toString());
}
return map["error"] == null;
}
// TODO: GET RID OF DYNAMIC AND USED TYPED DATA
static Future<dynamic> postBlock({
required Uri server,
required Map<String, dynamic> block,
}) async {
final response = await http.post(
server,
headers: {
"Content-Type": "application/json",
},
body: jsonEncode({
"action": "process",
"json_block": "true",
"subtype": "change",
"block": block,
}),
);
return jsonDecode(response.body);
}
}
class NAccountInfo {
final String frontier;
final String representative;
NAccountInfo({required this.frontier, required this.representative});
}

View file

@ -12,15 +12,17 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/themes/stack_colors.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart';
Future<T> showLoading<T>({ Future<T?> showLoading<T>({
required Future<T> whileFuture, required Future<T> whileFuture,
required BuildContext context, required BuildContext context,
required String message, required String message,
String? subMessage, String? subMessage,
bool isDesktop = false, bool isDesktop = false,
bool opaqueBG = false, bool opaqueBG = false,
void Function(Exception)? onException,
}) async { }) async {
unawaited( unawaited(
showDialog<void>( showDialog<void>(
@ -43,10 +45,24 @@ Future<T> showLoading<T>({
), ),
); );
final result = await whileFuture; Exception? ex;
T? result;
try {
result = await whileFuture;
} catch (e, s) {
Logging.instance.log(
"showLoading caught: $e\n$s",
level: LogLevel.Warning,
);
ex = e is Exception ? e : Exception(e.toString());
}
if (context.mounted) { if (context.mounted) {
Navigator.of(context, rootNavigator: isDesktop).pop(); Navigator.of(context, rootNavigator: isDesktop).pop();
if (ex != null) {
onException?.call(ex);
}
} }
return result; return result;

View file

@ -140,7 +140,7 @@ class SimpleWalletCard extends ConsumerWidget {
isDesktop: Util.isDesktop, isDesktop: Util.isDesktop,
); );
if (!success) { if (!success!) {
// TODO: show error dialog here? // TODO: show error dialog here?
Logging.instance.log( Logging.instance.log(
"Failed to load token wallet for $contract", "Failed to load token wallet for $contract",