mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-25 21:09:23 +00:00
1601 lines
54 KiB
Dart
1601 lines
54 KiB
Dart
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<NodeModel> 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<void> 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<List<String>> _getMnemonicList() async {
|
|
final mnemonicString =
|
|
await _secureStore.read(key: '${_walletId}_mnemonic');
|
|
if (mnemonicString == null) {
|
|
return [];
|
|
}
|
|
final List<String> data = mnemonicString.split(' ');
|
|
return data;
|
|
}
|
|
|
|
@override
|
|
Future<List<String>> get mnemonic => _getMnemonicList();
|
|
|
|
Future<int> 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<dynamic>(boxName: walletId, key: "storedNodeHeight") as int? ??
|
|
0;
|
|
|
|
if (currentHeight > cachedHeight && currentHeight != int64MaxValue) {
|
|
await DB.instance.put<dynamic>(
|
|
boxName: walletId, key: "storedNodeHeight", value: currentHeight);
|
|
return currentHeight;
|
|
} else {
|
|
return cachedHeight;
|
|
}
|
|
}
|
|
|
|
Future<int> 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<dynamic>(boxName: walletId, key: "storedSyncingHeight")
|
|
as int? ??
|
|
0;
|
|
|
|
if (syncingHeight > cachedHeight) {
|
|
await DB.instance.put<dynamic>(
|
|
boxName: walletId, key: "storedSyncingHeight", value: syncingHeight);
|
|
return syncingHeight;
|
|
} else {
|
|
return cachedHeight;
|
|
}
|
|
}
|
|
|
|
Future<void> updateStoredChainHeight({required int newHeight}) async {
|
|
await DB.instance.put<dynamic>(
|
|
boxName: walletId, key: "storedChainHeight", value: newHeight);
|
|
}
|
|
|
|
int get storedChainHeight {
|
|
return DB.instance.get<dynamic>(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<void> _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<dynamic>(boxName: walletId, key: indexKey)) + 1;
|
|
await DB.instance
|
|
.put<dynamic>(boxName: walletId, key: indexKey, value: newIndex);
|
|
}
|
|
|
|
Future<void> _checkCurrentReceivingAddressesForTransactions() async {
|
|
try {
|
|
await _checkReceivingAddressForTransactions();
|
|
} catch (e, s) {
|
|
Logging.instance.log(
|
|
"Exception rethrown from _checkCurrentReceivingAddressesForTransactions(): $e\n$s",
|
|
level: LogLevel.Error);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
Future<void> _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<dynamic>(boxName: walletId, key: indexKey) as int;
|
|
if (highestIndex >= curIndex) {
|
|
// First increment the receiving index
|
|
await _incrementAddressIndexForChain(0);
|
|
final newReceivingIndex =
|
|
DB.instance.get<dynamic>(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<void> stopSyncPercentTimer() async {
|
|
syncPercentTimer?.cancel();
|
|
syncPercentTimer = null;
|
|
}
|
|
|
|
Future<void> 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<dynamic>(boxName: walletId, key: "highestPercentCached")
|
|
as double? ??
|
|
0;
|
|
set highestPercentCached(double value) => DB.instance.put<dynamic>(
|
|
boxName: walletId,
|
|
key: "highestPercentCached",
|
|
value: value,
|
|
);
|
|
|
|
/// Refreshes display data for the wallet
|
|
@override
|
|
Future<void> 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<dynamic>(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<List<String>> get allOwnAddresses {
|
|
return Future(() => []);
|
|
}
|
|
|
|
@override
|
|
Future<Decimal> get balanceMinusMaxFee async =>
|
|
(await availableBalance) -
|
|
(Decimal.fromInt((await maxFee)) / Decimal.fromInt(Constants.satsPerCoin))
|
|
.toDecimal();
|
|
|
|
@override
|
|
Future<String> get currentReceivingAddress =>
|
|
_currentReceivingAddress ??= _getCurrentAddressForChain(0);
|
|
|
|
@override
|
|
Future<void> 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<String>? _currentReceivingAddress;
|
|
|
|
Future<FeeObject> _getFees() async {
|
|
return FeeObject(
|
|
numberOfBlocksFast: 10,
|
|
numberOfBlocksAverage: 10,
|
|
numberOfBlocksSlow: 10,
|
|
fast: 4,
|
|
medium: 2,
|
|
slow: 0);
|
|
}
|
|
|
|
@override
|
|
Future<FeeObject> get fees => _feeObject ??= _getFees();
|
|
Future<FeeObject>? _feeObject;
|
|
|
|
@override
|
|
// TODO: implement fullRescan
|
|
Future<void> 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<String> _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<void> _addToAddressesArrayForChain(String address, int chain) async {
|
|
String chainArray = '';
|
|
if (chain == 0) {
|
|
chainArray = 'receivingAddresses';
|
|
} else {
|
|
chainArray = 'changeAddresses';
|
|
}
|
|
|
|
final addressArray =
|
|
DB.instance.get<dynamic>(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<dynamic>(boxName: walletId, key: chainArray, value: [address]);
|
|
} else {
|
|
// Make a deep copy of the existing list
|
|
final List<String> newArray = [];
|
|
addressArray
|
|
.forEach((dynamic _address) => newArray.add(_address as String));
|
|
newArray.add(address); // Add the address passed into the method
|
|
await DB.instance
|
|
.put<dynamic>(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<String> _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<dynamic>(boxName: walletId, key: arrayKey)) as List<dynamic>;
|
|
return internalChainArray.last as String;
|
|
}
|
|
|
|
//TODO: take in the default language when creating wallet.
|
|
Future<void> _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<dynamic>(
|
|
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<WalletInfo>(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<dynamic>(boxName: walletId, key: "id", value: _walletId);
|
|
|
|
// Set relevant indexes
|
|
await DB.instance
|
|
.put<dynamic>(boxName: walletId, key: "receivingIndex", value: 0);
|
|
await DB.instance
|
|
.put<dynamic>(boxName: walletId, key: "changeIndex", value: 0);
|
|
await DB.instance.put<dynamic>(
|
|
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<dynamic>(
|
|
boxName: walletId,
|
|
key: 'addressBookEntries',
|
|
value: <String, String>{});
|
|
await DB.instance
|
|
.put<dynamic>(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<dynamic>(
|
|
boxName: walletId,
|
|
key: 'receivingAddresses',
|
|
value: [initialReceivingAddress]);
|
|
await DB.instance
|
|
.put<dynamic>(boxName: walletId, key: "receivingIndex", value: 0);
|
|
|
|
_currentReceivingAddress = Future(() => initialReceivingAddress);
|
|
|
|
Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info);
|
|
}
|
|
|
|
@override
|
|
// TODO: implement initializeWallet
|
|
Future<bool> 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<void> initializeExisting() async {
|
|
Logging.instance.log(
|
|
"Opening existing ${coin.prettyName} wallet $walletName...",
|
|
level: LogLevel.Info);
|
|
|
|
if ((DB.instance.get<dynamic>(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<dynamic>(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<dynamic>(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<int> get maxFee async {
|
|
var bal = await availableBalance;
|
|
var fee = walletBase!.calculateEstimatedFee(
|
|
wownero.getDefaultTransactionPriority(), bal.toBigInt().toInt()) ~/
|
|
10000;
|
|
|
|
return fee;
|
|
}
|
|
|
|
@override
|
|
// TODO: implement pendingBalance
|
|
Future<Decimal> 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<String> pathForWalletDir({
|
|
required String name,
|
|
required WalletType type,
|
|
}) async {
|
|
Directory root = (await getApplicationDocumentsDirectory());
|
|
if (Platform.isIOS) {
|
|
root = (await getLibraryDirectory());
|
|
}
|
|
if (Platform.isLinux) {
|
|
root = Directory("${root.path}/.stackwallet");
|
|
}
|
|
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<String> 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<void> 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<dynamic>(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<WalletInfo>(boxName: WalletInfo.boxName, value: walletInfo);
|
|
walletBase?.close();
|
|
walletBase = wallet as WowneroWalletBase;
|
|
await DB.instance.put<dynamic>(
|
|
boxName: walletId,
|
|
key: 'receivingAddresses',
|
|
value: [walletInfo.address!]);
|
|
await DB.instance
|
|
.put<dynamic>(boxName: walletId, key: "receivingIndex", value: 0);
|
|
await DB.instance
|
|
.put<dynamic>(boxName: walletId, key: "id", value: _walletId);
|
|
await DB.instance
|
|
.put<dynamic>(boxName: walletId, key: "changeIndex", value: 0);
|
|
await DB.instance.put<dynamic>(
|
|
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<dynamic>(
|
|
boxName: walletId,
|
|
key: 'addressBookEntries',
|
|
value: <String, String>{});
|
|
await DB.instance
|
|
.put<dynamic>(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<String> send({
|
|
required String toAddress,
|
|
required int amount,
|
|
Map<String, String> 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<bool> 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<Decimal> 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<TransactionData> get transactionData =>
|
|
_transactionData ??= _fetchTransactionData();
|
|
Future<TransactionData>? _transactionData;
|
|
|
|
Future<TransactionData> _fetchTransactionData() async {
|
|
final transactions = walletBase?.transactionHistory!.transactions;
|
|
|
|
final cachedTransactions =
|
|
DB.instance.get<dynamic>(boxName: walletId, key: 'latest_tx_model')
|
|
as TransactionData?;
|
|
int latestTxnBlockHeight =
|
|
DB.instance.get<dynamic>(boxName: walletId, key: "storedTxnDataHeight")
|
|
as int? ??
|
|
0;
|
|
|
|
final txidsList = DB.instance
|
|
.get<dynamic>(boxName: walletId, key: "cachedTxids") as List? ??
|
|
[];
|
|
|
|
final Set<String> cachedTxids = Set<String>.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<Map<String, dynamic>> midSortedArray = [];
|
|
|
|
if (transactions != null) {
|
|
for (var tx in transactions.entries) {
|
|
cachedTxids.add(tx.value.id);
|
|
Logging.instance.log(
|
|
"${tx.value.accountIndex} ${tx.value.addressIndex} ${tx.value.amount} ${tx.value.date} "
|
|
"${tx.value.direction} ${tx.value.fee} ${tx.value.height} ${tx.value.id} ${tx.value.isPending} ${tx.value.key} "
|
|
"${tx.value.recipientAddress}, ${tx.value.additionalInfo} con:${tx.value.confirmations}"
|
|
" ${tx.value.keyIndex}",
|
|
level: LogLevel.Info);
|
|
String am = wowneroAmountToString(amount: tx.value.amount!);
|
|
final worthNow = (currentPrice * Decimal.parse(am)).toStringAsFixed(2);
|
|
Map<String, dynamic> midSortedTx = {};
|
|
// // create final tx map
|
|
midSortedTx["txid"] = tx.value.id;
|
|
midSortedTx["confirmed_status"] = !tx.value.isPending &&
|
|
tx.value.confirmations != null &&
|
|
tx.value.confirmations! >= MINIMUM_CONFIRMATIONS;
|
|
midSortedTx["confirmations"] = tx.value.confirmations ?? 0;
|
|
midSortedTx["timestamp"] =
|
|
(tx.value.date.millisecondsSinceEpoch ~/ 1000);
|
|
midSortedTx["txType"] =
|
|
tx.value.direction == TransactionDirection.incoming
|
|
? "Received"
|
|
: "Sent";
|
|
midSortedTx["amount"] = tx.value.amount;
|
|
midSortedTx["worthNow"] = worthNow;
|
|
midSortedTx["worthAtBlockTimestamp"] = worthNow;
|
|
midSortedTx["fees"] = tx.value.fee;
|
|
// TODO: shouldn't wownero have an address I can grab
|
|
if (tx.value.direction == TransactionDirection.incoming) {
|
|
final addressInfo = tx.value.additionalInfo;
|
|
|
|
midSortedTx["address"] = walletBase?.getTransactionAddress(
|
|
addressInfo!['accountIndex'] as int,
|
|
addressInfo['addressIndex'] as int,
|
|
);
|
|
} else {
|
|
midSortedTx["address"] = "";
|
|
}
|
|
|
|
final int txHeight = tx.value.height ?? 0;
|
|
midSortedTx["height"] = txHeight;
|
|
if (txHeight >= latestTxnBlockHeight) {
|
|
latestTxnBlockHeight = txHeight;
|
|
}
|
|
|
|
midSortedTx["aliens"] = <dynamic>[];
|
|
midSortedTx["inputSize"] = 0;
|
|
midSortedTx["outputSize"] = 0;
|
|
midSortedTx["inputs"] = <dynamic>[];
|
|
midSortedTx["outputs"] = <dynamic>[];
|
|
midSortedArray.add(midSortedTx);
|
|
}
|
|
}
|
|
|
|
// sort by date ----
|
|
midSortedArray
|
|
.sort((a, b) => (b["timestamp"] as int) - (a["timestamp"] as int));
|
|
Logging.instance.log(midSortedArray, level: LogLevel.Info);
|
|
|
|
// buildDateTimeChunks
|
|
final Map<String, dynamic> result = {"dateTimeChunks": <dynamic>[]};
|
|
final dateArray = <dynamic>[];
|
|
|
|
for (int i = 0; i < midSortedArray.length; i++) {
|
|
final txObject = midSortedArray[i];
|
|
final date = extractDateFromTimestamp(txObject["timestamp"] as int);
|
|
final txTimeArray = [txObject["timestamp"], date];
|
|
|
|
if (dateArray.contains(txTimeArray[1])) {
|
|
result["dateTimeChunks"].forEach((dynamic chunk) {
|
|
if (extractDateFromTimestamp(chunk["timestamp"] as int) ==
|
|
txTimeArray[1]) {
|
|
if (chunk["transactions"] == null) {
|
|
chunk["transactions"] = <Map<String, dynamic>>[];
|
|
}
|
|
chunk["transactions"].add(txObject);
|
|
}
|
|
});
|
|
} else {
|
|
dateArray.add(txTimeArray[1]);
|
|
final chunk = {
|
|
"timestamp": txTimeArray[0],
|
|
"transactions": [txObject],
|
|
};
|
|
result["dateTimeChunks"].add(chunk);
|
|
}
|
|
}
|
|
|
|
final transactionsMap = cachedTransactions?.getAllTransactions() ?? {};
|
|
transactionsMap
|
|
.addAll(TransactionData.fromJson(result).getAllTransactions());
|
|
|
|
final txModel = TransactionData.fromMap(transactionsMap);
|
|
|
|
await DB.instance.put<dynamic>(
|
|
boxName: walletId,
|
|
key: 'storedTxnDataHeight',
|
|
value: latestTxnBlockHeight);
|
|
await DB.instance.put<dynamic>(
|
|
boxName: walletId, key: 'latest_tx_model', value: txModel);
|
|
await DB.instance.put<dynamic>(
|
|
boxName: walletId,
|
|
key: 'cachedTxids',
|
|
value: cachedTxids.toList(growable: false));
|
|
|
|
return txModel;
|
|
}
|
|
|
|
@override
|
|
// TODO: implement unspentOutputs
|
|
Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError();
|
|
|
|
@override
|
|
// TODO: implement validateAddress
|
|
bool validateAddress(String address) {
|
|
bool valid = RegExp("[a-zA-Z0-9]{95}").hasMatch(address) ||
|
|
RegExp("[a-zA-Z0-9]{106}").hasMatch(address);
|
|
return valid;
|
|
}
|
|
|
|
@override
|
|
String get walletId => _walletId;
|
|
late String _walletId;
|
|
|
|
@override
|
|
String get walletName => _walletName;
|
|
late String _walletName;
|
|
|
|
// setter for updating on rename
|
|
@override
|
|
set walletName(String newName) => _walletName = newName;
|
|
|
|
@override
|
|
set isFavorite(bool markFavorite) {
|
|
DB.instance.put<dynamic>(
|
|
boxName: walletId, key: "isFavorite", value: markFavorite);
|
|
}
|
|
|
|
@override
|
|
bool get isFavorite {
|
|
try {
|
|
return DB.instance.get<dynamic>(boxName: walletId, key: "isFavorite")
|
|
as bool;
|
|
} catch (e, s) {
|
|
Logging.instance
|
|
.log("isFavorite fetch failed: $e\n$s", level: LogLevel.Error);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
@override
|
|
// TODO: implement availableBalance
|
|
Future<Decimal> get availableBalance async {
|
|
var bal = 0;
|
|
for (var element in walletBase!.balance!.entries) {
|
|
bal = bal + element.value.unlockedBalance;
|
|
}
|
|
String am = wowneroAmountToString(amount: bal);
|
|
|
|
return Decimal.parse(am);
|
|
}
|
|
|
|
@override
|
|
Coin get coin => _coin;
|
|
|
|
@override
|
|
Future<String> confirmSend({required Map<String, dynamic> txData}) async {
|
|
try {
|
|
Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info);
|
|
final pendingWowneroTransaction =
|
|
txData['pendingWowneroTransaction'] as PendingWowneroTransaction;
|
|
try {
|
|
await pendingWowneroTransaction.commit();
|
|
Logging.instance.log(
|
|
"transaction ${pendingWowneroTransaction.id} has been sent",
|
|
level: LogLevel.Info);
|
|
return pendingWowneroTransaction.id;
|
|
} catch (e, s) {
|
|
Logging.instance.log("$walletName wownero confirmSend: $e\n$s",
|
|
level: LogLevel.Error);
|
|
rethrow;
|
|
}
|
|
} catch (e, s) {
|
|
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
|
level: LogLevel.Info);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
// TODO: fix the double free memory crash error.
|
|
@override
|
|
Future<Map<String, dynamic>> prepareSend(
|
|
{required String address,
|
|
required int satoshiAmount,
|
|
Map<String, dynamic>? args}) async {
|
|
int amount = satoshiAmount;
|
|
String toAddress = address;
|
|
try {
|
|
final feeRate = args?["feeRate"];
|
|
if (feeRate is FeeRateType) {
|
|
MoneroTransactionPriority feePriority = MoneroTransactionPriority.slow;
|
|
switch (feeRate) {
|
|
case FeeRateType.fast:
|
|
feePriority = MoneroTransactionPriority.fastest;
|
|
break;
|
|
case FeeRateType.average:
|
|
feePriority = MoneroTransactionPriority.medium;
|
|
break;
|
|
case FeeRateType.slow:
|
|
feePriority = MoneroTransactionPriority.slow;
|
|
break;
|
|
}
|
|
|
|
Future<PendingTransaction>? awaitPendingTransaction;
|
|
try {
|
|
// check for send all
|
|
bool isSendAll = false;
|
|
final balance = await availableBalance;
|
|
final satInDecimal = ((Decimal.fromInt(satoshiAmount) /
|
|
Decimal.fromInt(Constants.satsPerCoinWownero))
|
|
.toDecimal() *
|
|
Decimal.fromInt(1000));
|
|
if (satInDecimal == balance) {
|
|
isSendAll = true;
|
|
}
|
|
Logging.instance
|
|
.log("$toAddress $amount $args", level: LogLevel.Info);
|
|
String amountToSend = wowneroAmountToString(amount: amount * 1000);
|
|
Logging.instance.log("$amount $amountToSend", level: LogLevel.Info);
|
|
|
|
wownero_output.Output output = wownero_output.Output(walletBase!);
|
|
output.address = toAddress;
|
|
output.sendAll = isSendAll;
|
|
output.setCryptoAmount(amountToSend);
|
|
|
|
List<wownero_output.Output> outputs = [output];
|
|
Object tmp = wownero.createWowneroTransactionCreationCredentials(
|
|
outputs: outputs, priority: feePriority);
|
|
|
|
await prepareSendMutex.protect(() async {
|
|
awaitPendingTransaction = walletBase!.createTransaction(tmp);
|
|
});
|
|
} catch (e, s) {
|
|
Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s",
|
|
level: LogLevel.Warning);
|
|
}
|
|
|
|
PendingWowneroTransaction pendingWowneroTransaction =
|
|
await (awaitPendingTransaction!) as PendingWowneroTransaction;
|
|
int realfee = (Decimal.parse(pendingWowneroTransaction.feeFormatted) *
|
|
100000000.toDecimal())
|
|
.toBigInt()
|
|
.toInt();
|
|
debugPrint("fee? $realfee");
|
|
Map<String, dynamic> txData = {
|
|
"pendingWowneroTransaction": pendingWowneroTransaction,
|
|
"fee": realfee,
|
|
"addresss": toAddress,
|
|
"recipientAmt": satoshiAmount,
|
|
};
|
|
|
|
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
|
|
return txData;
|
|
} else {
|
|
throw ArgumentError("Invalid fee rate argument provided!");
|
|
}
|
|
} catch (e, s) {
|
|
Logging.instance.log("Exception rethrown from prepare send(): $e\n$s",
|
|
level: LogLevel.Info);
|
|
|
|
if (e.toString().contains("Incorrect unlocked balance")) {
|
|
throw Exception("Insufficient balance!");
|
|
} else if (e is CreationTransactionException) {
|
|
throw Exception("Insufficient funds to pay for transaction fee!");
|
|
} else {
|
|
throw Exception("Transaction failed with error code $e");
|
|
}
|
|
}
|
|
}
|
|
|
|
Mutex prepareSendMutex = Mutex();
|
|
Mutex estimateFeeMutex = Mutex();
|
|
|
|
@override
|
|
Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async {
|
|
MoneroTransactionPriority? priority;
|
|
FeeRateType feeRateType = FeeRateType.slow;
|
|
switch (feeRate) {
|
|
case 1:
|
|
priority = MoneroTransactionPriority.regular;
|
|
feeRateType = FeeRateType.slow;
|
|
break;
|
|
case 2:
|
|
priority = MoneroTransactionPriority.medium;
|
|
feeRateType = FeeRateType.average;
|
|
break;
|
|
case 3:
|
|
priority = MoneroTransactionPriority.fast;
|
|
feeRateType = FeeRateType.average;
|
|
break;
|
|
case 4:
|
|
priority = MoneroTransactionPriority.fastest;
|
|
feeRateType = FeeRateType.fast;
|
|
break;
|
|
case 0:
|
|
default:
|
|
priority = MoneroTransactionPriority.slow;
|
|
feeRateType = FeeRateType.slow;
|
|
break;
|
|
}
|
|
var aprox;
|
|
await estimateFeeMutex.protect(() async {
|
|
{
|
|
try {
|
|
aprox = (await prepareSend(
|
|
// This address is only used for getting an approximate fee, never for sending
|
|
address:
|
|
"WW3iVcnoAY6K9zNdU4qmdvZELefx6xZz4PMpTwUifRkvMQckyadhSPYMVPJhBdYE8P9c27fg9RPmVaWNFx1cDaj61HnetqBiy",
|
|
satoshiAmount: satoshiAmount,
|
|
args: {"feeRate": feeRateType}))['fee'];
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
} catch (e, s) {
|
|
aprox = -9999999999999999;
|
|
}
|
|
}
|
|
});
|
|
|
|
print("this is the aprox fee $aprox for $satoshiAmount");
|
|
final fee = (aprox as int);
|
|
return fee;
|
|
}
|
|
|
|
@override
|
|
Future<bool> generateNewAddress() async {
|
|
try {
|
|
const String indexKey = "receivingIndex";
|
|
// First increment the receiving index
|
|
await _incrementAddressIndexForChain(0);
|
|
final newReceivingIndex =
|
|
DB.instance.get<dynamic>(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);
|
|
|
|
return true;
|
|
} catch (e, s) {
|
|
Logging.instance.log(
|
|
"Exception rethrown from generateNewAddress(): $e\n$s",
|
|
level: LogLevel.Error);
|
|
return false;
|
|
}
|
|
}
|
|
}
|