mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-25 12:59:24 +00:00
1426 lines
45 KiB
Dart
1426 lines
45 KiB
Dart
/*
|
|
* This file is part of Stack Wallet.
|
|
*
|
|
* Copyright (c) 2023 Cypher Stack
|
|
* All Rights Reserved.
|
|
* The code is distributed under GPLv3 license, see LICENSE file for details.
|
|
* Generated by Cypher Stack on 2023-05-26
|
|
*
|
|
*/
|
|
|
|
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:ffi';
|
|
import 'dart:io';
|
|
import 'dart:isolate';
|
|
|
|
import 'package:decimal/decimal.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter_libepiccash/lib.dart' as epiccash;
|
|
import 'package:flutter_libepiccash/models/transaction.dart';
|
|
import 'package:isar/isar.dart';
|
|
import 'package:mutex/mutex.dart';
|
|
import 'package:stack_wallet_backup/generate_password.dart';
|
|
import 'package:stackwallet/db/isar/main_db.dart';
|
|
import 'package:stackwallet/models/balance.dart';
|
|
import 'package:stackwallet/models/epicbox_config_model.dart';
|
|
import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models;
|
|
import 'package:stackwallet/models/node_model.dart';
|
|
import 'package:stackwallet/models/paymint/fee_object_model.dart';
|
|
import 'package:stackwallet/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.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/mixins/epic_cash_hive.dart';
|
|
import 'package:stackwallet/services/mixins/wallet_cache.dart';
|
|
import 'package:stackwallet/services/mixins/wallet_db.dart';
|
|
import 'package:stackwallet/services/node_service.dart';
|
|
import 'package:stackwallet/utilities/amount/amount.dart';
|
|
import 'package:stackwallet/utilities/constants.dart';
|
|
import 'package:stackwallet/utilities/default_epicboxes.dart';
|
|
import 'package:stackwallet/utilities/default_nodes.dart';
|
|
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
|
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
|
|
import 'package:stackwallet/utilities/logger.dart';
|
|
import 'package:stackwallet/utilities/prefs.dart';
|
|
import 'package:stackwallet/utilities/stack_file_system.dart';
|
|
import 'package:stackwallet/utilities/test_epic_box_connection.dart';
|
|
import 'package:tuple/tuple.dart';
|
|
import 'package:websocket_universal/websocket_universal.dart';
|
|
|
|
const int MINIMUM_CONFIRMATIONS = 3;
|
|
|
|
const String GENESIS_HASH_MAINNET = "";
|
|
const String GENESIS_HASH_TESTNET = "";
|
|
|
|
Future<String> deleteEpicWallet({
|
|
required String walletId,
|
|
required SecureStorageInterface secureStore,
|
|
}) async {
|
|
final wallet = await secureStore.read(key: '${walletId}_wallet');
|
|
String? config = await secureStore.read(key: '${walletId}_config');
|
|
if (Platform.isIOS) {
|
|
Directory appDir = await StackFileSystem.applicationRootDirectory();
|
|
|
|
final path = "${appDir.path}/epiccash";
|
|
final String name = walletId.trim();
|
|
final walletDir = '$path/$name';
|
|
|
|
var editConfig = jsonDecode(config as String);
|
|
|
|
editConfig["wallet_dir"] = walletDir;
|
|
config = jsonEncode(editConfig);
|
|
}
|
|
|
|
if (wallet == null) {
|
|
return "Tried to delete non existent epic wallet file with walletId=$walletId";
|
|
} else {
|
|
try {
|
|
return epiccash.LibEpiccash.deleteWallet(
|
|
wallet: wallet,
|
|
config: config!,
|
|
);
|
|
} catch (e, s) {
|
|
Logging.instance.log("$e\n$s", level: LogLevel.Error);
|
|
return "deleteEpicWallet($walletId) failed...";
|
|
}
|
|
}
|
|
}
|
|
|
|
class EpicCashWallet extends CoinServiceAPI
|
|
with WalletCache, WalletDB, EpicCashHive {
|
|
EpicCashWallet({
|
|
required String walletId,
|
|
required String walletName,
|
|
required Coin coin,
|
|
required SecureStorageInterface secureStore,
|
|
MainDB? mockableOverride,
|
|
}) {
|
|
_walletId = walletId;
|
|
_walletName = walletName;
|
|
_coin = coin;
|
|
_secureStore = secureStore;
|
|
initCache(walletId, coin);
|
|
initEpicCashHive(walletId);
|
|
initWalletDB(mockableOverride: mockableOverride);
|
|
}
|
|
|
|
static const integrationTestFlag =
|
|
bool.fromEnvironment("IS_INTEGRATION_TEST");
|
|
final syncMutex = Mutex();
|
|
|
|
final _prefs = Prefs.instance;
|
|
|
|
NodeModel? _epicNode;
|
|
|
|
@override
|
|
Future<void> updateNode(bool shouldRefresh) async {
|
|
_epicNode = NodeService(secureStorageInterface: _secureStore)
|
|
.getPrimaryNodeFor(coin: coin) ??
|
|
DefaultNodes.getNodeFor(coin);
|
|
// TODO notify ui/ fire event for node changed?
|
|
|
|
String stringConfig = await getConfig();
|
|
await _secureStore.write(key: '${_walletId}_config', value: stringConfig);
|
|
|
|
if (shouldRefresh) {
|
|
unawaited(refresh());
|
|
}
|
|
}
|
|
|
|
@override
|
|
set isFavorite(bool markFavorite) {
|
|
_isFavorite = markFavorite;
|
|
updateCachedIsFavorite(markFavorite);
|
|
}
|
|
|
|
@override
|
|
bool get isFavorite => _isFavorite ??= getCachedIsFavorite();
|
|
|
|
bool? _isFavorite;
|
|
|
|
late ReceivePort receivePort;
|
|
|
|
Future<String> startSync() async {
|
|
Logging.instance.log("request start sync", level: LogLevel.Info);
|
|
final wallet = await _secureStore.read(key: '${_walletId}_wallet');
|
|
const int refreshFromNode = 1;
|
|
if (!syncMutex.isLocked) {
|
|
await epiccash.LibEpiccash.getWalletBalances(
|
|
wallet: wallet!,
|
|
refreshFromNode: refreshFromNode,
|
|
minimumConfirmations: 10,
|
|
);
|
|
} else {
|
|
Logging.instance.log("request start sync denied", level: LogLevel.Info);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
Future<
|
|
({
|
|
double awaitingFinalization,
|
|
double pending,
|
|
double spendable,
|
|
double total
|
|
})> allWalletBalances() async {
|
|
final wallet = await _secureStore.read(key: '${_walletId}_wallet');
|
|
const refreshFromNode = 0;
|
|
return await epiccash.LibEpiccash.getWalletBalances(
|
|
wallet: wallet!,
|
|
refreshFromNode: refreshFromNode,
|
|
minimumConfirmations: MINIMUM_CONFIRMATIONS,
|
|
);
|
|
}
|
|
|
|
Timer? timer;
|
|
late final Coin _coin;
|
|
|
|
@override
|
|
Coin get coin => _coin;
|
|
|
|
late SecureStorageInterface _secureStore;
|
|
|
|
/// returns an empty String on success, error message on failure
|
|
Future<String> cancelPendingTransactionAndPost(String txSlateId) async {
|
|
try {
|
|
final String wallet = (await _secureStore.read(
|
|
key: '${_walletId}_wallet',
|
|
))!;
|
|
|
|
final result = await epiccash.LibEpiccash.cancelTransaction(
|
|
wallet: wallet,
|
|
transactionId: txSlateId,
|
|
);
|
|
Logging.instance.log(
|
|
"cancel $txSlateId result: $result",
|
|
level: LogLevel.Info,
|
|
);
|
|
return result;
|
|
} catch (e, s) {
|
|
Logging.instance.log("$e, $s", level: LogLevel.Error);
|
|
return e.toString();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<String> confirmSend({required Map<String, dynamic> txData}) async {
|
|
try {
|
|
final wallet = await _secureStore.read(key: '${_walletId}_wallet');
|
|
EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig();
|
|
|
|
// TODO determine whether it is worth sending change to a change address.
|
|
String slateId;
|
|
String receiverAddress = txData['addresss'] as String;
|
|
|
|
if (!receiverAddress.startsWith("http://") ||
|
|
!receiverAddress.startsWith("https://")) {
|
|
bool isEpicboxConnected = await testEpicboxServer(
|
|
epicboxConfig.host, epicboxConfig.port ?? 443);
|
|
if (!isEpicboxConnected) {
|
|
throw Exception("Failed to send TX : Unable to reach epicbox server");
|
|
}
|
|
}
|
|
|
|
({String commitId, String slateId}) transaction;
|
|
|
|
if (receiverAddress.startsWith("http://") ||
|
|
receiverAddress.startsWith("https://")) {
|
|
transaction = await epiccash.LibEpiccash.txHttpSend(
|
|
wallet: wallet!,
|
|
selectionStrategyIsAll: 0,
|
|
minimumConfirmations: MINIMUM_CONFIRMATIONS,
|
|
message: txData['onChainNote'] as String,
|
|
amount: (txData['recipientAmt'] as Amount).raw.toInt(),
|
|
address: txData['addresss'] as String);
|
|
} else {
|
|
transaction = await epiccash.LibEpiccash.createTransaction(
|
|
wallet: wallet!,
|
|
amount: (txData['recipientAmt'] as Amount).raw.toInt(),
|
|
address: txData['addresss'] as String,
|
|
secretKeyIndex: 0,
|
|
epicboxConfig: epicboxConfig.toString(),
|
|
minimumConfirmations: MINIMUM_CONFIRMATIONS,
|
|
note: txData['onChainNote'] as String);
|
|
}
|
|
|
|
Map<String, String> txAddressInfo = {};
|
|
txAddressInfo['from'] = await currentReceivingAddress;
|
|
txAddressInfo['to'] = txData['addresss'] as String;
|
|
await putSendToAddresses(transaction, txAddressInfo);
|
|
|
|
slateId = transaction.slateId;
|
|
return slateId;
|
|
} catch (e, s) {
|
|
Logging.instance.log("Error sending $e - $s", level: LogLevel.Error);
|
|
rethrow;
|
|
}
|
|
// return "";
|
|
}
|
|
|
|
Future<isar_models.Address> _getReceivingAddressForIndex(
|
|
int index,
|
|
) async {
|
|
isar_models.Address? address = await db
|
|
.getAddresses(walletId)
|
|
.filter()
|
|
.subTypeEqualTo(isar_models.AddressSubType.receiving)
|
|
.and()
|
|
.typeEqualTo(isar_models.AddressType.mimbleWimble)
|
|
.and()
|
|
.derivationIndexEqualTo(index)
|
|
.findFirst();
|
|
|
|
if (address == null) {
|
|
final wallet = await _secureStore.read(key: '${_walletId}_wallet');
|
|
EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig();
|
|
|
|
String? walletAddress = await epiccash.LibEpiccash.getAddressInfo(
|
|
wallet: wallet!,
|
|
index: index,
|
|
epicboxConfig: epicboxConfig.toString(),
|
|
);
|
|
|
|
Logging.instance
|
|
.log("WALLET_ADDRESS_IS $walletAddress", level: LogLevel.Info);
|
|
|
|
address = isar_models.Address(
|
|
walletId: walletId,
|
|
value: walletAddress!,
|
|
derivationIndex: index,
|
|
derivationPath: null,
|
|
type: isar_models.AddressType.mimbleWimble,
|
|
subType: isar_models.AddressSubType.receiving,
|
|
publicKey: [], // ??
|
|
);
|
|
|
|
await db.updateOrPutAddresses([address]);
|
|
}
|
|
|
|
return address;
|
|
}
|
|
|
|
@override
|
|
Future<String> get currentReceivingAddress async =>
|
|
(await _currentReceivingAddress)?.value ??
|
|
(await _getReceivingAddressForIndex(0)).value;
|
|
|
|
Future<isar_models.Address?> get _currentReceivingAddress => db
|
|
.getAddresses(walletId)
|
|
.filter()
|
|
.subTypeEqualTo(isar_models.AddressSubType.receiving)
|
|
.and()
|
|
.typeEqualTo(isar_models.AddressType.mimbleWimble)
|
|
.sortByDerivationIndexDesc()
|
|
.findFirst();
|
|
|
|
@override
|
|
Future<void> exit() async {
|
|
_hasCalledExit = true;
|
|
timer?.cancel();
|
|
timer = null;
|
|
stopNetworkAlivePinging();
|
|
Logging.instance.log("EpicCash_wallet exit finished", level: LogLevel.Info);
|
|
}
|
|
|
|
bool _hasCalledExit = false;
|
|
|
|
@override
|
|
bool get hasCalledExit => _hasCalledExit;
|
|
|
|
Future<FeeObject> _getFees() async {
|
|
// TODO: implement _getFees
|
|
return FeeObject(
|
|
numberOfBlocksFast: 10,
|
|
numberOfBlocksAverage: 10,
|
|
numberOfBlocksSlow: 10,
|
|
fast: 1,
|
|
medium: 1,
|
|
slow: 1);
|
|
}
|
|
|
|
@override
|
|
Future<FeeObject> get fees => _feeObject ??= _getFees();
|
|
Future<FeeObject>? _feeObject;
|
|
|
|
@override
|
|
Future<void> fullRescan(
|
|
int maxUnusedAddressGap,
|
|
int maxNumberOfIndexesToCheck,
|
|
) async {
|
|
refreshMutex = true;
|
|
try {
|
|
GlobalEventBus.instance.fire(
|
|
WalletSyncStatusChangedEvent(
|
|
WalletSyncStatus.syncing,
|
|
walletId,
|
|
coin,
|
|
),
|
|
);
|
|
|
|
// clear blockchain info
|
|
await db.deleteWalletBlockchainData(walletId);
|
|
|
|
await epicUpdateLastScannedBlock(await getRestoreHeight());
|
|
|
|
await _startScans();
|
|
|
|
GlobalEventBus.instance.fire(
|
|
WalletSyncStatusChangedEvent(
|
|
WalletSyncStatus.synced,
|
|
walletId,
|
|
coin,
|
|
),
|
|
);
|
|
} catch (e, s) {
|
|
GlobalEventBus.instance.fire(
|
|
WalletSyncStatusChangedEvent(
|
|
WalletSyncStatus.unableToSync,
|
|
walletId,
|
|
coin,
|
|
),
|
|
);
|
|
|
|
Logging.instance.log(
|
|
"Exception rethrown from fullRescan(): $e\n$s",
|
|
level: LogLevel.Error,
|
|
printFullLength: true,
|
|
);
|
|
rethrow;
|
|
} finally {
|
|
refreshMutex = false;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> initializeExisting() async {
|
|
Logging.instance.log("initializeExisting() ${coin.prettyName} wallet",
|
|
level: LogLevel.Info);
|
|
|
|
final config = await getRealConfig();
|
|
final password = await _secureStore.read(key: '${_walletId}_password');
|
|
|
|
final walletOpen = await epiccash.LibEpiccash.openWallet(
|
|
config: config, password: password!);
|
|
await _secureStore.write(key: '${_walletId}_wallet', value: walletOpen);
|
|
|
|
if (getCachedId() == null) {
|
|
//todo: check if print needed
|
|
// debugPrint("Exception was thrown");
|
|
throw Exception(
|
|
"Attempted to initialize an existing wallet using an unknown wallet ID!");
|
|
}
|
|
await _prefs.init();
|
|
await updateNode(false);
|
|
await _refreshBalance();
|
|
// TODO: is there anything else that should be set up here whenever this wallet is first loaded again?
|
|
}
|
|
|
|
Future<void> storeEpicboxInfo() async {
|
|
final wallet = await _secureStore.read(key: '${_walletId}_wallet');
|
|
int index = 0;
|
|
|
|
Logging.instance.log("This index is $index", level: LogLevel.Info);
|
|
EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig();
|
|
|
|
String? walletAddress = await epiccash.LibEpiccash.getAddressInfo(
|
|
wallet: wallet!,
|
|
index: index,
|
|
epicboxConfig: epicboxConfig.toString(),
|
|
);
|
|
|
|
Logging.instance
|
|
.log("WALLET_ADDRESS_IS $walletAddress", level: LogLevel.Info);
|
|
await _secureStore.write(
|
|
key: '${_walletId}_address_info', value: walletAddress);
|
|
}
|
|
|
|
// TODO: make more robust estimate of date maybe using https://explorer.epic.tech/api-index
|
|
int calculateRestoreHeightFrom({required DateTime date}) {
|
|
int secondsSinceEpoch = date.millisecondsSinceEpoch ~/ 1000;
|
|
const int epicCashFirstBlock = 1565370278;
|
|
const double overestimateSecondsPerBlock = 61;
|
|
int chosenSeconds = secondsSinceEpoch - epicCashFirstBlock;
|
|
int approximateHeight = chosenSeconds ~/ overestimateSecondsPerBlock;
|
|
//todo: check if print needed
|
|
// debugPrint(
|
|
// "approximate height: $approximateHeight chosen_seconds: $chosenSeconds");
|
|
int height = approximateHeight;
|
|
if (height < 0) {
|
|
height = 0;
|
|
}
|
|
return height;
|
|
}
|
|
|
|
@override
|
|
Future<void> initializeNew(
|
|
({String mnemonicPassphrase, int wordCount})? data,
|
|
) async {
|
|
await _prefs.init();
|
|
await updateNode(false);
|
|
final mnemonic = await _getMnemonicList();
|
|
final String mnemonicString = mnemonic.join(" ");
|
|
|
|
final String password = generatePassword();
|
|
String stringConfig = await getConfig();
|
|
EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig();
|
|
|
|
await _secureStore.write(
|
|
key: '${_walletId}_mnemonic', value: mnemonicString);
|
|
await _secureStore.write(key: '${_walletId}_config', value: stringConfig);
|
|
await _secureStore.write(key: '${_walletId}_password', value: password);
|
|
await _secureStore.write(
|
|
key: '${_walletId}_epicboxConfig', value: epicboxConfig.toString());
|
|
|
|
String name = _walletId;
|
|
|
|
await epiccash.LibEpiccash.initializeNewWallet(
|
|
config: stringConfig,
|
|
mnemonic: mnemonicString,
|
|
password: password,
|
|
name: name,
|
|
);
|
|
|
|
//Open wallet
|
|
final walletOpen = await epiccash.LibEpiccash.openWallet(
|
|
config: stringConfig, password: password);
|
|
await _secureStore.write(key: '${_walletId}_wallet', value: walletOpen);
|
|
|
|
//Store Epic box address info
|
|
await storeEpicboxInfo();
|
|
|
|
// subtract a couple days to ensure we have a buffer for SWB
|
|
final bufferedCreateHeight = calculateRestoreHeightFrom(
|
|
date: DateTime.now().subtract(const Duration(days: 2)));
|
|
|
|
await Future.wait([
|
|
epicUpdateRestoreHeight(bufferedCreateHeight),
|
|
updateCachedIsFavorite(false),
|
|
updateCachedId(walletId),
|
|
epicUpdateReceivingIndex(0),
|
|
epicUpdateChangeIndex(0),
|
|
]);
|
|
|
|
final initialReceivingAddress = await _getReceivingAddressForIndex(0);
|
|
|
|
await db.putAddress(initialReceivingAddress);
|
|
}
|
|
|
|
bool refreshMutex = false;
|
|
|
|
@override
|
|
bool get isRefreshing => refreshMutex;
|
|
|
|
@override
|
|
// unused for epic
|
|
Future<int> get maxFee => throw UnimplementedError();
|
|
|
|
Future<List<String>> _getMnemonicList() async {
|
|
String? _mnemonicString = await mnemonicString;
|
|
if (_mnemonicString != null) {
|
|
final List<String> data = _mnemonicString.split(' ');
|
|
return data;
|
|
} else {
|
|
_mnemonicString = epiccash.LibEpiccash.getMnemonic();
|
|
await _secureStore.write(
|
|
key: '${_walletId}_mnemonic', value: _mnemonicString);
|
|
final List<String> data = _mnemonicString.split(' ');
|
|
return data;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<List<String>> get mnemonic => _getMnemonicList();
|
|
|
|
@override
|
|
Future<String?> get mnemonicString =>
|
|
_secureStore.read(key: '${_walletId}_mnemonic');
|
|
|
|
@override
|
|
Future<String?> get mnemonicPassphrase => _secureStore.read(
|
|
key: '${_walletId}_mnemonicPassphrase',
|
|
);
|
|
|
|
@override
|
|
Future<Map<String, dynamic>> prepareSend({
|
|
required String address,
|
|
required Amount amount,
|
|
Map<String, dynamic>? args,
|
|
}) async {
|
|
try {
|
|
int satAmount = amount.raw.toInt();
|
|
int realfee = await nativeFee(satAmount);
|
|
|
|
if (balance.spendable == amount) {
|
|
satAmount = balance.spendable.raw.toInt() - realfee;
|
|
}
|
|
|
|
Map<String, dynamic> txData = {
|
|
"fee": realfee,
|
|
"addresss": address,
|
|
"recipientAmt": Amount(
|
|
rawValue: BigInt.from(satAmount),
|
|
fractionDigits: coin.decimals,
|
|
),
|
|
};
|
|
|
|
Logging.instance.log("prepare send: $txData", level: LogLevel.Info);
|
|
return txData;
|
|
} catch (e, s) {
|
|
Logging.instance.log("Error getting fees $e - $s", level: LogLevel.Error);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
Future<int> nativeFee(int satoshiAmount,
|
|
{bool ifErrorEstimateFee = false}) async {
|
|
final wallet = await _secureStore.read(key: '${_walletId}_wallet');
|
|
try {
|
|
final available = balance.spendable.raw.toInt();
|
|
|
|
var transactionFees = await epiccash.LibEpiccash.getTransactionFees(
|
|
wallet: wallet!,
|
|
amount: satoshiAmount,
|
|
minimumConfirmations: MINIMUM_CONFIRMATIONS,
|
|
available: available,
|
|
);
|
|
|
|
int realfee = 0;
|
|
try {
|
|
realfee =
|
|
(Decimal.parse(transactionFees.fee.toString())).toBigInt().toInt();
|
|
} catch (e, s) {
|
|
//todo: come back to this
|
|
debugPrint("$e $s");
|
|
}
|
|
return realfee;
|
|
} catch (e, s) {
|
|
Logging.instance.log("Error getting fees $e - $s", level: LogLevel.Error);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
Future<String> currentWalletDirPath() async {
|
|
Directory appDir = await StackFileSystem.applicationRootDirectory();
|
|
|
|
final path = "${appDir.path}/epiccash";
|
|
final String name = _walletId.trim();
|
|
return '$path/$name';
|
|
}
|
|
|
|
Future<String> getConfig() async {
|
|
if (_epicNode == null) {
|
|
await updateNode(false);
|
|
}
|
|
final NodeModel node = _epicNode!;
|
|
final String nodeAddress = node.host;
|
|
final int port = node.port;
|
|
|
|
final uri = Uri.parse(nodeAddress).replace(port: port);
|
|
|
|
final String nodeApiAddress = uri.toString();
|
|
|
|
final walletDir = await currentWalletDirPath();
|
|
|
|
final Map<String, dynamic> config = {};
|
|
config["wallet_dir"] = walletDir;
|
|
config["check_node_api_http_addr"] = nodeApiAddress;
|
|
config["chain"] = "mainnet";
|
|
config["account"] = "default";
|
|
config["api_listen_port"] = port;
|
|
config["api_listen_interface"] =
|
|
nodeApiAddress.replaceFirst(uri.scheme, "");
|
|
String stringConfig = json.encode(config);
|
|
return stringConfig;
|
|
}
|
|
|
|
Future<bool> testEpicboxServer(String host, int port) async {
|
|
// TODO use an EpicBoxServerModel as the only param
|
|
final websocketConnectionUri = 'wss://$host:$port';
|
|
const connectionOptions = SocketConnectionOptions(
|
|
pingIntervalMs: 3000,
|
|
timeoutConnectionMs: 4000,
|
|
|
|
/// see ping/pong messages in [logEventStream] stream
|
|
skipPingMessages: true,
|
|
|
|
/// Set this attribute to `true` if do not need any ping/pong
|
|
/// messages and ping measurement. Default is `false`
|
|
pingRestrictionForce: true,
|
|
);
|
|
|
|
final IMessageProcessor<String, String> textSocketProcessor =
|
|
SocketSimpleTextProcessor();
|
|
final textSocketHandler = IWebSocketHandler<String, String>.createClient(
|
|
websocketConnectionUri,
|
|
textSocketProcessor,
|
|
connectionOptions: connectionOptions,
|
|
);
|
|
|
|
// Listening to server responses:
|
|
bool isConnected = true;
|
|
textSocketHandler.incomingMessagesStream.listen((inMsg) {
|
|
Logging.instance.log(
|
|
'> webSocket got text message from server: "$inMsg" '
|
|
'[ping: ${textSocketHandler.pingDelayMs}]',
|
|
level: LogLevel.Info);
|
|
});
|
|
|
|
// Connecting to server:
|
|
final isTextSocketConnected = await textSocketHandler.connect();
|
|
if (!isTextSocketConnected) {
|
|
// ignore: avoid_print
|
|
Logging.instance.log(
|
|
'Connection to [$websocketConnectionUri] failed for some reason!',
|
|
level: LogLevel.Error);
|
|
isConnected = false;
|
|
}
|
|
return isConnected;
|
|
}
|
|
|
|
Future<EpicBoxConfigModel> getEpicBoxConfig() async {
|
|
EpicBoxConfigModel? _epicBoxConfig;
|
|
// read epicbox config from secure store
|
|
String? storedConfig =
|
|
await _secureStore.read(key: '${_walletId}_epicboxConfig');
|
|
|
|
// we should move to storing the primary server model like we do with nodes, and build the config from that (see epic-mobile)
|
|
// EpicBoxServerModel? _epicBox = epicBox ??
|
|
// DB.instance.get<EpicBoxServerModel>(
|
|
// boxName: DB.boxNamePrimaryEpicBox, key: 'primary');
|
|
// Logging.instance.log(
|
|
// "Read primary Epic Box config: ${jsonEncode(_epicBox)}",
|
|
// level: LogLevel.Info);
|
|
|
|
if (storedConfig == null) {
|
|
// if no config stored, use the default epicbox server as config
|
|
_epicBoxConfig =
|
|
EpicBoxConfigModel.fromServer(DefaultEpicBoxes.defaultEpicBoxServer);
|
|
} else {
|
|
// if a config is stored, test it
|
|
|
|
_epicBoxConfig = EpicBoxConfigModel.fromString(
|
|
storedConfig); // fromString handles checking old config formats
|
|
}
|
|
|
|
bool isEpicboxConnected = await testEpicboxServer(
|
|
_epicBoxConfig.host, _epicBoxConfig.port ?? 443);
|
|
|
|
if (!isEpicboxConnected) {
|
|
// default Epicbox is not connected, default to Europe
|
|
_epicBoxConfig = EpicBoxConfigModel.fromServer(DefaultEpicBoxes.europe);
|
|
|
|
// example of selecting another random server from the default list
|
|
// alternative servers: copy list of all default EB servers but remove the default default
|
|
// List<EpicBoxServerModel> alternativeServers = DefaultEpicBoxes.all;
|
|
// alternativeServers.removeWhere((opt) => opt.name == DefaultEpicBoxes.defaultEpicBoxServer.name);
|
|
// alternativeServers.shuffle(); // randomize which server is used
|
|
// _epicBoxConfig = EpicBoxConfigModel.fromServer(alternativeServers.first);
|
|
|
|
// TODO test this connection before returning it
|
|
}
|
|
|
|
return _epicBoxConfig;
|
|
}
|
|
|
|
Future<String> getRealConfig() async {
|
|
String? config = await _secureStore.read(key: '${_walletId}_config');
|
|
if (Platform.isIOS) {
|
|
final walletDir = await currentWalletDirPath();
|
|
var editConfig = jsonDecode(config as String);
|
|
|
|
editConfig["wallet_dir"] = walletDir;
|
|
config = jsonEncode(editConfig);
|
|
}
|
|
return config!;
|
|
}
|
|
|
|
Future<void> updateEpicboxConfig(String host, int port) async {
|
|
String stringConfig = jsonEncode({
|
|
"epicbox_domain": host,
|
|
"epicbox_port": port,
|
|
"epicbox_protocol_unsecure": false,
|
|
"epicbox_address_index": 0,
|
|
});
|
|
await _secureStore.write(
|
|
key: '${_walletId}_epicboxConfig', value: stringConfig);
|
|
// TODO: refresh anything that needs to be refreshed/updated due to epicbox info changed
|
|
}
|
|
|
|
Future<void> _startScans() async {
|
|
try {
|
|
//First stop the current listener
|
|
epiccash.LibEpiccash.stopEpicboxListener();
|
|
final wallet = await _secureStore.read(key: '${_walletId}_wallet');
|
|
|
|
// max number of blocks to scan per loop iteration
|
|
const scanChunkSize = 10000;
|
|
|
|
// force firing of scan progress event
|
|
await getSyncPercent;
|
|
|
|
// fetch current chain height and last scanned block (should be the
|
|
// restore height if full rescan or a wallet restore)
|
|
int chainHeight = await this.chainHeight;
|
|
int lastScannedBlock =
|
|
epicGetLastScannedBlock() ?? await getRestoreHeight();
|
|
|
|
// loop while scanning in chain in chunks (of blocks?)
|
|
while (lastScannedBlock < chainHeight) {
|
|
Logging.instance.log(
|
|
"chainHeight: $chainHeight, lastScannedBlock: $lastScannedBlock",
|
|
level: LogLevel.Info,
|
|
);
|
|
|
|
int nextScannedBlock = await epiccash.LibEpiccash.scanOutputs(
|
|
wallet: wallet!,
|
|
startHeight: lastScannedBlock,
|
|
numberOfBlocks: scanChunkSize,
|
|
);
|
|
|
|
// update local cache
|
|
await epicUpdateLastScannedBlock(nextScannedBlock);
|
|
|
|
// force firing of scan progress event
|
|
await getSyncPercent;
|
|
|
|
// update while loop condition variables
|
|
chainHeight = await this.chainHeight;
|
|
lastScannedBlock = nextScannedBlock;
|
|
}
|
|
|
|
Logging.instance.log(
|
|
"_startScans successfully at the tip",
|
|
level: LogLevel.Info,
|
|
);
|
|
//Once scanner completes restart listener
|
|
await listenToEpicbox();
|
|
} catch (e, s) {
|
|
Logging.instance.log(
|
|
"_startScans failed: $e\n$s",
|
|
level: LogLevel.Error,
|
|
);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
Future<double> get getSyncPercent async {
|
|
int lastScannedBlock = epicGetLastScannedBlock() ?? 0;
|
|
final _chainHeight = await chainHeight;
|
|
double restorePercent = lastScannedBlock / _chainHeight;
|
|
GlobalEventBus.instance
|
|
.fire(RefreshPercentChangedEvent(highestPercent, walletId));
|
|
if (restorePercent > highestPercent) {
|
|
highestPercent = restorePercent;
|
|
}
|
|
|
|
final int blocksRemaining = _chainHeight - lastScannedBlock;
|
|
GlobalEventBus.instance
|
|
.fire(BlocksRemainingEvent(blocksRemaining, walletId));
|
|
|
|
return restorePercent < 0 ? 0.0 : restorePercent;
|
|
}
|
|
|
|
double highestPercent = 0;
|
|
|
|
@override
|
|
Future<void> recoverFromMnemonic({
|
|
required String mnemonic,
|
|
String? mnemonicPassphrase, // unused in epic
|
|
required int maxUnusedAddressGap,
|
|
required int maxNumberOfIndexesToCheck,
|
|
required int height,
|
|
}) async {
|
|
try {
|
|
await _prefs.init();
|
|
await updateNode(false);
|
|
final String password = generatePassword();
|
|
|
|
String stringConfig = await getConfig();
|
|
EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig();
|
|
final String name = _walletName.trim();
|
|
|
|
await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic);
|
|
await _secureStore.write(key: '${_walletId}_config', value: stringConfig);
|
|
await _secureStore.write(key: '${_walletId}_password', value: password);
|
|
|
|
await _secureStore.write(
|
|
key: '${_walletId}_epicboxConfig', value: epicboxConfig.toString());
|
|
|
|
await epiccash.LibEpiccash.recoverWallet(
|
|
config: stringConfig,
|
|
password: password,
|
|
mnemonic: mnemonic,
|
|
name: name,
|
|
);
|
|
|
|
await Future.wait([
|
|
epicUpdateRestoreHeight(height),
|
|
updateCachedId(walletId),
|
|
epicUpdateReceivingIndex(0),
|
|
epicUpdateChangeIndex(0),
|
|
updateCachedIsFavorite(false),
|
|
]);
|
|
|
|
//Open Wallet
|
|
final walletOpen = await epiccash.LibEpiccash.openWallet(
|
|
config: stringConfig, password: password);
|
|
await _secureStore.write(key: '${_walletId}_wallet', value: walletOpen);
|
|
|
|
//Store Epic box address info
|
|
await storeEpicboxInfo();
|
|
} catch (e, s) {
|
|
Logging.instance
|
|
.log("Error recovering wallet $e\n$s", level: LogLevel.Error);
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
Future<void> listenToEpicbox() async {
|
|
Logging.instance.log("STARTING WALLET LISTENER ....", level: LogLevel.Info);
|
|
final wallet = await _secureStore.read(key: '${_walletId}_wallet');
|
|
EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig();
|
|
epiccash.LibEpiccash.startEpicboxListener(
|
|
wallet: wallet!, epicboxConfig: epicboxConfig.toString());
|
|
}
|
|
|
|
Future<int> getRestoreHeight() async {
|
|
return epicGetRestoreHeight() ?? epicGetCreationHeight()!;
|
|
}
|
|
|
|
Future<int> get chainHeight async {
|
|
try {
|
|
final config = await getRealConfig();
|
|
int? latestHeight =
|
|
await epiccash.LibEpiccash.getChainHeight(config: config);
|
|
|
|
await updateCachedChainHeight(latestHeight);
|
|
if (latestHeight > storedChainHeight) {
|
|
GlobalEventBus.instance.fire(
|
|
UpdatedInBackgroundEvent(
|
|
"Updated current chain height in $walletId $walletName!",
|
|
walletId,
|
|
),
|
|
);
|
|
}
|
|
return latestHeight;
|
|
} catch (e, s) {
|
|
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
|
|
level: LogLevel.Error);
|
|
return storedChainHeight;
|
|
}
|
|
}
|
|
|
|
@override
|
|
int get storedChainHeight => getCachedChainHeight();
|
|
|
|
bool _shouldAutoSync = true;
|
|
|
|
@override
|
|
bool get shouldAutoSync => _shouldAutoSync;
|
|
|
|
@override
|
|
set shouldAutoSync(bool shouldAutoSync) {
|
|
if (_shouldAutoSync != shouldAutoSync) {
|
|
_shouldAutoSync = shouldAutoSync;
|
|
if (!shouldAutoSync) {
|
|
Logging.instance.log("Should autosync", level: LogLevel.Info);
|
|
timer?.cancel();
|
|
timer = null;
|
|
stopNetworkAlivePinging();
|
|
} else {
|
|
startNetworkAlivePinging();
|
|
refresh();
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<int> setCurrentIndex() async {
|
|
try {
|
|
final int receivingIndex = epicGetReceivingIndex()!;
|
|
// TODO: go through pendingarray and processed array and choose the index
|
|
// of the last one that has not been processed, or the index after the one most recently processed;
|
|
return receivingIndex;
|
|
} catch (e, s) {
|
|
Logging.instance.log("$e $s", level: LogLevel.Error);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
Future<Map<dynamic, dynamic>> getSlatesToCommits() async {
|
|
try {
|
|
var slatesToCommits = epicGetSlatesToCommits();
|
|
if (slatesToCommits == null) {
|
|
slatesToCommits = <dynamic, dynamic>{};
|
|
} else {
|
|
slatesToCommits = slatesToCommits;
|
|
}
|
|
return slatesToCommits;
|
|
} catch (e, s) {
|
|
Logging.instance.log("$e $s", level: LogLevel.Error);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
Future<bool> putSendToAddresses(({String slateId, String commitId}) slateData,
|
|
Map<String, String> txAddressInfo) async {
|
|
try {
|
|
var slatesToCommits = await getSlatesToCommits();
|
|
final from = txAddressInfo['from'];
|
|
final to = txAddressInfo['to'];
|
|
slatesToCommits[slateData.slateId] = {
|
|
"commitId": slateData.commitId,
|
|
"from": from,
|
|
"to": to,
|
|
};
|
|
|
|
await epicUpdateSlatesToCommits(slatesToCommits);
|
|
return true;
|
|
} catch (e, s) {
|
|
Logging.instance
|
|
.log("ERROR STORING ADDRESS $e $s", level: LogLevel.Error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<bool> putSlatesToCommits(String slateMessage, String encoded) async {
|
|
try {
|
|
var slatesToCommits = await getSlatesToCommits();
|
|
final slate = jsonDecode(slateMessage);
|
|
final part1 = jsonDecode(slate[0] as String);
|
|
final part2 = jsonDecode(slate[1] as String);
|
|
final slateId = part1[0]['tx_slate_id'];
|
|
if (slatesToCommits[slateId] != null &&
|
|
(slatesToCommits[slateId] as Map).isNotEmpty) {
|
|
// This happens when the sender receives the response.
|
|
return true;
|
|
}
|
|
final commitId = part2['tx']['body']['outputs'][0]['commit'];
|
|
|
|
final toFromInfoString = jsonDecode(encoded);
|
|
final toFromInfo = jsonDecode(toFromInfoString[0] as String);
|
|
final from = toFromInfo['from'];
|
|
final to = toFromInfo['to'];
|
|
slatesToCommits[slateId] = {
|
|
"commitId": commitId,
|
|
"from": from,
|
|
"to": to,
|
|
};
|
|
await epicUpdateSlatesToCommits(slatesToCommits);
|
|
return true;
|
|
} catch (e, s) {
|
|
Logging.instance.log("$e $s", level: LogLevel.Error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Refreshes display data for the wallet
|
|
@override
|
|
Future<void> refresh() async {
|
|
Logging.instance
|
|
.log("$walletId $walletName Calling refresh", level: LogLevel.Info);
|
|
if (refreshMutex) {
|
|
Logging.instance.log("$walletId $walletName refreshMutex denied",
|
|
level: LogLevel.Info);
|
|
return;
|
|
} else {
|
|
refreshMutex = true;
|
|
}
|
|
|
|
try {
|
|
GlobalEventBus.instance.fire(
|
|
WalletSyncStatusChangedEvent(
|
|
WalletSyncStatus.syncing,
|
|
walletId,
|
|
coin,
|
|
),
|
|
);
|
|
|
|
if (epicGetCreationHeight() == null) {
|
|
await epicUpdateCreationHeight(await chainHeight);
|
|
}
|
|
|
|
final int curAdd = await setCurrentIndex();
|
|
await _getReceivingAddressForIndex(curAdd);
|
|
|
|
await _startScans();
|
|
|
|
unawaited(startSync());
|
|
|
|
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId));
|
|
|
|
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId));
|
|
|
|
final currentHeight = await chainHeight;
|
|
const storedHeight = 1; //await storedChainHeight;
|
|
|
|
Logging.instance.log("chain height in refresh function: $currentHeight",
|
|
level: LogLevel.Info);
|
|
Logging.instance.log("cached height in refresh function: $storedHeight",
|
|
level: LogLevel.Info);
|
|
|
|
// TODO: implement refresh
|
|
// TODO: check if it needs a refresh and if so get all of the most recent data.
|
|
if (currentHeight != storedHeight) {
|
|
await _refreshTransactions();
|
|
GlobalEventBus.instance
|
|
.fire(RefreshPercentChangedEvent(0.50, walletId));
|
|
|
|
GlobalEventBus.instance.fire(UpdatedInBackgroundEvent(
|
|
"New data found in $walletName in background!", walletId));
|
|
}
|
|
|
|
await _refreshBalance();
|
|
|
|
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId));
|
|
GlobalEventBus.instance.fire(
|
|
WalletSyncStatusChangedEvent(
|
|
WalletSyncStatus.synced,
|
|
walletId,
|
|
coin,
|
|
),
|
|
);
|
|
refreshMutex = false;
|
|
if (shouldAutoSync) {
|
|
timer ??= Timer.periodic(const Duration(seconds: 60), (timer) async {
|
|
Logging.instance.log(
|
|
"Periodic refresh check for $walletId $walletName in object instance: $hashCode",
|
|
level: LogLevel.Info);
|
|
// 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));
|
|
}
|
|
// }
|
|
});
|
|
}
|
|
} catch (error, strace) {
|
|
refreshMutex = false;
|
|
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.Warning);
|
|
}
|
|
}
|
|
|
|
Future<bool> refreshIfThereIsNewData() async {
|
|
if (_hasCalledExit) return false;
|
|
// TODO returning true here signals this class to call refresh() after which it will fire an event that notifies the UI that new data has been fetched/found for this wallet
|
|
return true;
|
|
// TODO: do a quick check to see if there is any new data that would require a refresh
|
|
}
|
|
|
|
@override
|
|
Future<bool> testNetworkConnection() async {
|
|
try {
|
|
// force unwrap optional as we want connection test to fail if wallet
|
|
// wasn't initialized or epicbox node was set to null
|
|
return await testEpicNodeConnection(
|
|
NodeFormData()
|
|
..host = _epicNode!.host
|
|
..useSSL = _epicNode!.useSSL
|
|
..port = _epicNode!.port,
|
|
) !=
|
|
null;
|
|
} catch (e, s) {
|
|
Logging.instance.log("$e\n$s", level: LogLevel.Warning);
|
|
return 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();
|
|
|
|
if (_isConnected != hasNetwork) {
|
|
NodeConnectionStatus status = hasNetwork
|
|
? NodeConnectionStatus.connected
|
|
: NodeConnectionStatus.disconnected;
|
|
GlobalEventBus.instance
|
|
.fire(NodeConnectionStatusChangedEvent(status, walletId, coin));
|
|
|
|
_isConnected = hasNetwork;
|
|
if (hasNetwork) {
|
|
unawaited(refresh());
|
|
}
|
|
}
|
|
}
|
|
|
|
void stopNetworkAlivePinging() {
|
|
_networkAliveTimer?.cancel();
|
|
_networkAliveTimer = null;
|
|
}
|
|
|
|
bool _isConnected = false;
|
|
|
|
@override
|
|
bool get isConnected => _isConnected;
|
|
|
|
Future<void> _refreshTransactions() async {
|
|
final wallet = await _secureStore.read(key: '${_walletId}_wallet');
|
|
const refreshFromNode = 1;
|
|
|
|
var transactions = await epiccash.LibEpiccash.getTransactions(
|
|
wallet: wallet!, refreshFromNode: refreshFromNode);
|
|
|
|
final List<Tuple2<isar_models.Transaction, isar_models.Address?>> txnsData =
|
|
[];
|
|
|
|
final slatesToCommits = await getSlatesToCommits();
|
|
|
|
for (var tx in transactions) {
|
|
Logging.instance.log("tx: $tx", level: LogLevel.Info);
|
|
// // TODO: does "confirmed" mean finalized? If so please remove this todo
|
|
final isConfirmed = tx.confirmed;
|
|
|
|
int amt = 0;
|
|
if (tx.txType == TransactionType.TxReceived ||
|
|
tx.txType == TransactionType.TxReceivedCancelled) {
|
|
amt = int.parse(tx.amountCredited);
|
|
} else {
|
|
int debit = int.parse(tx.amountDebited);
|
|
int credit = int.parse(tx.amountCredited);
|
|
int fee = int.parse((tx.fee ?? "0")); //TODO -double check this
|
|
amt = debit - credit - fee;
|
|
}
|
|
|
|
DateTime dt = DateTime.parse(tx.creationTs);
|
|
|
|
String? slateId = tx.txSlateId;
|
|
String address = slatesToCommits[slateId]
|
|
?[tx.txType == TransactionType.TxReceived ? "from" : "to"]
|
|
as String? ??
|
|
"";
|
|
String? commitId = slatesToCommits[slateId]?['commitId'] as String?;
|
|
int? numberOfMessages = tx.messages?.messages.length;
|
|
String? onChainNote = tx.messages?.messages[0].message;
|
|
|
|
int? height;
|
|
|
|
if (isConfirmed) {
|
|
height = tx.kernelLookupMinHeight ?? 1;
|
|
} else {
|
|
height = null;
|
|
}
|
|
|
|
final isIncoming = (tx.txType == TransactionType.TxReceived ||
|
|
tx.txType == TransactionType.TxReceivedCancelled);
|
|
final txn = isar_models.Transaction(
|
|
walletId: walletId,
|
|
txid: commitId ?? tx.id.toString(),
|
|
timestamp: (dt.millisecondsSinceEpoch ~/ 1000),
|
|
type: isIncoming
|
|
? isar_models.TransactionType.incoming
|
|
: isar_models.TransactionType.outgoing,
|
|
subType: isar_models.TransactionSubType.none,
|
|
amount: amt,
|
|
amountString: Amount(
|
|
rawValue: BigInt.from(amt),
|
|
fractionDigits: coin.decimals,
|
|
).toJsonString(),
|
|
fee: (tx.fee == "null") ? 0 : int.parse(tx.fee!),
|
|
height: height,
|
|
isCancelled: tx.txType == TransactionType.TxSentCancelled ||
|
|
tx.txType == TransactionType.TxReceivedCancelled,
|
|
isLelantus: false,
|
|
slateId: slateId,
|
|
nonce: null,
|
|
otherData: onChainNote,
|
|
inputs: [],
|
|
outputs: [],
|
|
numberOfMessages: numberOfMessages,
|
|
);
|
|
|
|
// txn.address =
|
|
// ""; // for this when you send a transaction you will just need to save in a hashmap in hive with the key being the txid, and the value being the address it was sent to. then you can look this value up right here in your hashmap.
|
|
isar_models.Address? transactionAddress = await db
|
|
.getAddresses(walletId)
|
|
.filter()
|
|
.valueEqualTo(address)
|
|
.findFirst();
|
|
|
|
if (transactionAddress == null || transactionAddress!.value.isEmpty) {
|
|
if (isIncoming) {
|
|
//Use current receiving address as address
|
|
String receivingAddress = await currentReceivingAddress;
|
|
transactionAddress = isar_models.Address(
|
|
walletId: walletId,
|
|
value: receivingAddress,
|
|
publicKey: [],
|
|
derivationIndex: 0,
|
|
derivationPath: null,
|
|
type: isar_models.AddressType.mimbleWimble,
|
|
subType: isar_models.AddressSubType.receiving,
|
|
);
|
|
} else {
|
|
final myRcvAddr = await currentReceivingAddress;
|
|
final isSentToSelf = myRcvAddr == address;
|
|
|
|
transactionAddress = isar_models.Address(
|
|
walletId: walletId,
|
|
value: address,
|
|
publicKey: [],
|
|
derivationIndex: isSentToSelf ? 0 : -1,
|
|
derivationPath: null,
|
|
type: isSentToSelf
|
|
? isar_models.AddressType.mimbleWimble
|
|
: isar_models.AddressType.nonWallet,
|
|
subType: isSentToSelf
|
|
? isar_models.AddressSubType.receiving
|
|
: isar_models.AddressSubType.nonWallet,
|
|
);
|
|
}
|
|
}
|
|
|
|
txnsData.add(Tuple2(txn, transactionAddress));
|
|
}
|
|
|
|
await db.addNewTransactionData(txnsData, walletId);
|
|
|
|
// quick hack to notify manager to call notifyListeners if
|
|
// transactions changed
|
|
if (txnsData.isNotEmpty) {
|
|
GlobalEventBus.instance.fire(
|
|
UpdatedInBackgroundEvent(
|
|
"Transactions updated/added for: $walletId $walletName ",
|
|
walletId,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
|
|
// not used in epic
|
|
}
|
|
|
|
@override
|
|
bool validateAddress(String address) {
|
|
// Invalid address that contains HTTP and epicbox domain
|
|
if ((address.startsWith("http://") || address.startsWith("https://")) &&
|
|
address.contains("@")) {
|
|
return false;
|
|
}
|
|
if (address.startsWith("http://") || address.startsWith("https://")) {
|
|
if (Uri.tryParse(address) != null) {
|
|
return true;
|
|
}
|
|
}
|
|
return epiccash.LibEpiccash.validateSendAddress(address: address);
|
|
}
|
|
|
|
@override
|
|
String get walletId => _walletId;
|
|
late final String _walletId;
|
|
|
|
@override
|
|
String get walletName => _walletName;
|
|
late String _walletName;
|
|
|
|
@override
|
|
set walletName(String newName) => _walletName = newName;
|
|
|
|
@override
|
|
void Function(bool)? get onIsActiveWalletChanged => (isActive) async {
|
|
timer?.cancel();
|
|
timer = null;
|
|
if (isActive) {
|
|
unawaited(startSync());
|
|
}
|
|
this.isActive = isActive;
|
|
};
|
|
|
|
bool isActive = false;
|
|
|
|
@override
|
|
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
|
|
int currentFee =
|
|
await nativeFee(amount.raw.toInt(), ifErrorEstimateFee: true);
|
|
return Amount(
|
|
rawValue: BigInt.from(currentFee),
|
|
fractionDigits: coin.decimals,
|
|
);
|
|
}
|
|
|
|
// not used in epic currently
|
|
@override
|
|
Future<bool> generateNewAddress() async {
|
|
try {
|
|
return true;
|
|
} catch (e, s) {
|
|
Logging.instance.log(
|
|
"Exception rethrown from generateNewAddress(): $e\n$s",
|
|
level: LogLevel.Error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<void> _refreshBalance() async {
|
|
var balances = await allWalletBalances();
|
|
_balance = Balance(
|
|
total: Amount.fromDecimal(
|
|
Decimal.parse(balances.total.toString()) +
|
|
Decimal.parse(balances.awaitingFinalization.toString()),
|
|
fractionDigits: coin.decimals,
|
|
),
|
|
spendable: Amount.fromDecimal(
|
|
Decimal.parse(balances.spendable.toString()),
|
|
fractionDigits: coin.decimals,
|
|
),
|
|
blockedTotal: Amount(
|
|
rawValue: BigInt.zero,
|
|
fractionDigits: coin.decimals,
|
|
),
|
|
pendingSpendable: Amount.fromDecimal(
|
|
Decimal.parse(balances.pending.toString()),
|
|
fractionDigits: coin.decimals,
|
|
),
|
|
);
|
|
await updateCachedBalance(_balance!);
|
|
}
|
|
|
|
@override
|
|
Balance get balance => _balance ??= getCachedBalance();
|
|
Balance? _balance;
|
|
|
|
@override
|
|
Future<List<isar_models.UTXO>> get utxos => throw UnimplementedError();
|
|
|
|
@override
|
|
Future<List<isar_models.Transaction>> get transactions =>
|
|
db.getTransactions(walletId).findAll();
|
|
}
|