stack_wallet/lib/wallets/wallet/impl/epiccash_wallet.dart

1185 lines
38 KiB
Dart
Raw Normal View History

2024-01-09 16:56:05 +00:00
import 'dart:async';
import 'dart:convert';
import 'dart:io';
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' as epic_models;
import 'package:isar/isar.dart';
2024-01-09 16:56:05 +00:00
import 'package:mutex/mutex.dart';
import 'package:stack_wallet_backup/generate_password.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/epicbox_config_model.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart';
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';
2024-01-09 16:56:05 +00:00
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/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
2024-01-09 16:56:05 +00:00
import 'package:stackwallet/utilities/default_epicboxes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
import 'package:stackwallet/utilities/logger.dart';
2024-01-09 16:56:05 +00:00
import 'package:stackwallet/utilities/stack_file_system.dart';
import 'package:stackwallet/utilities/test_epic_box_connection.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/epiccash.dart';
2023-11-15 17:40:43 +00:00
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
2023-09-18 21:28:31 +00:00
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart';
2024-01-09 16:56:05 +00:00
import 'package:stackwallet/wallets/wallet/supporting/epiccash_wallet_info_extension.dart';
import 'package:websocket_universal/websocket_universal.dart';
2023-09-18 21:28:31 +00:00
2024-01-09 16:56:05 +00:00
//
// refactor of https://github.com/cypherstack/stack_wallet/blob/1d9fb4cd069f22492ece690ac788e05b8f8b1209/lib/services/coins/epiccash/epiccash_wallet.dart
//
2023-09-18 21:28:31 +00:00
class EpiccashWallet extends Bip39Wallet {
2023-11-15 17:40:43 +00:00
EpiccashWallet(CryptoCurrencyNetwork network) : super(Epiccash(network));
2023-09-18 21:28:31 +00:00
2024-01-09 16:56:05 +00:00
final syncMutex = Mutex();
NodeModel? _epicNode;
Timer? timer;
double highestPercent = 0;
Future<double> get getSyncPercent async {
int lastScannedBlock = info.epicData?.lastScannedBlock ?? 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;
}
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 secureStorageInterface.write(
key: '${walletId}_epicboxConfig',
value: stringConfig,
);
// TODO: refresh anything that needs to be refreshed/updated due to epicbox info changed
}
/// returns an empty String on success, error message on failure
Future<String> cancelPendingTransactionAndPost(String txSlateId) async {
try {
final String wallet = (await secureStorageInterface.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();
}
}
Future<EpicBoxConfigModel> getEpicBoxConfig() async {
EpicBoxConfigModel? _epicBoxConfig;
// read epicbox config from secure store
String? storedConfig =
await secureStorageInterface.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;
}
// ================= Private =================================================
Future<String> _getConfig() async {
if (_epicNode == null) {
await updateNode();
}
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 = jsonEncode(config);
return stringConfig;
}
Future<String> _currentWalletDirPath() async {
Directory appDir = await StackFileSystem.applicationRootDirectory();
final path = "${appDir.path}/epiccash";
final String name = walletId.trim();
return '$path/$name';
}
Future<int> _nativeFee(
int satoshiAmount, {
bool ifErrorEstimateFee = false,
}) async {
final wallet = await secureStorageInterface.read(key: '${walletId}_wallet');
try {
final available = info.cachedBalance.spendable.raw.toInt();
var transactionFees = await epiccash.LibEpiccash.getTransactionFees(
wallet: wallet!,
amount: satoshiAmount,
minimumConfirmations: cryptoCurrency.minConfirms,
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<void> _startSync() async {
Logging.instance.log("request start sync", level: LogLevel.Info);
final wallet = await secureStorageInterface.read(key: '${walletId}_wallet');
const int refreshFromNode = 1;
if (!syncMutex.isLocked) {
await syncMutex.protect(() async {
// How does getWalletBalances start syncing????
await epiccash.LibEpiccash.getWalletBalances(
wallet: wallet!,
refreshFromNode: refreshFromNode,
minimumConfirmations: 10,
);
});
} else {
Logging.instance.log("request start sync denied", level: LogLevel.Info);
}
}
Future<
({
double awaitingFinalization,
double pending,
double spendable,
double total
})> _allWalletBalances() async {
final wallet = await secureStorageInterface.read(key: '${walletId}_wallet');
const refreshFromNode = 0;
return await epiccash.LibEpiccash.getWalletBalances(
wallet: wallet!,
refreshFromNode: refreshFromNode,
minimumConfirmations: cryptoCurrency.minConfirms,
);
}
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<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 info.updateExtraEpiccashWalletInfo(
epicData: info.epicData!.copyWith(
slatesToCommits: slatesToCommits,
),
isar: mainDB.isar,
);
return true;
} catch (e, s) {
Logging.instance
.log("ERROR STORING ADDRESS $e $s", level: LogLevel.Error);
return false;
}
}
// TODO: [prio=high] this isn't needed. Condense to `info.epicData?.slatesToCommits ?? {}` where needed
Future<Map<dynamic, dynamic>> _getSlatesToCommits() async {
try {
var slatesToCommits = info.epicData?.slatesToCommits;
if (slatesToCommits == null) {
slatesToCommits = <dynamic, dynamic>{};
} else {
slatesToCommits = slatesToCommits;
}
return slatesToCommits;
} catch (e, s) {
Logging.instance.log("$e $s", level: LogLevel.Error);
return {};
}
}
Future<int> _getCurrentIndex() async {
try {
final int receivingIndex = info.epicData!.receivingIndex;
// 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<Address> _generateAndStoreReceivingAddressForIndex(
int index,
) async {
Address? address = await getCurrentReceivingAddress();
if (address == null) {
final wallet =
await secureStorageInterface.read(key: '${walletId}_wallet');
EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig();
final walletAddress = await epiccash.LibEpiccash.getAddressInfo(
wallet: wallet!,
index: index,
epicboxConfig: epicboxConfig.toString(),
);
Logging.instance.log(
"WALLET_ADDRESS_IS $walletAddress",
level: LogLevel.Info,
);
address = Address(
walletId: walletId,
value: walletAddress,
derivationIndex: index,
derivationPath: null,
type: AddressType.mimbleWimble,
subType: AddressSubType.receiving,
publicKey: [], // ??
);
await mainDB.updateOrPutAddresses([address]);
}
return address;
}
Future<void> _startScans() async {
try {
//First stop the current listener
epiccash.LibEpiccash.stopEpicboxListener();
final wallet =
await secureStorageInterface.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 = info.epicData!.lastScannedBlock;
// 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 info.updateExtraEpiccashWalletInfo(
epicData: info.epicData!.copyWith(
lastScannedBlock: nextScannedBlock,
),
isar: mainDB.isar,
);
// 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<void> _listenToEpicbox() async {
Logging.instance.log("STARTING WALLET LISTENER ....", level: LogLevel.Info);
final wallet = await secureStorageInterface.read(key: '${walletId}_wallet');
EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig();
epiccash.LibEpiccash.startEpicboxListener(
wallet: wallet!,
epicboxConfig: epicboxConfig.toString(),
);
}
// TODO: [prio=high] what is the point of this???
Future<String> _getRealConfig() async {
String? config = await secureStorageInterface.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!;
}
// 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;
}
// ============== Overrides ==================================================
@override
int get isarTransactionVersion => 2;
2023-09-18 21:28:31 +00:00
@override
FilterOperation? get changeAddressFilterOperation =>
FilterGroup.and(standardChangeAddressFilters);
@override
FilterOperation? get receivingAddressFilterOperation =>
FilterGroup.and(standardReceivingAddressFilters);
@override
2024-01-09 20:43:58 +00:00
Future<void> init({bool? isRestore}) async {
if (isRestore != true) {
String? encodedWallet =
await secureStorageInterface.read(key: "${walletId}_wallet");
// check if should create a new wallet
if (encodedWallet == null) {
await updateNode();
final mnemonicString = await getMnemonic();
final String password = generatePassword();
final String stringConfig = await _getConfig();
final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig();
await secureStorageInterface.write(
key: '${walletId}_config', value: stringConfig);
await secureStorageInterface.write(
key: '${walletId}_password', value: password);
await secureStorageInterface.write(
key: '${walletId}_epicboxConfig', value: epicboxConfig.toString());
String name = walletId;
await epiccash.LibEpiccash.initializeNewWallet(
config: stringConfig,
mnemonic: mnemonicString,
password: password,
name: name,
);
2024-01-09 16:56:05 +00:00
2024-01-09 20:43:58 +00:00
//Open wallet
encodedWallet = await epiccash.LibEpiccash.openWallet(
config: stringConfig,
password: password,
);
await secureStorageInterface.write(
key: '${walletId}_wallet',
value: encodedWallet,
);
2024-01-09 16:56:05 +00:00
2024-01-09 20:43:58 +00:00
//Store Epic box address info
await _generateAndStoreReceivingAddressForIndex(0);
// subtract a couple days to ensure we have a buffer for SWB
final bufferedCreateHeight = _calculateRestoreHeightFrom(
date: DateTime.now().subtract(const Duration(days: 2)));
final epicData = ExtraEpiccashWalletInfo(
receivingIndex: 0,
changeIndex: 0,
slatesToAddresses: {},
slatesToCommits: {},
lastScannedBlock: bufferedCreateHeight,
restoreHeight: bufferedCreateHeight,
creationHeight: bufferedCreateHeight,
);
2024-01-09 16:56:05 +00:00
2024-01-09 20:43:58 +00:00
await info.updateExtraEpiccashWalletInfo(
epicData: epicData,
isar: mainDB.isar,
);
} else {
Logging.instance.log(
"initializeExisting() ${cryptoCurrency.coin.prettyName} wallet",
level: LogLevel.Info);
2024-01-09 16:56:05 +00:00
2024-01-09 20:43:58 +00:00
final config = await _getRealConfig();
final password =
await secureStorageInterface.read(key: '${walletId}_password');
2024-01-09 16:56:05 +00:00
2024-01-09 20:43:58 +00:00
final walletOpen = await epiccash.LibEpiccash.openWallet(
config: config,
password: password!,
);
await secureStorageInterface.write(
key: '${walletId}_wallet', value: walletOpen);
2024-01-09 16:56:05 +00:00
2024-01-09 20:43:58 +00:00
await updateNode();
await updateBalance();
// TODO: is there anything else that should be set up here whenever this wallet is first loaded again?
}
2024-01-09 16:56:05 +00:00
}
return await super.init();
2023-09-18 21:28:31 +00:00
}
@override
2024-01-09 16:56:05 +00:00
Future<TxData> confirmSend({required TxData txData}) async {
try {
final wallet =
await secureStorageInterface.read(key: '${walletId}_wallet');
final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig();
// TODO determine whether it is worth sending change to a change address.
final String receiverAddress = txData.recipients!.first.address;
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: cryptoCurrency.minConfirms,
message: txData.noteOnChain!,
amount: txData.recipients!.first.amount.raw.toInt(),
address: txData.recipients!.first.address,
);
} else {
transaction = await epiccash.LibEpiccash.createTransaction(
wallet: wallet!,
amount: txData.recipients!.first.amount.raw.toInt(),
address: txData.recipients!.first.address,
secretKeyIndex: 0,
epicboxConfig: epicboxConfig.toString(),
minimumConfirmations: cryptoCurrency.minConfirms,
note: txData.noteOnChain!,
);
}
final Map<String, String> txAddressInfo = {};
txAddressInfo['from'] = (await getCurrentReceivingAddress())!.value;
txAddressInfo['to'] = txData.recipients!.first.address;
await _putSendToAddresses(transaction, txAddressInfo);
return txData.copyWith(
txid: transaction.slateId,
);
} catch (e, s) {
Logging.instance.log(
"Epic cash confirmSend: $e\n$s",
level: LogLevel.Error,
);
rethrow;
}
2023-09-18 21:28:31 +00:00
}
@override
2024-01-09 16:56:05 +00:00
Future<TxData> prepareSend({required TxData txData}) async {
try {
if (txData.recipients?.length != 1) {
throw Exception("Epic cash prepare send requires a single recipient!");
}
({String address, Amount amount, bool isChange}) recipient =
txData.recipients!.first;
final int realFee = await _nativeFee(recipient.amount.raw.toInt());
final feeAmount = Amount(
rawValue: BigInt.from(realFee),
fractionDigits: cryptoCurrency.fractionDigits,
);
if (feeAmount > info.cachedBalance.spendable) {
throw Exception(
"Epic cash prepare send fee is greater than available balance!");
}
if (info.cachedBalance.spendable == recipient.amount) {
recipient = (
address: recipient.address,
amount: recipient.amount - feeAmount,
isChange: recipient.isChange,
);
}
return txData.copyWith(
recipients: [recipient],
fee: feeAmount,
);
} catch (e, s) {
Logging.instance
.log("Epic cash prepareSend: $e\n$s", level: LogLevel.Error);
rethrow;
}
2023-09-18 21:28:31 +00:00
}
@override
2024-01-09 16:56:05 +00:00
Future<void> recover({required bool isRescan}) async {
try {
await refreshMutex.protect(() async {
if (isRescan) {
// clear blockchain info
await mainDB.deleteWalletBlockchainData(walletId);
await info.updateExtraEpiccashWalletInfo(
epicData: info.epicData!.copyWith(
lastScannedBlock: info.epicData!.restoreHeight,
),
isar: mainDB.isar,
);
2024-01-09 20:43:58 +00:00
unawaited(_startScans());
2024-01-09 16:56:05 +00:00
} else {
await updateNode();
final String password = generatePassword();
final String stringConfig = await _getConfig();
final EpicBoxConfigModel epicboxConfig = await getEpicBoxConfig();
await secureStorageInterface.write(
key: '${walletId}_config',
value: stringConfig,
);
await secureStorageInterface.write(
key: '${walletId}_password',
value: password,
);
await secureStorageInterface.write(
key: '${walletId}_epicboxConfig',
value: epicboxConfig.toString(),
);
await epiccash.LibEpiccash.recoverWallet(
config: stringConfig,
password: password,
mnemonic: await getMnemonic(),
name: info.walletId,
);
final epicData = ExtraEpiccashWalletInfo(
receivingIndex: 0,
changeIndex: 0,
slatesToAddresses: {},
slatesToCommits: {},
lastScannedBlock: info.restoreHeight,
restoreHeight: info.restoreHeight,
creationHeight: info.epicData?.creationHeight ?? info.restoreHeight,
);
await info.updateExtraEpiccashWalletInfo(
epicData: epicData,
isar: mainDB.isar,
);
//Open Wallet
final walletOpen = await epiccash.LibEpiccash.openWallet(
config: stringConfig,
password: password,
);
await secureStorageInterface.write(
key: '${walletId}_wallet',
value: walletOpen,
);
await _generateAndStoreReceivingAddressForIndex(
epicData.receivingIndex);
}
});
2024-01-09 20:43:58 +00:00
unawaited(refresh());
2024-01-09 16:56:05 +00:00
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from electrumx_mixin recover(): $e\n$s",
level: LogLevel.Info);
rethrow;
}
2023-09-18 21:28:31 +00:00
}
@override
2024-01-09 16:56:05 +00:00
Future<void> refresh() async {
// Awaiting this lock could be dangerous.
// Since refresh is periodic (generally)
if (refreshMutex.isLocked) {
return;
}
try {
// this acquire should be almost instant due to above check.
// Slight possibility of race but should be irrelevant
await refreshMutex.acquire();
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.syncing,
walletId,
cryptoCurrency.coin,
),
);
// if (info.epicData?.creationHeight == null) {
// await info.updateExtraEpiccashWalletInfo(epicData: inf, isar: isar)
// await epicUpdateCreationHeight(await chainHeight);
// }
// this will always be zero????
final int curAdd = await _getCurrentIndex();
await _generateAndStoreReceivingAddressForIndex(curAdd);
await _startScans();
unawaited(_startSync());
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId));
await updateChainHeight();
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId));
// if (this is MultiAddressInterface) {
// await (this as MultiAddressInterface)
// .checkReceivingAddressForTransactions();
// }
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId));
// // TODO: [prio=low] handle this differently. Extra modification of this file for coin specific functionality should be avoided.
// if (this is MultiAddressInterface) {
// await (this as MultiAddressInterface)
// .checkChangeAddressForTransactions();
// }
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.50, walletId));
final fetchFuture = updateTransactions();
// if (currentHeight != storedHeight) {
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.60, walletId));
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.70, walletId));
await fetchFuture;
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.80, walletId));
// await getAllTxsToWatch();
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.90, walletId));
await updateBalance();
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId));
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.synced,
walletId,
cryptoCurrency.coin,
),
);
if (shouldAutoSync) {
timer ??= Timer.periodic(const Duration(seconds: 150), (timer) async {
// chain height check currently broken
// if ((await chainHeight) != (await storedChainHeight)) {
// TODO: [prio=med] some kind of quick check if wallet needs to refresh to replace the old refreshIfThereIsNewData call
// if (await refreshIfThereIsNewData()) {
unawaited(refresh());
// }
// }
});
}
} catch (error, strace) {
GlobalEventBus.instance.fire(
NodeConnectionStatusChangedEvent(
NodeConnectionStatus.disconnected,
walletId,
cryptoCurrency.coin,
),
);
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.unableToSync,
walletId,
cryptoCurrency.coin,
),
);
Logging.instance.log(
"Caught exception in refreshWalletData(): $error\n$strace",
level: LogLevel.Error,
);
} finally {
refreshMutex.release();
}
2023-09-18 21:28:31 +00:00
}
@override
2024-01-09 16:56:05 +00:00
Future<void> updateBalance() async {
try {
final balances = await _allWalletBalances();
final balance = Balance(
total: Amount.fromDecimal(
Decimal.parse(balances.total.toString()) +
Decimal.parse(balances.awaitingFinalization.toString()),
fractionDigits: cryptoCurrency.fractionDigits,
),
spendable: Amount.fromDecimal(
Decimal.parse(balances.spendable.toString()),
fractionDigits: cryptoCurrency.fractionDigits,
),
blockedTotal: Amount.zeroWith(
fractionDigits: cryptoCurrency.fractionDigits,
),
pendingSpendable: Amount.fromDecimal(
Decimal.parse(balances.pending.toString()),
fractionDigits: cryptoCurrency.fractionDigits,
),
);
await info.updateBalance(
newBalance: balance,
isar: mainDB.isar,
);
} catch (e, s) {
Logging.instance.log(
"Epic cash wallet failed to update balance: $e\n$s",
level: LogLevel.Warning,
);
}
2023-09-18 21:28:31 +00:00
}
@override
2024-01-09 16:56:05 +00:00
Future<void> updateTransactions() async {
try {
final wallet =
await secureStorageInterface.read(key: '${walletId}_wallet');
const refreshFromNode = 1;
final myAddresses = await mainDB
.getAddresses(walletId)
.filter()
.typeEqualTo(AddressType.mimbleWimble)
.and()
.subTypeEqualTo(AddressSubType.receiving)
.and()
.valueIsNotEmpty()
.valueProperty()
.findAll();
final myAddressesSet = myAddresses.toSet();
final transactions = await epiccash.LibEpiccash.getTransactions(
wallet: wallet!,
refreshFromNode: refreshFromNode,
);
final List<TransactionV2> txns = [];
final slatesToCommits = await _getSlatesToCommits();
for (final tx in transactions) {
// Logging.instance.log("tx: $tx", level: LogLevel.Info);
2024-01-09 20:43:58 +00:00
final isIncoming =
tx.txType == epic_models.TransactionType.TxReceived ||
tx.txType == epic_models.TransactionType.TxReceivedCancelled;
2024-01-09 16:56:05 +00:00
final slateId = tx.txSlateId;
final commitId = slatesToCommits[slateId]?['commitId'] as String?;
final numberOfMessages = tx.messages?.messages.length;
final onChainNote = tx.messages?.messages[0].message;
final addressFrom = slatesToCommits[slateId]?["from"] as String?;
final addressTo = slatesToCommits[slateId]?["to"] as String?;
2024-01-09 20:43:58 +00:00
final credit = int.parse(tx.amountCredited);
final debit = int.parse(tx.amountDebited);
final fee = int.tryParse(tx.fee ?? "0") ?? 0;
2024-01-09 16:56:05 +00:00
// hack epic tx data into inputs and outputs
final List<OutputV2> outputs = [];
final List<InputV2> inputs = [];
final addressFromIsMine = myAddressesSet.contains(addressFrom);
final addressToIsMine = myAddressesSet.contains(addressTo);
2024-01-09 20:43:58 +00:00
OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor(
scriptPubKeyHex: "00",
valueStringSats: credit.toString(),
addresses: [
if (addressFrom != null) addressFrom,
],
walletOwns: true,
2024-01-09 16:56:05 +00:00
);
2024-01-09 20:43:58 +00:00
InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor(
scriptSigHex: null,
sequence: null,
outpoint: null,
addresses: [if (addressTo != null) addressTo],
valueStringSats: debit.toString(),
witness: null,
innerRedeemScriptAsm: null,
coinbase: null,
walletOwns: true,
2024-01-09 16:56:05 +00:00
);
final TransactionType txType;
2024-01-09 20:43:58 +00:00
if (isIncoming) {
if (addressToIsMine && addressFromIsMine) {
txType = TransactionType.sentToSelf;
} else {
txType = TransactionType.incoming;
}
output = output.copyWith(
addresses: [
myAddressesSet
.first, // Must be changed if we ever do more than a single wallet address!!!
],
walletOwns: true,
);
2024-01-09 16:56:05 +00:00
} else {
txType = TransactionType.outgoing;
}
2024-01-09 20:43:58 +00:00
outputs.add(output);
inputs.add(input);
2024-01-09 16:56:05 +00:00
final otherData = {
"isEpiccashTransaction": true,
"numberOfMessages": numberOfMessages,
"slateId": slateId,
"onChainNote": onChainNote,
"isCancelled":
tx.txType == epic_models.TransactionType.TxSentCancelled ||
tx.txType == epic_models.TransactionType.TxReceivedCancelled,
2024-01-09 20:43:58 +00:00
"anonFees": Amount(
rawValue: BigInt.from(fee),
fractionDigits: cryptoCurrency.fractionDigits,
).toJsonString(),
2024-01-09 16:56:05 +00:00
};
final txn = TransactionV2(
walletId: walletId,
blockHash: null,
hash: commitId ?? tx.id.toString(),
txid: commitId ?? tx.id.toString(),
timestamp:
DateTime.parse(tx.creationTs).millisecondsSinceEpoch ~/ 1000,
height: tx.confirmed ? tx.kernelLookupMinHeight ?? 1 : null,
inputs: List.unmodifiable(inputs),
outputs: List.unmodifiable(outputs),
version: 0,
type: txType,
subType: TransactionSubType.none,
otherData: jsonEncode(otherData),
);
txns.add(txn);
}
await mainDB.updateOrPutTransactionV2s(txns);
} catch (e, s) {
Logging.instance.log(
"${cryptoCurrency.runtimeType} ${cryptoCurrency.network} net wallet"
" \"${info.name}\"_${info.walletId} updateTransactions() failed: $e\n$s",
level: LogLevel.Warning,
);
}
}
@override
Future<bool> updateUTXOs() async {
// not used for epiccash
return false;
2023-09-18 21:28:31 +00:00
}
@override
2024-01-09 16:56:05 +00:00
Future<void> updateNode() async {
_epicNode = getCurrentNode();
// TODO: [prio=low] move this out of secure storage if secure storage not needed
final String stringConfig = await _getConfig();
await secureStorageInterface.write(
key: '${walletId}_config',
value: stringConfig,
);
await refresh();
}
@override
Future<bool> pingCheck() async {
try {
final node = nodeService.getPrimaryNodeFor(coin: cryptoCurrency.coin);
// 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 = node!.host
..useSSL = node.useSSL
..port = node.port,
) !=
null;
} catch (e, s) {
Logging.instance.log("$e\n$s", level: LogLevel.Info);
return false;
}
}
@override
Future<void> updateChainHeight() async {
2024-01-09 16:56:05 +00:00
final config = await _getRealConfig();
final latestHeight =
await epiccash.LibEpiccash.getChainHeight(config: config);
await info.updateCachedChainHeight(
newHeight: latestHeight,
isar: mainDB.isar,
);
}
@override
2024-01-09 16:56:05 +00:00
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
// setting ifErrorEstimateFee doesn't do anything as its not used in the nativeFee function?????
int currentFee = await _nativeFee(
amount.raw.toInt(),
ifErrorEstimateFee: true,
);
return Amount(
rawValue: BigInt.from(currentFee),
fractionDigits: cryptoCurrency.fractionDigits,
);
}
@override
2024-01-09 16:56:05 +00:00
Future<FeeObject> get fees async {
// this wasn't done before the refactor either so...
// TODO: implement _getFees
return FeeObject(
numberOfBlocksFast: 10,
numberOfBlocksAverage: 10,
numberOfBlocksSlow: 10,
fast: 1,
medium: 1,
slow: 1);
}
@override
Future<TxData> updateSentCachedTxData({required TxData txData}) async {
// TODO: [prio=low] Was not used before refactor so maybe not required(?)
return txData;
}
@override
Future<void> exit() async {
timer?.cancel();
timer = null;
await super.exit();
Logging.instance.log("EpicCash_wallet exit finished", level: LogLevel.Info);
}
}
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...";
}
}
2023-09-18 21:28:31 +00:00
}