stack_wallet/lib/services/coins/ethereum/ethereum_wallet.dart

1022 lines
32 KiB
Dart
Raw Normal View History

import 'dart:async';
2023-01-03 15:15:27 +00:00
import 'dart:convert';
2022-12-13 17:39:19 +00:00
import 'dart:math';
import 'package:bip39/bip39.dart' as bip39;
import 'package:decimal/decimal.dart';
import 'package:devicelocale/devicelocale.dart';
import 'package:ethereum_addresses/ethereum_addresses.dart';
2023-01-11 13:35:51 +00:00
import 'package:stackwallet/models/node_model.dart';
2022-12-13 17:39:19 +00:00
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/price.dart';
2023-01-08 15:19:58 +00:00
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/constants.dart';
2022-12-13 17:39:19 +00:00
import 'package:stackwallet/utilities/enums/coin_enum.dart';
2023-01-12 14:14:49 +00:00
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
2022-12-13 17:39:19 +00:00
import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart';
2022-12-26 14:12:51 +00:00
import 'package:stackwallet/utilities/format.dart';
2022-12-14 10:15:22 +00:00
import 'package:stackwallet/utilities/prefs.dart';
2022-12-13 17:39:19 +00:00
import 'package:string_to_hex/string_to_hex.dart';
import 'package:web3dart/web3dart.dart';
2022-12-26 14:12:51 +00:00
import 'package:web3dart/web3dart.dart' as web3;
2022-12-14 16:09:24 +00:00
import 'package:web3dart/web3dart.dart' as Transaction;
import 'package:stackwallet/models/models.dart' as models;
2022-12-20 12:54:51 +00:00
2022-12-13 17:39:19 +00:00
import 'package:http/http.dart';
import 'package:stackwallet/hive/db.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
2022-12-20 12:54:51 +00:00
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';
2023-01-08 15:19:58 +00:00
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/services/notifications_api.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
2023-01-11 13:35:51 +00:00
import 'package:stackwallet/services/node_service.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
const int MINIMUM_CONFIRMATIONS = 5;
2022-12-13 17:39:19 +00:00
const String GENESIS_HASH_MAINNET =
2022-12-14 10:15:22 +00:00
"0x11bbe8db4e347b4e8c937c1c8370e4b5ed33adb3db69cbdb7a38e1e50b1b82fa";
2022-12-13 17:39:19 +00:00
2023-01-09 17:15:40 +00:00
//THis is used for mapping transactions per address from the block explorer
2023-01-05 16:04:45 +00:00
class AddressTransaction {
final String message;
final List<dynamic> result;
2023-01-06 15:25:28 +00:00
final String status;
2023-01-05 16:04:45 +00:00
const AddressTransaction({
required this.message,
required this.result,
required this.status,
});
factory AddressTransaction.fromJson(Map<String, dynamic> json) {
return AddressTransaction(
message: json['message'] as String,
result: json['result'] as List<dynamic>,
2023-01-06 15:25:28 +00:00
status: json['status'] as String,
2023-01-05 16:04:45 +00:00
);
}
}
2023-01-09 17:15:40 +00:00
class GasTracker {
2023-01-12 08:09:11 +00:00
final int code;
2023-01-12 14:14:49 +00:00
final Map<String, dynamic> data;
2023-01-09 17:15:40 +00:00
const GasTracker({
2023-01-12 08:09:11 +00:00
required this.code,
required this.data,
2023-01-09 17:15:40 +00:00
});
factory GasTracker.fromJson(Map<String, dynamic> json) {
return GasTracker(
2023-01-12 08:09:11 +00:00
code: json['code'] as int,
2023-01-12 14:14:49 +00:00
data: json['data'] as Map<String, dynamic>,
2023-01-09 17:15:40 +00:00
);
}
}
2022-12-13 17:39:19 +00:00
class EthereumWallet extends CoinServiceAPI {
2023-01-11 13:35:51 +00:00
NodeModel? _ethNode;
2023-01-12 14:14:49 +00:00
final _gasLimit = 21000;
2023-01-13 14:36:50 +00:00
final _blockExplorer = "https://eth-goerli.blockscout.com/api?";
final _gasTrackerUrl = "https://beaconcha.in/api/v1/execution/gasnow";
2023-01-11 13:35:51 +00:00
2022-12-13 17:39:19 +00:00
@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 (returning false by default): $e\n$s",
level: LogLevel.Error);
return false;
}
}
@override
Coin get coin => _coin;
late SecureStorageInterface _secureStore;
2023-01-08 15:19:58 +00:00
late final TransactionNotificationTracker txTracker;
2022-12-13 17:39:19 +00:00
late PriceAPI _priceAPI;
2022-12-14 10:15:22 +00:00
final _prefs = Prefs.instance;
2023-01-08 15:19:58 +00:00
bool longMutex = false;
2023-01-13 14:36:50 +00:00
Future<NodeModel> getCurrentNode() async {
return NodeService(secureStorageInterface: _secureStore)
.getPrimaryNodeFor(coin: coin) ??
DefaultNodes.getNodeFor(coin);
}
2023-01-05 16:04:45 +00:00
2023-01-13 14:36:50 +00:00
Future<Web3Client> getEthClient() async {
final node = await getCurrentNode();
return Web3Client(node.host, Client());
}
2022-12-13 17:39:19 +00:00
2022-12-14 16:09:24 +00:00
late EthPrivateKey _credentials;
2023-01-08 15:19:58 +00:00
EthereumWallet({
required String walletId,
required String walletName,
required Coin coin,
PriceAPI? priceAPI,
required SecureStorageInterface secureStore,
required TransactionNotificationTracker tracker,
}) {
txTracker = tracker;
2022-12-13 17:39:19 +00:00
_walletId = walletId;
_walletName = walletName;
_coin = coin;
_priceAPI = priceAPI ?? PriceAPI(Client());
_secureStore = secureStore;
}
2023-01-08 15:19:58 +00:00
bool _shouldAutoSync = false;
@override
bool get shouldAutoSync => _shouldAutoSync;
2022-12-13 17:39:19 +00:00
@override
2023-01-08 15:19:58 +00:00
set shouldAutoSync(bool shouldAutoSync) {
if (_shouldAutoSync != shouldAutoSync) {
_shouldAutoSync = shouldAutoSync;
if (!shouldAutoSync) {
timer?.cancel();
timer = null;
stopNetworkAlivePinging();
} else {
startNetworkAlivePinging();
refresh();
}
}
}
2022-12-13 17:39:19 +00:00
@override
String get walletName => _walletName;
late String _walletName;
late Coin _coin;
2023-01-08 15:19:58 +00:00
Timer? timer;
Timer? _networkAliveTimer;
2022-12-13 17:39:19 +00:00
@override
2022-12-14 16:09:24 +00:00
Future<List<String>> get allOwnAddresses =>
_allOwnAddresses ??= _fetchAllOwnAddresses();
Future<List<String>>? _allOwnAddresses;
Future<List<String>> _fetchAllOwnAddresses() async {
List<String> addresses = [];
final ownAddress = _credentials.address;
addresses.add(ownAddress.toString());
return addresses;
}
2022-12-13 17:39:19 +00:00
@override
2022-12-14 16:09:24 +00:00
Future<Decimal> get availableBalance async {
2023-01-13 14:36:50 +00:00
Web3Client client = await getEthClient();
EtherAmount ethBalance = await client.getBalance(_credentials.address);
2022-12-14 16:09:24 +00:00
return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString());
}
2022-12-13 17:39:19 +00:00
@override
Future<Decimal> get balanceMinusMaxFee async =>
(await availableBalance) -
(Decimal.fromInt((await maxFee)) /
Decimal.fromInt(Constants.satsPerCoin(coin)))
.toDecimal();
2022-12-13 17:39:19 +00:00
@override
2022-12-14 16:09:24 +00:00
Future<String> confirmSend({required Map<String, dynamic> txData}) async {
2023-01-13 14:36:50 +00:00
Web3Client client = await getEthClient();
final int chainId = await client.getNetworkId();
2022-12-26 14:12:51 +00:00
final amount = txData['recipientAmt'];
final decimalAmount =
Format.satoshisToAmount(amount as int, coin: Coin.ethereum);
final bigIntAmount = amountToBigInt(decimalAmount.toDouble());
2022-12-20 12:54:51 +00:00
final tx = Transaction.Transaction(
2023-01-11 13:35:51 +00:00
to: EthereumAddress.fromHex(txData['address'] as String),
gasPrice:
EtherAmount.fromUnitAndValue(EtherUnit.wei, txData['feeInWei']),
2023-01-12 14:14:49 +00:00
maxGas: _gasLimit,
2022-12-26 14:12:51 +00:00
value: EtherAmount.inWei(bigIntAmount));
final transaction =
2023-01-13 14:36:50 +00:00
await client.sendTransaction(_credentials, tx, chainId: chainId);
2022-12-14 16:09:24 +00:00
return transaction;
2022-12-13 17:39:19 +00:00
}
2022-12-26 14:12:51 +00:00
BigInt amountToBigInt(num amount) {
const decimal = 18; //Eth has up to 18 decimal places
final amountToSendinDecimal = amount * (pow(10, decimal));
return BigInt.from(amountToSendinDecimal);
}
2022-12-13 17:39:19 +00:00
@override
2022-12-14 16:09:24 +00:00
Future<String> get currentReceivingAddress async {
final _currentReceivingAddress = _credentials.address;
final checkSumAddress =
checksumEthereumAddress(_currentReceivingAddress.toString());
return checkSumAddress;
2022-12-14 16:09:24 +00:00
}
2022-12-13 17:39:19 +00:00
@override
2022-12-20 12:54:51 +00:00
Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async {
2023-01-12 14:14:49 +00:00
final gweiAmount = feeRate / (pow(10, 9));
final fee = _gasLimit * gweiAmount;
//Convert gwei to ETH
final feeInWei = fee * (pow(10, 9));
final ethAmount = feeInWei / (pow(10, 18));
return Format.decimalAmountToSatoshis(
Decimal.parse(ethAmount.toString()), coin);
2022-12-13 17:39:19 +00:00
}
@override
2023-01-11 13:35:51 +00:00
Future<void> exit() async {
_hasCalledExit = true;
timer?.cancel();
timer = null;
stopNetworkAlivePinging();
2022-12-13 17:39:19 +00:00
}
@override
2022-12-14 16:09:24 +00:00
Future<FeeObject> get fees => _feeObject ??= _getFees();
Future<FeeObject>? _feeObject;
Future<FeeObject> _getFees() async {
2023-01-12 08:09:11 +00:00
GasTracker fees = await getGasOracle();
2023-01-12 14:14:49 +00:00
final feesMap = fees.data;
2022-12-14 16:09:24 +00:00
return FeeObject(
numberOfBlocksFast: 1,
2023-01-12 14:14:49 +00:00
numberOfBlocksAverage: 3,
numberOfBlocksSlow: 3,
2023-01-12 14:14:49 +00:00
fast: feesMap['fast'] as int,
medium: feesMap['standard'] as int,
slow: feesMap['slow'] as int);
2022-12-14 16:09:24 +00:00
}
2022-12-13 17:39:19 +00:00
2023-01-12 08:09:11 +00:00
Future<GasTracker> getGasOracle() async {
2023-01-13 14:36:50 +00:00
final response = await get(Uri.parse(_gasTrackerUrl));
2023-01-12 08:09:11 +00:00
if (response.statusCode == 200) {
return GasTracker.fromJson(
json.decode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to load gas oracle');
}
}
//Full rescan is not needed for ETH since we have a balance
2022-12-13 17:39:19 +00:00
@override
Future<void> fullRescan(
int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) {
// TODO: implement fullRescan
throw UnimplementedError();
}
@override
Future<bool> generateNewAddress() {
2023-01-11 13:35:51 +00:00
// TODO: implement generateNewAddress - might not be needed for ETH
2022-12-13 17:39:19 +00:00
throw UnimplementedError();
}
2022-12-20 12:54:51 +00:00
bool _hasCalledExit = false;
2022-12-13 17:39:19 +00:00
@override
2022-12-20 12:54:51 +00:00
bool get hasCalledExit => _hasCalledExit;
2022-12-13 17:39:19 +00:00
@override
2022-12-14 10:15:22 +00:00
Future<void> initializeExisting() async {
//First get mnemonic so we can initialize credentials
final mnemonicString =
await _secureStore.read(key: '${_walletId}_mnemonic');
_credentials =
EthPrivateKey.fromHex(StringToHex.toHexString(mnemonicString));
2022-12-14 10:15:22 +00:00
Logging.instance.log("Opening existing ${coin.prettyName} wallet.",
level: LogLevel.Info);
if ((DB.instance.get<dynamic>(boxName: walletId, key: "id")) == null) {
throw Exception(
"Attempted to initialize an existing wallet using an unknown wallet ID!");
}
await _prefs.init();
final data =
DB.instance.get<dynamic>(boxName: walletId, key: "latest_tx_model")
as TransactionData?;
if (data != null) {
_transactionData = Future(() => data);
}
2022-12-13 17:39:19 +00:00
}
@override
2022-12-14 10:15:22 +00:00
Future<void> initializeNew() async {
await _prefs.init();
final String mnemonic = bip39.generateMnemonic(strength: 256);
2022-12-14 16:09:24 +00:00
_credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic));
2022-12-14 10:15:22 +00:00
await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic);
//Store credentials in secure store
await _secureStore.write(
key: '${_walletId}_credentials', value: _credentials.toString());
2022-12-14 10:15:22 +00:00
await DB.instance
.put<dynamic>(boxName: walletId, key: "id", value: _walletId);
await DB.instance.put<dynamic>(
boxName: walletId, key: 'receivingAddresses', value: ["0"]);
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);
2022-12-13 17:39:19 +00:00
}
2022-12-14 16:09:24 +00:00
bool _isConnected = false;
2022-12-13 17:39:19 +00:00
@override
2022-12-14 16:09:24 +00:00
bool get isConnected => _isConnected;
2022-12-13 17:39:19 +00:00
@override
2022-12-14 16:09:24 +00:00
bool get isRefreshing => refreshMutex;
2022-12-13 17:39:19 +00:00
2023-01-11 13:35:51 +00:00
bool refreshMutex = false;
2022-12-13 17:39:19 +00:00
@override
Future<int> get maxFee async {
final fee = (await fees).fast;
final feeEstimate = await estimateFeeFor(0, fee);
return feeEstimate;
}
2022-12-13 17:39:19 +00:00
@override
2022-12-14 10:15:22 +00:00
Future<List<String>> get mnemonic => _getMnemonicList();
Future<int> get chainHeight async {
2023-01-13 14:36:50 +00:00
Web3Client client = await getEthClient();
try {
2023-01-13 14:36:50 +00:00
final result = await client.getBlockNumber();
return result;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
level: LogLevel.Error);
return -1;
}
}
int get storedChainHeight {
final storedHeight = DB.instance
.get<dynamic>(boxName: walletId, key: "storedChainHeight") as int?;
return storedHeight ?? 0;
}
Future<void> updateStoredChainHeight({required int newHeight}) async {
await DB.instance.put<dynamic>(
boxName: walletId, key: "storedChainHeight", value: newHeight);
}
2022-12-20 12:54:51 +00:00
2022-12-14 10:15:22 +00:00
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;
}
2022-12-13 17:39:19 +00:00
@override
// TODO: implement pendingBalance - Not needed since we don't use UTXOs to get a balance
2022-12-13 17:39:19 +00:00
Future<Decimal> get pendingBalance => throw UnimplementedError();
@override
Future<Map<String, dynamic>> prepareSend(
{required String address,
required int satoshiAmount,
2022-12-20 12:54:51 +00:00
Map<String, dynamic>? args}) async {
2023-01-12 14:14:49 +00:00
final feeRateType = args?["feeRate"];
int fee = 0;
final feeObject = await fees;
switch (feeRateType) {
case FeeRateType.fast:
fee = feeObject.fast;
break;
case FeeRateType.average:
fee = feeObject.medium;
break;
case FeeRateType.slow:
fee = feeObject.slow;
break;
}
final feeEstimate = await estimateFeeFor(satoshiAmount, fee);
2023-01-13 09:21:10 +00:00
print("FEE ESTIMATE IS $feeEstimate");
print("AMOUNT TO SEND IS $satoshiAmount");
bool isSendAll = false;
final balance =
Format.decimalAmountToSatoshis(await availableBalance, coin);
if (satoshiAmount == balance) {
isSendAll = true;
}
if (isSendAll) {
//Subtract fee amount from send amount
satoshiAmount -= feeEstimate;
}
2022-12-20 12:54:51 +00:00
Map<String, dynamic> txData = {
2023-01-12 14:14:49 +00:00
"fee": feeEstimate,
"feeInWei": fee,
2023-01-11 13:35:51 +00:00
"address": address,
2022-12-20 12:54:51 +00:00
"recipientAmt": satoshiAmount,
};
return txData;
2022-12-13 17:39:19 +00:00
}
@override
Future<void> recoverFromMnemonic(
{required String mnemonic,
required int maxUnusedAddressGap,
required int maxNumberOfIndexesToCheck,
2022-12-14 16:09:24 +00:00
required int height}) async {
2023-01-08 15:19:58 +00:00
longMutex = true;
final start = DateTime.now();
2022-12-14 16:09:24 +00:00
2023-01-08 15:19:58 +00:00
try {
if ((await _secureStore.read(key: '${_walletId}_mnemonic')) != null) {
longMutex = false;
throw Exception("Attempted to overwrite mnemonic on restore!");
}
2022-12-14 16:09:24 +00:00
2023-01-08 15:19:58 +00:00
await _secureStore.write(
key: '${_walletId}_mnemonic', value: mnemonic.trim());
_credentials = EthPrivateKey.fromHex(StringToHex.toHexString(mnemonic));
2023-01-09 11:10:34 +00:00
await DB.instance
.put<dynamic>(boxName: walletId, key: "id", value: _walletId);
await DB.instance
.put<dynamic>(boxName: walletId, key: "isFavorite", value: false);
2023-01-08 15:19:58 +00:00
} 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);
}
Future<bool> refreshIfThereIsNewData() async {
2023-01-13 14:36:50 +00:00
Web3Client client = await getEthClient();
2023-01-08 15:19:58 +00:00
if (longMutex) return false;
if (_hasCalledExit) return false;
2023-01-11 13:35:51 +00:00
final currentChainHeight = await chainHeight;
2023-01-08 15:19:58 +00:00
try {
bool needsRefresh = false;
Set<String> txnsToCheck = {};
for (final String txid in txTracker.pendings) {
if (!txTracker.wasNotifiedConfirmed(txid)) {
txnsToCheck.add(txid);
}
}
2023-01-09 11:10:34 +00:00
for (String txid in txnsToCheck) {
2023-01-13 14:36:50 +00:00
final txn = await client.getTransactionByHash(txid);
2023-01-11 13:35:51 +00:00
final int txBlockNumber = txn.blockNumber.blockNum;
final int txConfirmations = currentChainHeight - txBlockNumber;
bool isUnconfirmed = txConfirmations < MINIMUM_CONFIRMATIONS;
if (!isUnconfirmed) {
needsRefresh = true;
break;
}
2023-01-09 11:10:34 +00:00
}
2023-01-13 14:36:50 +00:00
if (!needsRefresh) {
var allOwnAddresses = await _fetchAllOwnAddresses();
AddressTransaction addressTransactions =
await fetchAddressTransactions(allOwnAddresses.elementAt(0));
final txData = await transactionData;
if (addressTransactions.message == "OK") {
final allTxs = addressTransactions.result;
allTxs.forEach((element) {
if (txData.findTransaction(element["hash"] as String) == null) {
Logging.instance.log(
" txid not found in address history already ${element["hash"]}",
level: LogLevel.Info);
needsRefresh = true;
}
});
}
}
2023-01-08 15:19:58 +00:00
return needsRefresh;
} catch (e, s) {
Logging.instance.log(
"Exception caught in refreshIfThereIsNewData: $e\n$s",
level: LogLevel.Error);
rethrow;
}
}
Future<void> getAllTxsToWatch(
TransactionData txData,
) async {
if (_hasCalledExit) return;
List<models.Transaction> unconfirmedTxnsToNotifyPending = [];
List<models.Transaction> unconfirmedTxnsToNotifyConfirmed = [];
for (final chunk in txData.txChunks) {
for (final tx in chunk.transactions) {
if (tx.confirmedStatus) {
// get all transactions that were notified as pending but not as confirmed
if (txTracker.wasNotifiedPending(tx.txid) &&
!txTracker.wasNotifiedConfirmed(tx.txid)) {
unconfirmedTxnsToNotifyConfirmed.add(tx);
}
} else {
// get all transactions that were not notified as pending yet
if (!txTracker.wasNotifiedPending(tx.txid)) {
unconfirmedTxnsToNotifyPending.add(tx);
}
}
}
}
// notify on unconfirmed transactions
for (final tx in unconfirmedTxnsToNotifyPending) {
if (tx.txType == "Received") {
unawaited(NotificationApi.showNotification(
title: "Incoming transaction",
body: walletName,
walletId: walletId,
iconAssetName: Assets.svg.iconFor(coin: coin),
date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS,
coinName: coin.name,
txid: tx.txid,
confirmations: tx.confirmations,
requiredConfirmations: MINIMUM_CONFIRMATIONS,
));
await txTracker.addNotifiedPending(tx.txid);
} else if (tx.txType == "Sent") {
unawaited(NotificationApi.showNotification(
title: "Sending transaction",
body: walletName,
walletId: walletId,
iconAssetName: Assets.svg.iconFor(coin: coin),
date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
shouldWatchForUpdates: tx.confirmations < MINIMUM_CONFIRMATIONS,
coinName: coin.name,
txid: tx.txid,
confirmations: tx.confirmations,
requiredConfirmations: MINIMUM_CONFIRMATIONS,
));
await txTracker.addNotifiedPending(tx.txid);
}
}
// notify on confirmed
for (final tx in unconfirmedTxnsToNotifyConfirmed) {
if (tx.txType == "Received") {
unawaited(NotificationApi.showNotification(
title: "Incoming transaction confirmed",
body: walletName,
walletId: walletId,
iconAssetName: Assets.svg.iconFor(coin: coin),
date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
shouldWatchForUpdates: false,
coinName: coin.name,
));
await txTracker.addNotifiedConfirmed(tx.txid);
} else if (tx.txType == "Sent") {
unawaited(NotificationApi.showNotification(
title: "Outgoing transaction confirmed",
body: walletName,
walletId: walletId,
iconAssetName: Assets.svg.iconFor(coin: coin),
date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
shouldWatchForUpdates: false,
coinName: coin.name,
));
await txTracker.addNotifiedConfirmed(tx.txid);
}
}
2022-12-13 17:39:19 +00:00
}
@override
2022-12-20 12:54:51 +00:00
Future<void> refresh() async {
if (refreshMutex) {
Logging.instance.log("$walletId $walletName refreshMutex denied",
level: LogLevel.Info);
return;
} else {
refreshMutex = true;
}
2022-12-20 12:54:51 +00:00
try {
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.syncing,
walletId,
coin,
),
);
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId));
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId));
final currentHeight = await chainHeight;
2022-12-20 12:54:51 +00:00
const storedHeight = 1; //await storedChainHeight;
Logging.instance
.log("chain height: $currentHeight", level: LogLevel.Info);
Logging.instance
.log("cached height: $storedHeight", level: LogLevel.Info);
if (currentHeight != storedHeight) {
if (currentHeight != -1) {
// -1 failed to fetch current height
unawaited(updateStoredChainHeight(newHeight: currentHeight));
}
2023-01-08 15:19:58 +00:00
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId));
final newTxData = _fetchTransactionData();
GlobalEventBus.instance
.fire(RefreshPercentChangedEvent(0.50, walletId));
2023-01-08 15:19:58 +00:00
final feeObj = _getFees();
GlobalEventBus.instance
.fire(RefreshPercentChangedEvent(0.60, walletId));
_transactionData = Future(() => newTxData);
GlobalEventBus.instance
.fire(RefreshPercentChangedEvent(0.70, walletId));
_feeObject = Future(() => feeObj);
GlobalEventBus.instance
.fire(RefreshPercentChangedEvent(0.80, walletId));
final allTxsToWatch = getAllTxsToWatch(await newTxData);
await Future.wait([
newTxData,
feeObj,
allTxsToWatch,
]);
GlobalEventBus.instance
.fire(RefreshPercentChangedEvent(0.90, walletId));
}
refreshMutex = false;
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId));
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.synced,
walletId,
coin,
),
);
2023-01-13 14:36:50 +00:00
if (shouldAutoSync) {
timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async {
Logging.instance.log(
"Periodic refresh check for $walletId $walletName in object instance: $hashCode",
level: LogLevel.Info);
if (await refreshIfThereIsNewData()) {
await refresh();
GlobalEventBus.instance.fire(UpdatedInBackgroundEvent(
"New data found in $walletId $walletName in background!",
walletId));
}
});
}
2022-12-20 12:54:51 +00:00
} 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);
}
2022-12-13 17:39:19 +00:00
}
@override
Future<String> send(
{required String toAddress,
required int amount,
Map<String, String> args = const {}}) {
// TODO: implement send
throw UnimplementedError();
}
@override
2023-01-08 15:19:58 +00:00
Future<bool> testNetworkConnection() async {
2023-01-13 14:36:50 +00:00
Web3Client client = await getEthClient();
2023-01-08 15:19:58 +00:00
try {
2023-01-13 14:36:50 +00:00
final result = await client.isListeningForNetwork();
2023-01-09 17:15:40 +00:00
return result;
2023-01-08 15:19:58 +00:00
} catch (_) {
return false;
}
}
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));
}
2022-12-13 17:39:19 +00:00
}
@override
2022-12-14 16:09:24 +00:00
Future<Decimal> get totalBalance async {
2023-01-13 14:36:50 +00:00
Web3Client client = await getEthClient();
EtherAmount ethBalance = await client.getBalance(_credentials.address);
2022-12-14 16:09:24 +00:00
return Decimal.parse(ethBalance.getValueInUnit(EtherUnit.ether).toString());
}
2022-12-13 17:39:19 +00:00
@override
2022-12-14 10:15:22 +00:00
Future<TransactionData> get transactionData =>
_transactionData ??= _fetchTransactionData();
Future<TransactionData>? _transactionData;
TransactionData? cachedTxData;
2022-12-13 17:39:19 +00:00
@override
// TODO: implement unspentOutputs - NOT NEEDED, ETH DOES NOT USE UTXOs
2022-12-13 17:39:19 +00:00
Future<List<UtxoObject>> get unspentOutputs => throw UnimplementedError();
@override
2023-01-11 13:35:51 +00:00
Future<void> updateNode(bool shouldRefresh) async {
_ethNode = NodeService(secureStorageInterface: _secureStore)
.getPrimaryNodeFor(coin: coin) ??
DefaultNodes.getNodeFor(coin);
if (shouldRefresh) {
unawaited(refresh());
}
2022-12-13 17:39:19 +00:00
}
@override
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
final priceData =
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
final locale = await Devicelocale.currentLocale;
final String worthNow = Format.localizedStringAsFixed(
value:
((currentPrice * Decimal.fromInt(txData["recipientAmt"] as int)) /
Decimal.fromInt(Constants.satsPerCoin(coin)))
.toDecimal(scaleOnInfinitePrecision: 2),
decimalPlaces: 2,
locale: locale!);
final tx = models.Transaction(
txid: txData["txid"] as String,
confirmedStatus: false,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
txType: "Sent",
amount: txData["recipientAmt"] as int,
worthNow: worthNow,
worthAtBlockTimestamp: worthNow,
fees: txData["fee"] as int,
inputSize: 0,
outputSize: 0,
inputs: [],
outputs: [],
address: txData["address"] as String,
height: -1,
confirmations: 0,
);
if (cachedTxData == null) {
final data = await _fetchTransactionData();
_transactionData = Future(() => data);
} else {
final transactions = cachedTxData!.getAllTransactions();
transactions[tx.txid] = tx;
cachedTxData = models.TransactionData.fromMap(transactions);
_transactionData = Future(() => cachedTxData!);
}
2022-12-13 17:39:19 +00:00
}
@override
bool validateAddress(String address) {
return isValidEthereumAddress(address);
2022-12-13 17:39:19 +00:00
}
2023-01-06 15:25:28 +00:00
Future<AddressTransaction> fetchAddressTransactions(String address) async {
final response = await get(Uri.parse(
"${_blockExplorer}module=account&action=txlist&address=$address"));
if (response.statusCode == 200) {
return AddressTransaction.fromJson(
json.decode(response.body) as Map<String, dynamic>);
} else {
throw Exception('Failed to load transactions');
}
2023-01-05 16:04:45 +00:00
}
2022-12-14 10:15:22 +00:00
Future<TransactionData> _fetchTransactionData() async {
String thisAddress = await currentReceivingAddress;
2023-01-06 15:25:28 +00:00
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;
2023-01-06 15:25:28 +00:00
final priceData =
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
final List<Map<String, dynamic>> midSortedArray = [];
AddressTransaction txs = await fetchAddressTransactions(thisAddress);
2023-01-13 09:21:10 +00:00
2023-01-06 15:25:28 +00:00
if (txs.message == "OK") {
final allTxs = txs.result;
allTxs.forEach((element) {
Map<String, dynamic> midSortedTx = {};
// create final tx map
midSortedTx["txid"] = element["hash"];
int confirmations = int.parse(element['confirmations'].toString());
int transactionAmount = int.parse(element['value'].toString());
const decimal = 18; //Eth has up to 18 decimal places
final transactionAmountInDecimal =
transactionAmount / (pow(10, decimal));
//Convert to satoshi, default display for other coins
final satAmount = Format.decimalAmountToSatoshis(
Decimal.parse(transactionAmountInDecimal.toString()), coin);
midSortedTx["confirmed_status"] =
(confirmations != 0) && (confirmations >= MINIMUM_CONFIRMATIONS);
midSortedTx["confirmations"] = confirmations;
midSortedTx["timestamp"] = element["timeStamp"];
if (checksumEthereumAddress(element["from"].toString()) ==
thisAddress) {
midSortedTx["txType"] = "Sent";
} else {
midSortedTx["txType"] = "Received";
}
2023-01-05 11:07:46 +00:00
2023-01-06 15:25:28 +00:00
midSortedTx["amount"] = satAmount;
final String worthNow = ((currentPrice * Decimal.fromInt(satAmount)) /
Decimal.fromInt(Constants.satsPerCoin(coin)))
.toDecimal(scaleOnInfinitePrecision: 2)
.toStringAsFixed(2);
//Calculate fees (GasLimit * gasPrice)
int txFee = int.parse(element['gasPrice'].toString()) *
int.parse(element['gasUsed'].toString());
final txFeeDecimal = txFee / (pow(10, decimal));
midSortedTx["worthNow"] = worthNow;
midSortedTx["worthAtBlockTimestamp"] = worthNow;
midSortedTx["aliens"] = <dynamic>[];
midSortedTx["fees"] = Format.decimalAmountToSatoshis(
Decimal.parse(txFeeDecimal.toString()), coin);
midSortedTx["address"] = element["to"];
midSortedTx["inputSize"] = 1;
midSortedTx["outputSize"] = 1;
midSortedTx["inputs"] = <dynamic>[];
midSortedTx["outputs"] = <dynamic>[];
midSortedTx["height"] = int.parse(element['blockNumber'].toString());
midSortedArray.add(midSortedTx);
});
}
2023-01-06 15:25:28 +00:00
midSortedArray.sort((a, b) =>
(int.parse(b['timestamp'].toString())) -
(int.parse(a['timestamp'].toString())));
// 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(int.parse(txObject['timestamp'].toString()));
final txTimeArray = [txObject["timestamp"], date];
if (dateArray.contains(txTimeArray[1])) {
result["dateTimeChunks"].forEach((dynamic chunk) {
if (extractDateFromTimestamp(
int.parse(chunk['timestamp'].toString())) ==
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);
}
2023-01-05 16:04:45 +00:00
}
2023-01-06 15:25:28 +00:00
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);
cachedTxData = txModel;
return txModel;
2022-12-14 10:15:22 +00:00
}
2022-12-13 17:39:19 +00:00
@override
String get walletId => _walletId;
late String _walletId;
@override
set walletName(String newName) => _walletName = newName;
2023-01-08 15:19:58 +00:00
void stopNetworkAlivePinging() {
_networkAliveTimer?.cancel();
_networkAliveTimer = null;
}
void startNetworkAlivePinging() {
// call once on start right away
_periodicPingCheck();
// then periodically check
_networkAliveTimer = Timer.periodic(
Constants.networkAliveTimerDuration,
(_) async {
_periodicPingCheck();
},
);
}
2022-12-13 17:39:19 +00:00
}