diff --git a/assets/svg/coin_icons/Wownero.svg b/assets/svg/coin_icons/Wownero.svg
new file mode 100644
index 000000000..f7a90e94c
--- /dev/null
+++ b/assets/svg/coin_icons/Wownero.svg
@@ -0,0 +1,7 @@
+
diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero
index 4bffa40cb..66c311b9c 160000
--- a/crypto_plugins/flutter_libmonero
+++ b/crypto_plugins/flutter_libmonero
@@ -1 +1 @@
-Subproject commit 4bffa40cb60ad3d98cf0ea5b5d819f3f4895dcd6
+Subproject commit 66c311b9c9799d3d81e2297a9982797603ee989f
diff --git a/lib/main.dart b/lib/main.dart
index be1d11d0c..0be26c39e 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -8,6 +8,7 @@ import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_libmonero/monero/monero.dart';
+import 'package:flutter_libmonero/wownero/wownero.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hive_flutter/hive_flutter.dart';
@@ -84,7 +85,6 @@ void main() async {
appDirectory = (await getLibraryDirectory());
}
// FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
- await Hive.initFlutter(appDirectory.path);
if (!(Logging.isArmLinux || Logging.isTestEnv)) {
final isar = await Isar.open(
[LogSchema],
@@ -128,11 +128,14 @@ void main() async {
Hive.registerAdapter(NodeAdapter());
- Hive.registerAdapter(WalletInfoAdapter());
+ if (!Hive.isAdapterRegistered(WalletInfoAdapter().typeId)) {
+ Hive.registerAdapter(WalletInfoAdapter());
+ }
Hive.registerAdapter(WalletTypeAdapter());
Hive.registerAdapter(UnspentCoinsInfoAdapter());
+ await Hive.initFlutter(appDirectory.path);
await Hive.openBox(DB.boxNameDBInfo);
int dbVersion = DB.instance.get(
@@ -143,6 +146,7 @@ void main() async {
}
monero.onStartup();
+ wownero.onStartup();
await Hive.openBox(DB.boxNameTheme);
diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart
index 3aaee6607..68f116a95 100644
--- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart
+++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart
@@ -200,7 +200,7 @@ class _RestoreWalletViewState extends ConsumerState {
// TODO: do actual check to make sure it is a valid mnemonic for monero
if (bip39.validateMnemonic(mnemonic) == false &&
- !(widget.coin == Coin.monero)) {
+ !(widget.coin == Coin.monero || widget.coin == Coin.wownero)) {
unawaited(showFloatingFlushBar(
type: FlushBarType.warning,
message: "Invalid seed phrase!",
diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart
index e677ab5c0..7528dacc8 100644
--- a/lib/pages/exchange_view/send_from_view.dart
+++ b/lib/pages/exchange_view/send_from_view.dart
@@ -85,7 +85,7 @@ class _SendFromViewState extends ConsumerState {
height: 8,
),
Text(
- "You need to send ${amount.toStringAsFixed(coin == Coin.monero ? 12 : 8)} ${coin.ticker}",
+ "You need to send ${amount.toStringAsFixed(coin == Coin.monero || coin == Coin.wownero ? 12 : 8)} ${coin.ticker}",
style: STextStyles.itemSubtitle(context),
),
const SizedBox(
@@ -307,7 +307,10 @@ class _SendFromCardState extends ConsumerState {
"${Format.localizedStringAsFixed(
value: snapshot.data!,
locale: locale,
- decimalPlaces: coin == Coin.monero ? 12 : 8,
+ decimalPlaces:
+ coin == Coin.monero || coin == Coin.wownero
+ ? 12
+ : 8,
)} ${coin.ticker}",
style: STextStyles.itemSubtitle(context),
);
diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart
index 2f8eca393..143b1e84d 100644
--- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart
+++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart
@@ -90,6 +90,7 @@ class _AddEditNodeViewState extends ConsumerState {
break;
case Coin.monero:
+ case Coin.wownero:
try {
final uri = Uri.parse(formData.host!);
if (uri.scheme.startsWith("http")) {
@@ -384,6 +385,7 @@ class _AddEditNodeViewState extends ConsumerState {
// strip unused path
String address = formData.host!;
if (coin == Coin.monero ||
+ coin == Coin.wownero ||
coin == Coin.epicCash) {
if (address.startsWith("http")) {
final uri = Uri.parse(address);
@@ -539,6 +541,7 @@ class _NodeFormState extends ConsumerState {
case Coin.epicCash:
case Coin.monero:
+ case Coin.wownero:
return true;
}
}
@@ -699,7 +702,9 @@ class _NodeFormState extends ConsumerState {
focusNode: _hostFocusNode,
style: STextStyles.field(context),
decoration: standardInputDecoration(
- (widget.coin != Coin.monero && widget.coin != Coin.epicCash)
+ (widget.coin != Coin.monero &&
+ widget.coin != Coin.wownero &&
+ widget.coin != Coin.epicCash)
? "IP address"
: "Url",
_hostFocusNode,
@@ -880,7 +885,9 @@ class _NodeFormState extends ConsumerState {
const SizedBox(
height: 8,
),
- if (widget.coin != Coin.monero && widget.coin != Coin.epicCash)
+ if (widget.coin != Coin.monero &&
+ widget.coin != Coin.wownero &&
+ widget.coin != Coin.epicCash)
Row(
children: [
GestureDetector(
@@ -931,11 +938,15 @@ class _NodeFormState extends ConsumerState {
),
],
),
- if (widget.coin != Coin.monero && widget.coin != Coin.epicCash)
+ if (widget.coin != Coin.monero &&
+ widget.coin != Coin.wownero &&
+ widget.coin != Coin.epicCash)
const SizedBox(
height: 8,
),
- if (widget.coin != Coin.monero && widget.coin != Coin.epicCash)
+ if (widget.coin != Coin.monero &&
+ widget.coin != Coin.wownero &&
+ widget.coin != Coin.epicCash)
Row(
children: [
GestureDetector(
diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart
index 340bd859b..8baddb700 100644
--- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart
+++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/node_details_view.dart
@@ -81,6 +81,7 @@ class _NodeDetailsViewState extends ConsumerState {
break;
case Coin.monero:
+ case Coin.wownero:
try {
final uri = Uri.parse(node!.host);
if (uri.scheme.startsWith("http")) {
diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart
index 892cd13d8..c4045dc9b 100644
--- a/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart
+++ b/lib/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart
@@ -12,6 +12,7 @@ import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_net
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
import 'package:stackwallet/services/coins/monero/monero_wallet.dart';
+import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart';
import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart';
@@ -205,7 +206,7 @@ class _WalletNetworkSettingsViewState
.getManager(widget.walletId)
.coin;
- if (coin == Coin.monero || coin == Coin.epicCash) {
+ if (coin == Coin.monero || coin == Coin.wownero || coin == Coin.epicCash) {
_blocksRemainingSubscription = eventBus.on().listen(
(event) async {
if (event.walletId == widget.walletId) {
@@ -271,6 +272,15 @@ class _WalletNetworkSettingsViewState
if (_percent < highestPercent) {
_percent = highestPercent.clamp(0.0, 1.0);
}
+ } else if (coin == Coin.wownero) {
+ double highestPercent = (ref
+ .read(walletsChangeNotifierProvider)
+ .getManager(widget.walletId)
+ .wallet as WowneroWallet)
+ .highestPercentCached;
+ if (_percent < highestPercent) {
+ _percent = highestPercent.clamp(0.0, 1.0);
+ }
} else if (coin == Coin.epicCash) {
double highestPercent = (ref
.read(walletsChangeNotifierProvider)
@@ -545,6 +555,7 @@ class _WalletNetworkSettingsViewState
),
),
if (coin == Coin.monero ||
+ coin == Coin.wownero ||
coin == Coin.epicCash)
Text(
" (Blocks to go: ${_blocksRemaining == -1 ? "?" : _blocksRemaining})",
diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart
index 391b5caec..f68a72c3c 100644
--- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart
+++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart
@@ -239,7 +239,7 @@ class _TransactionDetailsViewState
children: [
SelectableText(
"$amountPrefix${Format.localizedStringAsFixed(
- value: coin == Coin.monero
+ value: coin == Coin.monero || coin == Coin.wownero
? (amount / 10000.toDecimal()).toDecimal()
: amount,
locale: ref.watch(
@@ -254,7 +254,7 @@ class _TransactionDetailsViewState
height: 2,
),
SelectableText(
- "${Format.localizedStringAsFixed(value: (coin == Coin.monero ? (amount / 10000.toDecimal()).toDecimal() : amount) * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1)), locale: ref.watch(
+ "${Format.localizedStringAsFixed(value: (coin == Coin.monero || coin == Coin.wownero ? (amount / 10000.toDecimal()).toDecimal() : amount) * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1)), locale: ref.watch(
localeServiceChangeNotifierProvider
.select((value) => value.locale),
), decimalPlaces: 2)} ${ref.watch(
@@ -298,14 +298,14 @@ class _TransactionDetailsViewState
],
),
),
- if (!(coin == Coin.monero &&
+ if (!((coin == Coin.monero || coin == Coin.wownero) &&
_transaction.txType.toLowerCase() == "sent") &&
!((coin == Coin.firo || coin == Coin.firoTestNet) &&
_transaction.subType == "mint"))
const SizedBox(
height: 12,
),
- if (!(coin == Coin.monero &&
+ if (!((coin == Coin.monero || coin == Coin.wownero) &&
_transaction.txType.toLowerCase() == "sent") &&
!((coin == Coin.firo || coin == Coin.firoTestNet) &&
_transaction.subType == "mint"))
@@ -462,7 +462,7 @@ class _TransactionDetailsViewState
showFeePending
? _transaction.confirmedStatus
? Format.localizedStringAsFixed(
- value: coin == Coin.monero
+ value: coin == Coin.monero || coin == Coin.wownero
? (fee / 10000.toDecimal()).toDecimal()
: fee,
locale: ref.watch(
@@ -471,7 +471,7 @@ class _TransactionDetailsViewState
decimalPlaces: Constants.decimalPlaces)
: "Pending"
: Format.localizedStringAsFixed(
- value: coin == Coin.monero
+ value: coin == Coin.monero || coin == Coin.wownero
? (fee / 10000.toDecimal()).toDecimal()
: fee,
locale: ref.watch(
diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart
index 728706702..028c04416 100644
--- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart
+++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart
@@ -750,7 +750,7 @@ class _TransactionSearchViewState
}
int? amount;
if (amountDecimal != null) {
- if (widget.coin == Coin.monero) {
+ if (widget.coin == Coin.monero || widget.coin == Coin.wownero) {
amount = (amountDecimal * Decimal.fromInt(Constants.satsPerCoinMonero))
.floor()
.toBigInt()
diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart
index 7a0dec2e1..089b20a33 100644
--- a/lib/services/coins/coin_service.dart
+++ b/lib/services/coins/coin_service.dart
@@ -9,6 +9,7 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/services/coins/monero/monero_wallet.dart';
+import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart';
import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
@@ -145,6 +146,14 @@ abstract class CoinServiceAPI {
// tracker: tracker,
);
+ case Coin.wownero:
+ return WowneroWallet(
+ walletId: walletId,
+ walletName: walletName,
+ coin: coin,
+ // tracker: tracker,
+ );
+
case Coin.namecoin:
return NamecoinWallet(
walletId: walletId,
diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart
new file mode 100644
index 000000000..857b89031
--- /dev/null
+++ b/lib/services/coins/wownero/wownero_wallet.dart
@@ -0,0 +1,1560 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:cw_core/monero_transaction_priority.dart';
+import 'package:cw_core/node.dart';
+import 'package:cw_core/pending_transaction.dart';
+import 'package:cw_core/sync_status.dart';
+import 'package:cw_core/transaction_direction.dart';
+import 'package:cw_core/wallet_base.dart';
+import 'package:cw_core/wallet_credentials.dart';
+import 'package:cw_core/wallet_info.dart';
+import 'package:cw_core/wallet_service.dart';
+import 'package:cw_core/wallet_type.dart';
+import 'package:cw_wownero/api/exceptions/creation_transaction_exception.dart';
+import 'package:cw_wownero/api/wallet.dart';
+import 'package:cw_wownero/pending_wownero_transaction.dart';
+import 'package:cw_wownero/wownero_amount_format.dart';
+import 'package:cw_wownero/wownero_wallet.dart';
+import 'package:dart_numerics/dart_numerics.dart';
+import 'package:decimal/decimal.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_libmonero/core/key_service.dart';
+import 'package:flutter_libmonero/core/wallet_creation_service.dart';
+import 'package:flutter_libmonero/view_model/send/output.dart'
+ as wownero_output;
+import 'package:flutter_libmonero/wownero/wownero.dart';
+import 'package:flutter_secure_storage/flutter_secure_storage.dart';
+import 'package:http/http.dart';
+import 'package:mutex/mutex.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import 'package:stackwallet/hive/db.dart';
+import 'package:stackwallet/models/node_model.dart';
+import 'package:stackwallet/models/paymint/fee_object_model.dart';
+import 'package:stackwallet/models/paymint/transactions_model.dart';
+import 'package:stackwallet/models/paymint/utxo_model.dart';
+import 'package:stackwallet/services/coins/coin_service.dart';
+import 'package:stackwallet/services/event_bus/events/global/blocks_remaining_event.dart';
+import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
+import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_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/global_event_bus.dart';
+import 'package:stackwallet/services/node_service.dart';
+import 'package:stackwallet/services/price.dart';
+import 'package:stackwallet/utilities/constants.dart';
+import 'package:stackwallet/utilities/default_nodes.dart';
+import 'package:stackwallet/utilities/enums/coin_enum.dart';
+import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
+import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
+import 'package:stackwallet/utilities/logger.dart';
+import 'package:stackwallet/utilities/prefs.dart';
+
+const int MINIMUM_CONFIRMATIONS = 10;
+
+//https://github.com/wownero-project/wownero/blob/8361d60aef6e17908658128284899e3a11d808d4/src/cryptonote_config.h#L162
+const String GENESIS_HASH_MAINNET =
+ "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1";
+const String GENESIS_HASH_TESTNET =
+ "013c01ff0001ffffffffffff03029b2e4c0281c0b02e7c53291a94d1d0cbff8883f8024f5142ee494ffbbd08807121017767aafcde9be00dcfd098715ebcf7f410daebc582fda69d24a28e9d0bc890d1";
+
+class WowneroWallet extends CoinServiceAPI {
+ static const integrationTestFlag =
+ bool.fromEnvironment("IS_INTEGRATION_TEST");
+ final _prefs = Prefs.instance;
+
+ Timer? timer;
+ Timer? wowneroAutosaveTimer;
+ late Coin _coin;
+
+ late FlutterSecureStorageInterface _secureStore;
+
+ late PriceAPI _priceAPI;
+
+ Future getCurrentNode() async {
+ return NodeService().getPrimaryNodeFor(coin: coin) ??
+ DefaultNodes.getNodeFor(coin);
+ }
+
+ WowneroWallet(
+ {required String walletId,
+ required String walletName,
+ required Coin coin,
+ PriceAPI? priceAPI,
+ FlutterSecureStorageInterface? secureStore}) {
+ _walletId = walletId;
+ _walletName = walletName;
+ _coin = coin;
+
+ _priceAPI = priceAPI ?? PriceAPI(Client());
+ _secureStore =
+ secureStore ?? const SecureStorageWrapper(FlutterSecureStorage());
+ }
+
+ bool _shouldAutoSync = false;
+
+ @override
+ bool get shouldAutoSync => _shouldAutoSync;
+
+ @override
+ set shouldAutoSync(bool shouldAutoSync) {
+ if (_shouldAutoSync != shouldAutoSync) {
+ _shouldAutoSync = shouldAutoSync;
+ if (!shouldAutoSync) {
+ timer?.cancel();
+ wowneroAutosaveTimer?.cancel();
+ timer = null;
+ wowneroAutosaveTimer = null;
+ stopNetworkAlivePinging();
+ } else {
+ startNetworkAlivePinging();
+ // Walletbase needs to be open for this to work
+ refresh();
+ }
+ }
+ }
+
+ @override
+ Future updateNode(bool shouldRefresh) async {
+ final node = await getCurrentNode();
+
+ final host = Uri.parse(node.host).host;
+ await walletBase?.connectToNode(
+ node: Node(uri: "$host:${node.port}", type: WalletType.wownero));
+
+ // TODO: is this sync call needed? Do we need to notify ui here?
+ await walletBase?.startSync();
+
+ if (shouldRefresh) {
+ await refresh();
+ }
+ }
+
+ Future> _getMnemonicList() async {
+ final mnemonicString =
+ await _secureStore.read(key: '${_walletId}_mnemonic');
+ if (mnemonicString == null) {
+ return [];
+ }
+ final List data = mnemonicString.split(' ');
+ return data;
+ }
+
+ @override
+ Future> get mnemonic => _getMnemonicList();
+
+ Future get currentNodeHeight async {
+ try {
+ if (walletBase!.syncStatus! is SyncedSyncStatus &&
+ walletBase!.syncStatus!.progress() == 1.0) {
+ return await walletBase!.getNodeHeight();
+ }
+ } catch (e, s) {}
+ int _height = -1;
+ try {
+ _height = (walletBase!.syncStatus as SyncingSyncStatus).height;
+ } catch (e, s) {
+ Logging.instance.log("$e $s", level: LogLevel.Warning);
+ }
+
+ int blocksRemaining = -1;
+
+ try {
+ blocksRemaining =
+ (walletBase!.syncStatus as SyncingSyncStatus).blocksLeft;
+ } catch (e, s) {
+ Logging.instance.log("$e $s", level: LogLevel.Warning);
+ }
+ int currentHeight = _height + blocksRemaining;
+ if (_height == -1 || blocksRemaining == -1) {
+ currentHeight = int64MaxValue;
+ }
+ final cachedHeight = DB.instance
+ .get(boxName: walletId, key: "storedNodeHeight") as int? ??
+ 0;
+
+ if (currentHeight > cachedHeight && currentHeight != int64MaxValue) {
+ await DB.instance.put(
+ boxName: walletId, key: "storedNodeHeight", value: currentHeight);
+ return currentHeight;
+ } else {
+ return cachedHeight;
+ }
+ }
+
+ Future get currentSyncingHeight async {
+ //TODO return the tip of the wownero blockchain
+ try {
+ if (walletBase!.syncStatus! is SyncedSyncStatus &&
+ walletBase!.syncStatus!.progress() == 1.0) {
+ Logging.instance
+ .log("currentSyncingHeight lol", level: LogLevel.Warning);
+ return getSyncingHeight();
+ }
+ } catch (e, s) {}
+ int syncingHeight = -1;
+ try {
+ syncingHeight = (walletBase!.syncStatus as SyncingSyncStatus).height;
+ } catch (e, s) {
+ Logging.instance.log("$e $s", level: LogLevel.Warning);
+ }
+ final cachedHeight =
+ DB.instance.get(boxName: walletId, key: "storedSyncingHeight")
+ as int? ??
+ 0;
+
+ if (syncingHeight > cachedHeight) {
+ await DB.instance.put(
+ boxName: walletId, key: "storedSyncingHeight", value: syncingHeight);
+ return syncingHeight;
+ } else {
+ return cachedHeight;
+ }
+ }
+
+ Future updateStoredChainHeight({required int newHeight}) async {
+ await DB.instance.put(
+ boxName: walletId, key: "storedChainHeight", value: newHeight);
+ }
+
+ int get storedChainHeight {
+ return DB.instance.get(boxName: walletId, key: "storedChainHeight")
+ as int? ??
+ 0;
+ }
+
+ /// Increases the index for either the internal or external chain, depending on [chain].
+ /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
+ Future _incrementAddressIndexForChain(int chain) async {
+ // Here we assume chain == 1 if it isn't 0
+ String indexKey = chain == 0 ? "receivingIndex" : "changeIndex";
+
+ final newIndex =
+ (DB.instance.get(boxName: walletId, key: indexKey)) + 1;
+ await DB.instance
+ .put(boxName: walletId, key: indexKey, value: newIndex);
+ }
+
+ Future _checkCurrentReceivingAddressesForTransactions() async {
+ try {
+ await _checkReceivingAddressForTransactions();
+ } catch (e, s) {
+ Logging.instance.log(
+ "Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s",
+ level: LogLevel.Error);
+ rethrow;
+ }
+ }
+
+ Future _checkReceivingAddressForTransactions() async {
+ try {
+ int highestIndex = -1;
+ for (var element
+ in walletBase!.transactionHistory!.transactions!.entries) {
+ if (element.value.direction == TransactionDirection.incoming) {
+ int curAddressIndex =
+ element.value.additionalInfo!['addressIndex'] as int;
+ if (curAddressIndex > highestIndex) {
+ highestIndex = curAddressIndex;
+ }
+ }
+ }
+
+ // Check the new receiving index
+ String indexKey = "receivingIndex";
+ final curIndex =
+ DB.instance.get(boxName: walletId, key: indexKey) as int;
+ if (highestIndex >= curIndex) {
+ // First increment the receiving index
+ await _incrementAddressIndexForChain(0);
+ final newReceivingIndex =
+ DB.instance.get(boxName: walletId, key: indexKey) as int;
+
+ // Use new index to derive a new receiving address
+ final newReceivingAddress =
+ await _generateAddressForChain(0, newReceivingIndex);
+
+ // Add that new receiving address to the array of receiving addresses
+ await _addToAddressesArrayForChain(newReceivingAddress, 0);
+
+ // Set the new receiving address that the service
+
+ _currentReceivingAddress = Future(() => newReceivingAddress);
+ }
+ } on SocketException catch (se, s) {
+ Logging.instance.log(
+ "SocketException caught in _checkReceivingAddressForTransactions(): $se\n$s",
+ level: LogLevel.Error);
+ return;
+ } catch (e, s) {
+ Logging.instance.log(
+ "Exception rethrown from _checkReceivingAddressForTransactions(): $e\n$s",
+ level: LogLevel.Error);
+ rethrow;
+ }
+ }
+
+ @override
+ bool get isRefreshing => refreshMutex;
+
+ bool refreshMutex = false;
+
+ Timer? syncPercentTimer;
+
+ Mutex syncHeightMutex = Mutex();
+ Future stopSyncPercentTimer() async {
+ syncPercentTimer?.cancel();
+ syncPercentTimer = null;
+ }
+
+ Future startSyncPercentTimer() async {
+ if (syncPercentTimer != null) {
+ return;
+ }
+ syncPercentTimer?.cancel();
+ GlobalEventBus.instance
+ .fire(RefreshPercentChangedEvent(highestPercentCached, walletId));
+ syncPercentTimer = Timer.periodic(const Duration(seconds: 30), (_) async {
+ if (syncHeightMutex.isLocked) {
+ return;
+ }
+ await syncHeightMutex.protect(() async {
+ // int restoreheight = walletBase!.walletInfo.restoreHeight ?? 0;
+ int _height = await currentSyncingHeight;
+ int _currentHeight = await currentNodeHeight;
+ double progress = 0;
+ try {
+ progress = walletBase!.syncStatus!.progress();
+ } catch (e, s) {
+ Logging.instance.log("$e $s", level: LogLevel.Warning);
+ }
+
+ final int blocksRemaining = _currentHeight - _height;
+
+ GlobalEventBus.instance
+ .fire(BlocksRemainingEvent(blocksRemaining, walletId));
+
+ if (progress == 1 && _currentHeight > 0 && _height > 0) {
+ await stopSyncPercentTimer();
+ GlobalEventBus.instance.fire(
+ WalletSyncStatusChangedEvent(
+ WalletSyncStatus.synced,
+ walletId,
+ coin,
+ ),
+ );
+ return;
+ }
+
+ // for some reason this can be 0 which screws up the percent calculation
+ // int64MaxValue is NOT the best value to use here
+ if (_currentHeight < 1) {
+ _currentHeight = int64MaxValue;
+ }
+
+ if (_height < 1) {
+ _height = 1;
+ }
+
+ double restorePercent = progress;
+ double highestPercent = highestPercentCached;
+
+ Logging.instance.log(
+ "currentSyncingHeight: $_height, nodeHeight: $_currentHeight, restorePercent: $restorePercent, highestPercentCached: $highestPercentCached",
+ level: LogLevel.Info);
+
+ if (restorePercent > 0 && restorePercent <= 1) {
+ // if (restorePercent > highestPercent) {
+ highestPercent = restorePercent;
+ highestPercentCached = restorePercent;
+ // }
+ }
+
+ GlobalEventBus.instance
+ .fire(RefreshPercentChangedEvent(highestPercent, walletId));
+ });
+ });
+ }
+
+ double get highestPercentCached =>
+ DB.instance.get(boxName: walletId, key: "highestPercentCached")
+ as double? ??
+ 0;
+ set highestPercentCached(double value) => DB.instance.put(
+ boxName: walletId,
+ key: "highestPercentCached",
+ value: value,
+ );
+
+ /// Refreshes display data for the wallet
+ @override
+ Future refresh() async {
+ if (refreshMutex) {
+ Logging.instance.log("$walletId $walletName refreshMutex denied",
+ level: LogLevel.Info);
+ return;
+ } else {
+ refreshMutex = true;
+ }
+
+ if (walletBase == null) {
+ throw Exception("Tried to call refresh() in wownero without walletBase!");
+ }
+
+ try {
+ await startSyncPercentTimer();
+ GlobalEventBus.instance.fire(
+ WalletSyncStatusChangedEvent(
+ WalletSyncStatus.syncing,
+ walletId,
+ coin,
+ ),
+ );
+
+ final int _currentSyncingHeight = await currentSyncingHeight;
+ final int storedHeight = storedChainHeight;
+ int _currentNodeHeight = await currentNodeHeight;
+
+ double progress = 0;
+ try {
+ progress = (walletBase!.syncStatus!).progress();
+ } catch (e, s) {
+ Logging.instance.log("$e $s", level: LogLevel.Warning);
+ }
+ await _fetchTransactionData();
+
+ bool stillSyncing = false;
+ Logging.instance.log(
+ "storedHeight: $storedHeight, _currentSyncingHeight: $_currentSyncingHeight, _currentNodeHeight: $_currentNodeHeight, progress: $progress, issynced: ${await walletBase!.isConnected()}",
+ level: LogLevel.Info);
+
+ if (progress < 1.0) {
+ stillSyncing = true;
+ }
+
+ if (_currentSyncingHeight > storedHeight) {
+ // 0 is returned from wownero as I assume an error?????
+ if (_currentSyncingHeight > 0) {
+ // 0 failed to fetch current height???
+ await updateStoredChainHeight(newHeight: _currentSyncingHeight);
+ }
+ }
+
+ await _checkCurrentReceivingAddressesForTransactions();
+ String indexKey = "receivingIndex";
+ final curIndex =
+ DB.instance.get(boxName: walletId, key: indexKey) as int;
+ // Use new index to derive a new receiving address
+ try {
+ final newReceivingAddress = await _generateAddressForChain(0, curIndex);
+ _currentReceivingAddress = Future(() => newReceivingAddress);
+ } catch (e, s) {
+ Logging.instance.log(
+ "Failed to call _generateAddressForChain(0, $curIndex): $e\n$s",
+ level: LogLevel.Error);
+ }
+ final newTxData = await _fetchTransactionData();
+ _transactionData = Future(() => newTxData);
+
+ if (isActive || shouldAutoSync) {
+ timer ??= Timer.periodic(const Duration(seconds: 60), (timer) async {
+ debugPrint("run timer");
+ //TODO: check for new data and refresh if needed. if wownero even needs this
+ // chain height check currently broken
+ // if ((await chainHeight) != (await storedChainHeight)) {
+ // if (await refreshIfThereIsNewData()) {
+ await refresh();
+ GlobalEventBus.instance.fire(UpdatedInBackgroundEvent(
+ "New data found in $walletId $walletName in background!",
+ walletId));
+ // }
+ // }
+ });
+ wowneroAutosaveTimer ??=
+ Timer.periodic(const Duration(seconds: 93), (timer) async {
+ debugPrint("run wownero timer");
+ if (isActive) {
+ await walletBase?.save();
+ GlobalEventBus.instance.fire(UpdatedInBackgroundEvent(
+ "New data found in $walletId $walletName in background!",
+ walletId));
+ }
+ });
+ }
+
+ if (stillSyncing) {
+ debugPrint("still syncing");
+ GlobalEventBus.instance.fire(
+ WalletSyncStatusChangedEvent(
+ WalletSyncStatus.syncing,
+ walletId,
+ coin,
+ ),
+ );
+ refreshMutex = false;
+ return;
+ }
+ await stopSyncPercentTimer();
+ GlobalEventBus.instance.fire(
+ WalletSyncStatusChangedEvent(
+ WalletSyncStatus.synced,
+ walletId,
+ coin,
+ ),
+ );
+ refreshMutex = false;
+ } catch (error, strace) {
+ refreshMutex = false;
+ await stopSyncPercentTimer();
+ GlobalEventBus.instance.fire(
+ NodeConnectionStatusChangedEvent(
+ NodeConnectionStatus.disconnected,
+ walletId,
+ coin,
+ ),
+ );
+ GlobalEventBus.instance.fire(
+ WalletSyncStatusChangedEvent(
+ WalletSyncStatus.unableToSync,
+ walletId,
+ coin,
+ ),
+ );
+ Logging.instance.log(
+ "Caught exception in refreshWalletData(): $error\n$strace",
+ level: LogLevel.Error);
+ }
+ }
+
+ @override
+ // TODO: implement allOwnAddresses
+ Future> get allOwnAddresses {
+ return Future(() => []);
+ }
+
+ @override
+ Future get balanceMinusMaxFee async =>
+ (await availableBalance) -
+ (Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin))
+ .toDecimal();
+
+ @override
+ Future get currentReceivingAddress =>
+ _currentReceivingAddress ??= _getCurrentAddressForChain(0);
+
+ @override
+ Future exit() async {
+ await stopSyncPercentTimer();
+ _hasCalledExit = true;
+ isActive = false;
+ await walletBase?.save(prioritySave: true);
+ walletBase?.close();
+ wowneroAutosaveTimer?.cancel();
+ wowneroAutosaveTimer = null;
+ timer?.cancel();
+ timer = null;
+ stopNetworkAlivePinging();
+ }
+
+ bool _hasCalledExit = false;
+
+ @override
+ bool get hasCalledExit => _hasCalledExit;
+
+ Future? _currentReceivingAddress;
+
+ Future _getFees() async {
+ return FeeObject(
+ numberOfBlocksFast: 10,
+ numberOfBlocksAverage: 10,
+ numberOfBlocksSlow: 10,
+ fast: 4,
+ medium: 2,
+ slow: 0);
+ }
+
+ @override
+ Future get fees => _feeObject ??= _getFees();
+ Future? _feeObject;
+
+ @override
+ // TODO: implement fullRescan
+ Future fullRescan(
+ int maxUnusedAddressGap,
+ int maxNumberOfIndexesToCheck,
+ ) async {
+ var restoreHeight = walletBase?.walletInfo.restoreHeight;
+ await walletBase?.rescan(height: restoreHeight);
+ GlobalEventBus.instance.fire(
+ WalletSyncStatusChangedEvent(
+ WalletSyncStatus.syncing,
+ walletId,
+ coin,
+ ),
+ );
+ return;
+ }
+
+ Future _generateAddressForChain(int chain, int index) async {
+ //
+ String address = walletBase!.getTransactionAddress(chain, index);
+
+ return address;
+ }
+
+ /// Adds [address] to the relevant chain's address array, which is determined by [chain].
+ /// [address] - Expects a standard native segwit address
+ /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
+ Future _addToAddressesArrayForChain(String address, int chain) async {
+ String chainArray = '';
+ if (chain == 0) {
+ chainArray = 'receivingAddresses';
+ } else {
+ chainArray = 'changeAddresses';
+ }
+
+ final addressArray =
+ DB.instance.get(boxName: walletId, key: chainArray);
+ if (addressArray == null) {
+ Logging.instance.log(
+ 'Attempting to add the following to $chainArray array for chain $chain:${[
+ address
+ ]}',
+ level: LogLevel.Info);
+ await DB.instance
+ .put(boxName: walletId, key: chainArray, value: [address]);
+ } else {
+ // Make a deep copy of the existing list
+ final List newArray = [];
+ addressArray
+ .forEach((dynamic _address) => newArray.add(_address as String));
+ newArray.add(address); // Add the address passed into the method
+ await DB.instance
+ .put(boxName: walletId, key: chainArray, value: newArray);
+ }
+ }
+
+ /// Returns the latest receiving/change (external/internal) address for the wallet depending on [chain]
+ /// and
+ /// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
+ Future _getCurrentAddressForChain(int chain) async {
+ // Here, we assume that chain == 1 if it isn't 0
+ String arrayKey = chain == 0 ? "receivingAddresses" : "changeAddresses";
+ final internalChainArray = (DB.instance
+ .get(boxName: walletId, key: arrayKey)) as List;
+ return internalChainArray.last as String;
+ }
+
+ //TODO: take in the default language when creating wallet.
+ Future _generateNewWallet() async {
+ Logging.instance
+ .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
+ // TODO: ping wownero server and make sure the genesis hash matches
+ // if (!integrationTestFlag) {
+ // final features = await electrumXClient.getServerFeatures();
+ // Logging.instance.log("features: $features");
+ // if (_networkType == BasicNetworkType.main) {
+ // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) {
+ // throw Exception("genesis hash does not match main net!");
+ // }
+ // } else if (_networkType == BasicNetworkType.test) {
+ // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) {
+ // throw Exception("genesis hash does not match test net!");
+ // }
+ // }
+ // }
+
+ // this should never fail
+ if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) {
+ throw Exception(
+ "Attempted to overwrite mnemonic on generate new wallet!");
+ }
+
+ storage = const FlutterSecureStorage();
+ // TODO: Wallet Service may need to be switched to Wownero
+ walletService =
+ wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox);
+ prefs = await SharedPreferences.getInstance();
+ keysStorage = KeyService(storage!);
+ WalletInfo walletInfo;
+ WalletCredentials credentials;
+ try {
+ String name = _walletId;
+ final dirPath =
+ await pathForWalletDir(name: name, type: WalletType.wownero);
+ final path = await pathForWallet(name: name, type: WalletType.wownero);
+ credentials = wownero.createWowneroNewWalletCredentials(
+ name: name,
+ language: "English",
+ );
+
+ walletInfo = WalletInfo.external(
+ id: WalletBase.idFor(name, WalletType.wownero),
+ name: name,
+ type: WalletType.wownero,
+ isRecovery: false,
+ restoreHeight: credentials.height ?? 0,
+ date: DateTime.now(),
+ path: path,
+ dirPath: dirPath,
+ // TODO: find out what to put for address
+ address: '');
+ credentials.walletInfo = walletInfo;
+
+ _walletCreationService = WalletCreationService(
+ secureStorage: storage,
+ sharedPreferences: prefs,
+ walletService: walletService,
+ keyService: keysStorage,
+ );
+ _walletCreationService?.changeWalletType();
+ // To restore from a seed
+ final wallet = await _walletCreationService?.create(credentials);
+
+ // subtract a couple days to ensure we have a buffer for SWB
+ final bufferedCreateHeight =
+ getSeedHeightSync(wallet?.seed.trim() as String);
+
+ await DB.instance.put(
+ boxName: walletId, key: "restoreHeight", value: bufferedCreateHeight);
+ walletInfo.restoreHeight = bufferedCreateHeight;
+
+ await _secureStore.write(
+ key: '${_walletId}_mnemonic', value: wallet?.seed.trim());
+ walletInfo.address = wallet?.walletAddresses.address;
+ await DB.instance
+ .add(boxName: WalletInfo.boxName, value: walletInfo);
+ walletBase?.close();
+ walletBase = wallet as WowneroWalletBase;
+ } catch (e, s) {
+ debugPrint(e.toString());
+ debugPrint(s.toString());
+ }
+ final node = await getCurrentNode();
+ final host = Uri.parse(node.host).host;
+ await walletBase?.connectToNode(
+ node: Node(uri: "$host:${node.port}", type: WalletType.wownero));
+ await walletBase?.startSync();
+ await DB.instance
+ .put(boxName: walletId, key: "id", value: _walletId);
+
+ // Set relevant indexes
+ await DB.instance
+ .put(boxName: walletId, key: "receivingIndex", value: 0);
+ await DB.instance
+ .put(boxName: walletId, key: "changeIndex", value: 0);
+ await DB.instance.put(
+ boxName: walletId,
+ key: 'blocked_tx_hashes',
+ value: ["0xdefault"],
+ ); // A list of transaction hashes to represent frozen utxos in wallet
+ // initialize address book entries
+ await DB.instance.put(
+ boxName: walletId,
+ key: 'addressBookEntries',
+ value: {});
+ await DB.instance
+ .put(boxName: walletId, key: "isFavorite", value: false);
+
+ // Generate and add addresses to relevant arrays
+ final initialReceivingAddress = await _generateAddressForChain(0, 0);
+ // final initialChangeAddress = await _generateAddressForChain(1, 0);
+
+ await _addToAddressesArrayForChain(initialReceivingAddress, 0);
+ // await _addToAddressesArrayForChain(initialChangeAddress, 1);
+
+ await DB.instance.put(
+ boxName: walletId,
+ key: 'receivingAddresses',
+ value: [initialReceivingAddress]);
+ await DB.instance
+ .put(boxName: walletId, key: "receivingIndex", value: 0);
+
+ _currentReceivingAddress = Future(() => initialReceivingAddress);
+
+ Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info);
+ }
+
+ @override
+ // TODO: implement initializeWallet
+ Future initializeNew() async {
+ await _prefs.init();
+ // TODO: ping actual wownero network
+ // try {
+ // final hasNetwork = await _electrumXClient.ping();
+ // if (!hasNetwork) {
+ // return false;
+ // }
+ // } catch (e, s) {
+ // Logging.instance.log("Caught in initializeWallet(): $e\n$s");
+ // return false;
+ // }
+ storage = const FlutterSecureStorage();
+ walletService =
+ wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox);
+ prefs = await SharedPreferences.getInstance();
+ keysStorage = KeyService(storage!);
+
+ await _generateNewWallet();
+ // var password;
+ // try {
+ // password =
+ // await keysStorage?.getWalletPassword(walletName: this._walletId);
+ // } catch (e, s) {
+ // Logging.instance.log("$e $s");
+ // Logging.instance.log("Generating new ${coin.ticker} wallet.");
+ // // Triggers for new users automatically. Generates new wallet
+ // await _generateNewWallet(wallet);
+ // await wallet.put("id", this._walletId);
+ // return true;
+ // }
+ // walletBase = (await walletService?.openWallet(this._walletId, password))
+ // as WowneroWalletBase;
+ // Logging.instance.log("Opening existing ${coin.ticker} wallet.");
+ // // Wallet already exists, triggers for a returning user
+ // final currentAddress = awaicurrentHeightt _getCurrentAddressForChain(0);
+ // this._currentReceivingAddress = Future(() => currentAddress);
+ //
+ // await walletBase?.connectToNode(
+ // node: Node(
+ // uri: "xmr-node.cakewallet.com:18081", type: WalletType.wownero));
+ // walletBase?.startSync();
+
+ return true;
+ }
+
+ @override
+ Future initializeExisting() async {
+ Logging.instance.log(
+ "Opening existing ${coin.prettyName} wallet $walletName...",
+ level: LogLevel.Info);
+
+ if ((DB.instance.get(boxName: walletId, key: "id")) == null) {
+ debugPrint("Exception was thrown");
+ throw Exception(
+ "Attempted to initialize an existing wallet using an unknown wallet ID!");
+ }
+
+ storage = const FlutterSecureStorage();
+ walletService =
+ wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox);
+ prefs = await SharedPreferences.getInstance();
+ keysStorage = KeyService(storage!);
+
+ await _prefs.init();
+ final data =
+ DB.instance.get(boxName: walletId, key: "latest_tx_model")
+ as TransactionData?;
+ if (data != null) {
+ _transactionData = Future(() => data);
+ }
+
+ String? password;
+ try {
+ password = await keysStorage?.getWalletPassword(walletName: _walletId);
+ } catch (e, s) {
+ debugPrint("Exception was thrown $e $s");
+ throw Exception("Password not found $e, $s");
+ }
+ walletBase = (await walletService?.openWallet(_walletId, password!))
+ as WowneroWalletBase;
+ debugPrint("walletBase $walletBase");
+ Logging.instance.log(
+ "Opened existing ${coin.prettyName} wallet $walletName",
+ level: LogLevel.Info);
+ // Wallet already exists, triggers for a returning user
+
+ String indexKey = "receivingIndex";
+ final curIndex =
+ await DB.instance.get(boxName: walletId, key: indexKey) as int;
+ // Use new index to derive a new receiving address
+ final newReceivingAddress = await _generateAddressForChain(0, curIndex);
+ Logging.instance.log("xmr address in init existing: $newReceivingAddress",
+ level: LogLevel.Info);
+ _currentReceivingAddress = Future(() => newReceivingAddress);
+ }
+
+ @override
+ Future get maxFee async {
+ var bal = await availableBalance;
+ var fee = walletBase!.calculateEstimatedFee(
+ wownero.getDefaultTransactionPriority(), bal.toBigInt().toInt()) ~/
+ 10000;
+
+ return fee;
+ }
+
+ @override
+ // TODO: implement pendingBalance
+ Future get pendingBalance => throw UnimplementedError();
+
+ bool longMutex = false;
+
+ // TODO: are these needed?
+ FlutterSecureStorage? storage;
+ WalletService? walletService;
+ SharedPreferences? prefs;
+ KeyService? keysStorage;
+ WowneroWalletBase? walletBase;
+ WalletCreationService? _walletCreationService;
+
+ String toStringForinfo(WalletInfo info) {
+ return "id: ${info.id} name: ${info.name} type: ${info.type} recovery: ${info.isRecovery}"
+ " restoreheight: ${info.restoreHeight} timestamp: ${info.timestamp} dirPath: ${info.dirPath} "
+ "path: ${info.path} address: ${info.address} addresses: ${info.addresses}";
+ }
+
+ Future pathForWalletDir({
+ required String name,
+ required WalletType type,
+ }) async {
+ Directory root = (await getApplicationDocumentsDirectory());
+ if (Platform.isIOS) {
+ root = (await getLibraryDirectory());
+ }
+ final prefix = walletTypeToString(type).toLowerCase();
+ final walletsDir = Directory('${root.path}/wallets');
+ final walletDire = Directory('${walletsDir.path}/$prefix/$name');
+
+ if (!walletDire.existsSync()) {
+ walletDire.createSync(recursive: true);
+ }
+
+ return walletDire.path;
+ }
+
+ Future pathForWallet({
+ required String name,
+ required WalletType type,
+ }) async =>
+ await pathForWalletDir(name: name, type: type)
+ .then((path) => '$path/$name');
+
+ // TODO: take in a dynamic height
+ @override
+ Future recoverFromMnemonic({
+ required String mnemonic,
+ required int maxUnusedAddressGap,
+ required int maxNumberOfIndexesToCheck,
+ required int height,
+ }) async {
+ await _prefs.init();
+ longMutex = true;
+ final start = DateTime.now();
+ try {
+ // Logging.instance.log("IS_INTEGRATION_TEST: $integrationTestFlag");
+ // if (!integrationTestFlag) {
+ // final features = await electrumXClient.getServerFeatures();
+ // Logging.instance.log("features: $features");
+ // if (_networkType == BasicNetworkType.main) {
+ // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) {
+ // throw Exception("genesis hash does not match main net!");
+ // }
+ // } else if (_networkType == BasicNetworkType.test) {
+ // if (features['genesis_hash'] != GENESIS_HASH_TESTNET) {
+ // throw Exception("genesis hash does not match test net!");
+ // }
+ // }
+ // }
+ // check to make sure we aren't overwriting a mnemonic
+ // this should never fail
+ if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) {
+ longMutex = false;
+ throw Exception("Attempted to overwrite mnemonic on restore!");
+ }
+ await _secureStore.write(
+ key: '${_walletId}_mnemonic', value: mnemonic.trim());
+
+ height = getSeedHeightSync(mnemonic.trim());
+
+ await DB.instance
+ .put(boxName: walletId, key: "restoreHeight", value: height);
+
+ storage = const FlutterSecureStorage();
+ walletService =
+ wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox);
+ prefs = await SharedPreferences.getInstance();
+ keysStorage = KeyService(storage!);
+ WalletInfo walletInfo;
+ WalletCredentials credentials;
+ String name = _walletId;
+ final dirPath =
+ await pathForWalletDir(name: name, type: WalletType.wownero);
+ final path = await pathForWallet(name: name, type: WalletType.wownero);
+ credentials = wownero.createWowneroRestoreWalletFromSeedCredentials(
+ name: name,
+ height: height,
+ mnemonic: mnemonic.trim(),
+ );
+ try {
+ walletInfo = WalletInfo.external(
+ id: WalletBase.idFor(name, WalletType.wownero),
+ name: name,
+ type: WalletType.wownero,
+ isRecovery: false,
+ restoreHeight: credentials.height ?? 0,
+ date: DateTime.now(),
+ path: path,
+ dirPath: dirPath,
+ // TODO: find out what to put for address
+ address: '');
+ credentials.walletInfo = walletInfo;
+
+ _walletCreationService = WalletCreationService(
+ secureStorage: storage,
+ sharedPreferences: prefs,
+ walletService: walletService,
+ keyService: keysStorage,
+ );
+ _walletCreationService!.changeWalletType();
+ // To restore from a seed
+ final wallet =
+ await _walletCreationService!.restoreFromSeed(credentials);
+ walletInfo.address = wallet.walletAddresses.address;
+ await DB.instance
+ .add(boxName: WalletInfo.boxName, value: walletInfo);
+ walletBase?.close();
+ walletBase = wallet as WowneroWalletBase;
+ await DB.instance.put(
+ boxName: walletId,
+ key: 'receivingAddresses',
+ value: [walletInfo.address!]);
+ await DB.instance
+ .put(boxName: walletId, key: "receivingIndex", value: 0);
+ await DB.instance
+ .put(boxName: walletId, key: "id", value: _walletId);
+ await DB.instance
+ .put(boxName: walletId, key: "changeIndex", value: 0);
+ await DB.instance.put(
+ boxName: walletId,
+ key: 'blocked_tx_hashes',
+ value: ["0xdefault"],
+ ); // A list of transaction hashes to represent frozen utxos in wallet
+ // initialize address book entries
+ await DB.instance.put(
+ boxName: walletId,
+ key: 'addressBookEntries',
+ value: {});
+ await DB.instance
+ .put(boxName: walletId, key: "isFavorite", value: false);
+ } catch (e, s) {
+ debugPrint(e.toString());
+ debugPrint(s.toString());
+ }
+ final node = await getCurrentNode();
+ final host = Uri.parse(node.host).host;
+ await walletBase?.connectToNode(
+ node: Node(uri: "$host:${node.port}", type: WalletType.wownero));
+ await walletBase?.rescan(height: credentials.height);
+ } catch (e, s) {
+ Logging.instance.log(
+ "Exception rethrown from recoverFromMnemonic(): $e\n$s",
+ level: LogLevel.Error);
+ longMutex = false;
+ rethrow;
+ }
+ longMutex = false;
+
+ final end = DateTime.now();
+ Logging.instance.log(
+ "$walletName Recovery time: ${end.difference(start).inMilliseconds} millis",
+ level: LogLevel.Info);
+ }
+
+ @override
+ Future send({
+ required String toAddress,
+ required int amount,
+ Map args = const {},
+ }) async {
+ try {
+ final txData = await prepareSend(
+ address: toAddress, satoshiAmount: amount, args: args);
+ final txHash = await confirmSend(txData: txData);
+ return txHash;
+ } catch (e, s) {
+ Logging.instance
+ .log("Exception rethrown from send(): $e\n$s", level: LogLevel.Error);
+ rethrow;
+ }
+ }
+
+ @override
+ Future testNetworkConnection() async {
+ return await walletBase?.isConnected() ?? false;
+ }
+
+ Timer? _networkAliveTimer;
+
+ void startNetworkAlivePinging() {
+ // call once on start right away
+ _periodicPingCheck();
+
+ // then periodically check
+ _networkAliveTimer = Timer.periodic(
+ Constants.networkAliveTimerDuration,
+ (_) async {
+ _periodicPingCheck();
+ },
+ );
+ }
+
+ void _periodicPingCheck() async {
+ bool hasNetwork = await testNetworkConnection();
+ _isConnected = hasNetwork;
+ if (_isConnected != hasNetwork) {
+ NodeConnectionStatus status = hasNetwork
+ ? NodeConnectionStatus.connected
+ : NodeConnectionStatus.disconnected;
+ GlobalEventBus.instance
+ .fire(NodeConnectionStatusChangedEvent(status, walletId, coin));
+ }
+ }
+
+ void stopNetworkAlivePinging() {
+ _networkAliveTimer?.cancel();
+ _networkAliveTimer = null;
+ }
+
+ bool _isConnected = false;
+
+ @override
+ bool get isConnected => _isConnected;
+
+ @override
+ Future get totalBalance async {
+ var transactions = walletBase?.transactionHistory!.transactions;
+ int transactionBalance = 0;
+ for (var tx in transactions!.entries) {
+ if (tx.value.direction == TransactionDirection.incoming) {
+ transactionBalance += tx.value.amount!;
+ } else {
+ transactionBalance += -tx.value.amount! - tx.value.fee!;
+ }
+ }
+
+ // TODO: grab total balance
+ var bal = 0;
+ for (var element in walletBase!.balance!.entries) {
+ bal = bal + element.value.fullBalance;
+ }
+ debugPrint("balances: $transactionBalance $bal");
+ if (isActive) {
+ String am = wowneroAmountToString(amount: bal);
+
+ return Decimal.parse(am);
+ } else {
+ String am = wowneroAmountToString(amount: transactionBalance);
+
+ return Decimal.parse(am);
+ }
+ }
+
+ @override
+ // TODO: implement onIsActiveWalletChanged
+ void Function(bool)? get onIsActiveWalletChanged => (isActive) async {
+ await walletBase?.save();
+ walletBase?.close();
+ wowneroAutosaveTimer?.cancel();
+ wowneroAutosaveTimer = null;
+ timer?.cancel();
+ timer = null;
+ await stopSyncPercentTimer();
+ if (isActive) {
+ String? password;
+ try {
+ password =
+ await keysStorage?.getWalletPassword(walletName: _walletId);
+ } catch (e, s) {
+ debugPrint("Exception was thrown $e $s");
+ throw Exception("Password not found $e, $s");
+ }
+ walletBase = (await walletService?.openWallet(_walletId, password!))
+ as WowneroWalletBase?;
+ if (!(await walletBase!.isConnected())) {
+ final node = await getCurrentNode();
+ final host = Uri.parse(node.host).host;
+ await walletBase?.connectToNode(
+ node:
+ Node(uri: "$host:${node.port}", type: WalletType.wownero));
+ await walletBase?.startSync();
+ }
+ await refresh();
+ }
+ this.isActive = isActive;
+ };
+
+ bool isActive = false;
+
+ @override
+ Future get transactionData =>
+ _transactionData ??= _fetchTransactionData();
+ Future? _transactionData;
+
+ Future _fetchTransactionData() async {
+ final transactions = walletBase?.transactionHistory!.transactions;
+
+ final cachedTransactions =
+ DB.instance.get(boxName: walletId, key: 'latest_tx_model')
+ as TransactionData?;
+ int latestTxnBlockHeight =
+ DB.instance.get(boxName: walletId, key: "storedTxnDataHeight")
+ as int? ??
+ 0;
+
+ final txidsList = DB.instance
+ .get(boxName: walletId, key: "cachedTxids") as List? ??
+ [];
+
+ final Set cachedTxids = Set.from(txidsList);
+
+ // TODO: filter to skip cached + confirmed txn processing in next step
+ // final unconfirmedCachedTransactions =
+ // cachedTransactions?.getAllTransactions() ?? {};
+ // unconfirmedCachedTransactions
+ // .removeWhere((key, value) => value.confirmedStatus);
+ //
+ // if (cachedTransactions != null) {
+ // for (final tx in allTxHashes.toList(growable: false)) {
+ // final txHeight = tx["height"] as int;
+ // if (txHeight > 0 &&
+ // txHeight < latestTxnBlockHeight - MINIMUM_CONFIRMATIONS) {
+ // if (unconfirmedCachedTransactions[tx["tx_hash"] as String] == null) {
+ // allTxHashes.remove(tx);
+ // }
+ // }
+ // }
+ // }
+
+ // sort thing stuff
+ // change to get Wownero price
+ final priceData =
+ await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
+ Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
+ final List