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

1097 lines
33 KiB
Dart
Raw Normal View History

import 'dart:async';
2023-01-25 12:09:07 +00:00
import 'package:bip39/bip39.dart' as bip39;
2022-12-13 17:39:19 +00:00
import 'package:decimal/decimal.dart';
import 'package:ethereum_addresses/ethereum_addresses.dart';
import 'package:http/http.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/hive/db.dart';
2023-03-01 21:52:13 +00:00
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.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/token_balance.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
2023-02-23 22:59:58 +00:00
import 'package:stackwallet/services/ethereum/ethereum_api.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/eth_token_cache.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/services/notifications_api.dart';
2023-01-08 15:19:58 +00:00
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/amount.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/default_nodes.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';
2023-01-20 17:24:19 +00:00
import 'package:stackwallet/utilities/eth_commons.dart';
2023-03-31 23:17:15 +00:00
import 'package:stackwallet/utilities/extensions/extensions.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-13 17:39:19 +00:00
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart';
import 'package:tuple/tuple.dart';
import 'package:web3dart/web3dart.dart' as web3;
2023-01-11 13:35:51 +00:00
2023-01-25 16:08:27 +00:00
const int MINIMUM_CONFIRMATIONS = 3;
2022-12-13 17:39:19 +00:00
class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB {
EthereumWallet({
required String walletId,
required String walletName,
required Coin coin,
required SecureStorageInterface secureStore,
required TransactionNotificationTracker tracker,
MainDB? mockableOverride,
}) {
txTracker = tracker;
_walletId = walletId;
_walletName = walletName;
_coin = coin;
_secureStore = secureStore;
initCache(walletId, coin);
initWalletDB(mockableOverride: mockableOverride);
}
2023-01-11 13:35:51 +00:00
NodeModel? _ethNode;
2023-01-12 14:14:49 +00:00
final _gasLimit = 21000;
2023-01-11 13:35:51 +00:00
Timer? timer;
Timer? _networkAliveTimer;
2023-03-23 22:28:20 +00:00
Future<void> updateTokenContracts(List<String> contractAddresses) async {
// final set = getWalletTokenContractAddresses().toSet();
// set.addAll(contractAddresses);
await updateWalletTokenContractAddresses(contractAddresses);
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
2023-03-23 18:16:28 +00:00
"$contractAddresses updated/added for: $walletId $walletName",
walletId,
),
);
}
TokenBalance getCachedTokenBalance(EthContract contract) {
final jsonString = DB.instance.get<dynamic>(
boxName: _walletId,
key: TokenCacheKeys.tokenBalance(contract.address),
) as String?;
if (jsonString == null) {
return TokenBalance(
contractAddress: contract.address,
decimalPlaces: contract.decimals,
total: 0,
spendable: 0,
blockedTotal: 0,
pendingSpendable: 0,
);
}
return TokenBalance.fromJson(
jsonString,
);
}
2023-03-23 22:28:20 +00:00
// Future<void> removeTokenContract(String contractAddress) async {
// final set = getWalletTokenContractAddresses().toSet();
// set.removeWhere((e) => e == contractAddress);
// await updateWalletTokenContractAddresses(set.toList());
//
// GlobalEventBus.instance.fire(
// UpdatedInBackgroundEvent(
// "$contractAddress removed for: $walletId $walletName",
// walletId,
// ),
// );
// }
2023-03-01 00:36:54 +00:00
2023-01-25 09:29:20 +00:00
@override
String get walletId => _walletId;
late String _walletId;
@override
String get walletName => _walletName;
2023-01-25 09:29:20 +00:00
late String _walletName;
@override
set walletName(String newName) => _walletName = newName;
2023-01-25 09:29:20 +00:00
2022-12-13 17:39:19 +00:00
@override
set isFavorite(bool markFavorite) {
_isFavorite = markFavorite;
updateCachedIsFavorite(markFavorite);
2022-12-13 17:39:19 +00:00
}
@override
bool get isFavorite => _isFavorite ??= getCachedIsFavorite();
bool? _isFavorite;
2022-12-13 17:39:19 +00:00
@override
Coin get coin => _coin;
late Coin _coin;
2022-12-13 17:39:19 +00:00
late SecureStorageInterface _secureStore;
2023-01-08 15:19:58 +00:00
late final TransactionNotificationTracker txTracker;
2022-12-14 10:15:22 +00:00
final _prefs = Prefs.instance;
2023-01-08 15:19:58 +00:00
bool longMutex = false;
NodeModel getCurrentNode() {
return _ethNode ??
NodeService(secureStorageInterface: _secureStore)
2023-01-13 14:36:50 +00:00
.getPrimaryNodeFor(coin: coin) ??
DefaultNodes.getNodeFor(coin);
}
2023-01-05 16:04:45 +00:00
web3.Web3Client getEthClient() {
final node = getCurrentNode();
return web3.Web3Client(node.host, Client());
2023-01-13 14:36:50 +00:00
}
2022-12-13 17:39:19 +00:00
late web3.EthPrivateKey _credentials;
2022-12-13 17:39:19 +00:00
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
Future<List<UTXO>> get utxos => db.getUTXOs(walletId).findAll();
2022-12-13 17:39:19 +00:00
@override
Future<List<Transaction>> get transactions => db
.getTransactions(walletId)
.filter()
2023-03-31 23:17:15 +00:00
.otherDataEqualTo(
null) // eth txns with other data where other data is the token contract address
.sortByTimestampDesc()
.findAll();
2022-12-13 17:39:19 +00:00
@override
Future<String> get currentReceivingAddress async {
final address = await _currentReceivingAddress;
2023-03-01 21:19:53 +00:00
return checksumEthereumAddress(
address?.value ?? _credentials.address.toString());
}
Future<Address?> get _currentReceivingAddress => db
.getAddresses(walletId)
.filter()
.typeEqualTo(AddressType.ethereum)
.subTypeEqualTo(AddressSubType.receiving)
.sortByDerivationIndexDesc()
.findFirst();
@override
Balance get balance => _balance ??= getCachedBalance();
Balance? _balance;
Future<void> updateBalance() async {
web3.Web3Client client = getEthClient();
web3.EtherAmount ethBalance = await client.getBalance(_credentials.address);
// TODO: check if toInt() is ok and if getBalance actually returns enough balance data
_balance = Balance(
coin: coin,
total: ethBalance.getInWei.toInt(),
spendable: ethBalance.getInWei.toInt(),
blockedTotal: 0,
pendingSpendable: 0,
);
await updateCachedBalance(_balance!);
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-03-29 18:49:12 +00:00
final fee = estimateFee(feeRate, _gasLimit, coin.decimals);
return Format.decimalAmountToSatoshis(Decimal.parse(fee.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
2023-03-29 18:49:12 +00:00
Future<FeeObject> get fees => EthereumAPI.getFees();
2023-01-12 08:09:11 +00:00
//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 {
Logging.instance.log(
"initializeExisting() ${coin.prettyName} wallet",
level: LogLevel.Info,
);
//First get mnemonic so we can initialize credentials
String privateKey =
getPrivateKey((await mnemonicString)!, (await mnemonicPassphrase)!);
_credentials = web3.EthPrivateKey.fromHex(privateKey);
2022-12-14 10:15:22 +00:00
if (getCachedId() == null) {
2022-12-14 10:15:22 +00:00
throw Exception(
"Attempted to initialize an existing wallet using an unknown wallet ID!");
}
await _prefs.init();
2022-12-13 17:39:19 +00:00
}
@override
2022-12-14 10:15:22 +00:00
Future<void> initializeNew() async {
Logging.instance.log(
"Generating new ${coin.prettyName} wallet.",
level: LogLevel.Info,
);
if (getCachedId() != null) {
throw Exception(
"Attempted to initialize a new wallet using an existing wallet ID!");
}
2022-12-14 10:15:22 +00:00
await _prefs.init();
try {
await _generateNewWallet();
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from initializeNew(): $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
await Future.wait([
updateCachedId(walletId),
updateCachedIsFavorite(false),
]);
}
Future<void> _generateNewWallet() async {
// Logging.instance
// .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info);
// if (!integrationTestFlag) {
// try {
// final features = await electrumXClient.getServerFeatures();
// Logging.instance.log("features: $features", level: LogLevel.Info);
// switch (coin) {
// case Coin.namecoin:
// if (features['genesis_hash'] != GENESIS_HASH_MAINNET) {
// throw Exception("genesis hash does not match main net!");
// }
// break;
// default:
// throw Exception(
// "Attempted to generate a EthereumWallet using a non eth coin type: ${coin.name}");
// }
// } catch (e, s) {
// Logging.instance.log("$e/n$s", level: LogLevel.Info);
// }
// }
// this should never fail - sanity check
if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) {
throw Exception(
"Attempted to overwrite mnemonic on generate new wallet!");
}
2022-12-14 10:15:22 +00:00
final String mnemonic = bip39.generateMnemonic(strength: 256);
await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic);
await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase',
value: "",
);
String privateKey = getPrivateKey(mnemonic, "");
_credentials = web3.EthPrivateKey.fromHex(privateKey);
final address = Address(
walletId: walletId, value: _credentials.address.toString(),
publicKey: [], // maybe store address bytes here? seems a waste of space though
derivationIndex: 0,
derivationPath: DerivationPath()..value = "$hdPathEthereum/0",
type: AddressType.ethereum,
subType: AddressSubType.receiving,
);
await db.putAddress(address);
Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info);
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();
@override
Future<String?> get mnemonicString =>
_secureStore.read(key: '${_walletId}_mnemonic');
@override
Future<String?> get mnemonicPassphrase => _secureStore.read(
key: '${_walletId}_mnemonicPassphrase',
);
Future<int> get chainHeight async {
web3.Web3Client client = getEthClient();
try {
final height = await client.getBlockNumber();
await updateCachedChainHeight(height);
if (height > storedChainHeight) {
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"Updated current chain height in $walletId $walletName!",
walletId,
),
);
}
return height;
} catch (e, s) {
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
level: LogLevel.Error);
return storedChainHeight;
}
}
@override
int get storedChainHeight => getCachedChainHeight();
2022-12-20 12:54:51 +00:00
2022-12-14 10:15:22 +00:00
Future<List<String>> _getMnemonicList() async {
final _mnemonicString = await mnemonicString;
if (_mnemonicString == null) {
2022-12-14 10:15:22 +00:00
return [];
}
final List<String> data = _mnemonicString.split(' ');
2022-12-14 10:15:22 +00:00
return data;
}
2022-12-13 17:39:19 +00:00
@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
2023-03-31 23:17:15 +00:00
// bool isSendAll = false;
// final availableBalance = balance.spendable;
// if (satoshiAmount == availableBalance) {
// isSendAll = true;
// }
//
// if (isSendAll) {
// //Subtract fee amount from send amount
// satoshiAmount -= feeEstimate;
// }
2023-01-13 09:21:10 +00:00
2023-03-31 23:17:15 +00:00
final decimalAmount = Format.satoshisToAmount(satoshiAmount, coin: coin);
final bigIntAmount = amountToBigInt(
decimalAmount.toDouble(),
Constants.decimalPlacesForCoin(coin),
);
final client = getEthClient();
final myAddress = await currentReceivingAddress;
final myWeb3Address = web3.EthereumAddress.fromHex(myAddress);
final est = await client.estimateGas(
sender: myWeb3Address,
to: web3.EthereumAddress.fromHex(address),
gasPrice: web3.EtherAmount.fromUnitAndValue(
web3.EtherUnit.wei,
fee,
),
amountOfGas: BigInt.from(_gasLimit),
value: web3.EtherAmount.inWei(bigIntAmount),
);
final nonce = args?["nonce"] as int? ??
await client.getTransactionCount(myWeb3Address,
atBlock: const web3.BlockNum.pending());
final nResponse = await EthereumAPI.getAddressNonce(address: myAddress);
print("==============================================================");
print("ETH client.estimateGas: $est");
print("ETH estimateFeeFor : $feeEstimate");
print("ETH nonce custom response: $nResponse");
print("ETH actual nonce : $nonce");
print("==============================================================");
final tx = web3.Transaction(
to: web3.EthereumAddress.fromHex(address),
gasPrice: web3.EtherAmount.fromUnitAndValue(
web3.EtherUnit.wei,
fee,
),
maxGas: _gasLimit,
value: web3.EtherAmount.inWei(bigIntAmount),
nonce: nonce,
);
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,
2023-03-31 23:17:15 +00:00
"ethTx": tx,
"chainId": (await client.getChainId()).toInt(),
"nonce": tx.nonce,
2022-12-20 12:54:51 +00:00
};
return txData;
2022-12-13 17:39:19 +00:00
}
2023-03-31 15:25:08 +00:00
@override
Future<String> confirmSend({required Map<String, dynamic> txData}) async {
web3.Web3Client client = getEthClient();
2023-03-31 23:17:15 +00:00
final txid = await client.sendTransaction(
_credentials,
txData["ethTx"] as web3.Transaction,
chainId: txData["chainId"] as int,
);
2023-03-31 15:25:08 +00:00
return txid;
}
2022-12-13 17:39:19 +00:00
@override
Future<void> recoverFromMnemonic({
required String mnemonic,
String? mnemonicPassphrase,
required int maxUnusedAddressGap,
required int maxNumberOfIndexesToCheck,
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 {
// check to make sure we aren't overwriting a mnemonic
// this should never fail
if ((await mnemonicString) != null ||
(await this.mnemonicPassphrase) != null) {
2023-01-08 15:19:58 +00:00
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());
await _secureStore.write(
key: '${_walletId}_mnemonicPassphrase',
value: mnemonicPassphrase ?? "",
);
2023-01-08 15:19:58 +00:00
String privateKey =
getPrivateKey(mnemonic.trim(), mnemonicPassphrase ?? "");
_credentials = web3.EthPrivateKey.fromHex(privateKey);
final address = Address(
walletId: walletId, value: _credentials.address.toString(),
publicKey: [], // maybe store address bytes here? seems a waste of space though
derivationIndex: 0,
derivationPath: DerivationPath()..value = "$hdPathEthereum/0",
type: AddressType.ethereum,
subType: AddressSubType.receiving,
);
2023-01-25 12:09:07 +00:00
await db.putAddress(address);
await Future.wait([
updateCachedId(walletId),
updateCachedIsFavorite(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<List<Address>> _fetchAllOwnAddresses() => db
.getAddresses(walletId)
.filter()
.not()
.typeEqualTo(AddressType.nonWallet)
.and()
.group((q) => q
.subTypeEqualTo(AddressSubType.receiving)
.or()
.subTypeEqualTo(AddressSubType.change))
.findAll();
2023-01-08 15:19:58 +00:00
Future<bool> refreshIfThereIsNewData() async {
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-03-31 23:17:15 +00:00
final response = await EthereumAPI.getEthTransactionByHash(txid);
final txBlockNumber = response.value?.blockNumber;
if (txBlockNumber != null) {
final int txConfirmations = currentChainHeight - txBlockNumber;
bool isUnconfirmed = txConfirmations < MINIMUM_CONFIRMATIONS;
if (!isUnconfirmed) {
needsRefresh = true;
break;
}
2023-01-11 13:35:51 +00:00
}
2023-01-09 11:10:34 +00:00
}
2023-01-13 14:36:50 +00:00
if (!needsRefresh) {
var allOwnAddresses = await _fetchAllOwnAddresses();
final response = await EthereumAPI.getEthTransactions(
2023-03-01 21:19:53 +00:00
allOwnAddresses.elementAt(0).value,
);
if (response.value != null) {
final allTxs = response.value!;
for (final element in allTxs) {
final txid = element.hash;
if ((await db
.getTransactions(walletId)
.filter()
.txidMatches(txid)
.findFirst()) ==
null) {
2023-01-13 14:36:50 +00:00
Logging.instance.log(
" txid not found in address history already $txid",
2023-01-13 14:36:50 +00:00
level: LogLevel.Info);
needsRefresh = true;
break;
2023-01-13 14:36:50 +00:00
}
}
2023-03-03 17:36:28 +00:00
} else {
Logging.instance.log(
" refreshIfThereIsNewData get eth transactions failed: ${response.exception}",
level: LogLevel.Error,
);
2023-01-13 14:36:50 +00:00
}
}
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() async {
2023-01-08 15:19:58 +00:00
if (_hasCalledExit) return;
List<Transaction> unconfirmedTxnsToNotifyPending = [];
List<Transaction> unconfirmedTxnsToNotifyConfirmed = [];
final currentChainHeight = await chainHeight;
2023-01-08 15:19:58 +00:00
final txCount = await db.getTransactions(walletId).count();
const paginateLimit = 50;
for (int i = 0; i < txCount; i += paginateLimit) {
final transactions = await db
.getTransactions(walletId)
.offset(i)
.limit(paginateLimit)
.findAll();
for (final tx in transactions) {
if (tx.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) {
2023-01-08 15:19:58 +00:00
// 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) {
final confirmations = tx.getConfirmations(currentChainHeight);
if (tx.type == TransactionType.incoming) {
2023-01-08 15:19:58 +00:00
unawaited(NotificationApi.showNotification(
title: "Incoming transaction",
body: walletName,
walletId: walletId,
iconAssetName: Assets.svg.iconFor(coin: coin),
date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS,
2023-01-08 15:19:58 +00:00
coinName: coin.name,
txid: tx.txid,
confirmations: confirmations,
2023-01-08 15:19:58 +00:00
requiredConfirmations: MINIMUM_CONFIRMATIONS,
));
await txTracker.addNotifiedPending(tx.txid);
} else if (tx.type == TransactionType.outgoing) {
2023-01-08 15:19:58 +00:00
unawaited(NotificationApi.showNotification(
title: "Sending transaction",
body: walletName,
walletId: walletId,
iconAssetName: Assets.svg.iconFor(coin: coin),
date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000),
shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS,
2023-01-08 15:19:58 +00:00
coinName: coin.name,
txid: tx.txid,
confirmations: confirmations,
2023-01-08 15:19:58 +00:00
requiredConfirmations: MINIMUM_CONFIRMATIONS,
));
await txTracker.addNotifiedPending(tx.txid);
}
}
// notify on confirmed
for (final tx in unconfirmedTxnsToNotifyConfirmed) {
if (tx.type == TransactionType.incoming) {
2023-01-08 15:19:58 +00:00
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.type == TransactionType.outgoing) {
2023-01-08 15:19:58 +00:00
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) {
2023-01-08 15:19:58 +00:00
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId));
final newTxDataFuture = _refreshTransactions();
GlobalEventBus.instance
.fire(RefreshPercentChangedEvent(0.50, walletId));
2023-01-08 15:19:58 +00:00
2023-03-29 18:49:12 +00:00
// final feeObj = _getFees();
2023-01-08 15:19:58 +00:00
GlobalEventBus.instance
.fire(RefreshPercentChangedEvent(0.60, walletId));
GlobalEventBus.instance
.fire(RefreshPercentChangedEvent(0.70, walletId));
2023-03-29 18:49:12 +00:00
// _feeObject = Future(() => feeObj);
2023-01-08 15:19:58 +00:00
GlobalEventBus.instance
.fire(RefreshPercentChangedEvent(0.80, walletId));
final allTxsToWatch = getAllTxsToWatch();
2023-01-08 15:19:58 +00:00
await Future.wait([
updateBalance(),
newTxDataFuture,
2023-03-29 18:49:12 +00:00
// feeObj,
2023-01-08 15:19:58 +00:00
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 $walletName $walletId refresh(): $error\n$strace",
level: LogLevel.Warning,
);
2022-12-20 12:54:51 +00:00
}
2022-12-13 17:39:19 +00:00
}
@override
2023-01-08 15:19:58 +00:00
Future<bool> testNetworkConnection() async {
web3.Web3Client client = getEthClient();
2023-01-08 15:19:58 +00:00
try {
await client.getBlockNumber();
return true;
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
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 {
2023-03-31 23:17:15 +00:00
final txid = txData["txid"] as String;
final addressString = txData["address"] as String;
final response = await EthereumAPI.getEthTransactionByHash(txid);
final transaction = Transaction(
walletId: walletId,
txid: txid,
timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000,
type: TransactionType.outgoing,
subType: TransactionSubType.none,
amount: txData["recipientAmt"] as int,
amountString: Amount(
rawValue: BigInt.from(txData["recipientAmt"] as int),
fractionDigits: coin.decimals,
).toJsonString(),
fee: txData["fee"] as int,
height: null,
isCancelled: false,
isLelantus: false,
otherData: null,
slateId: null,
nonce: (txData["nonce"] as int?) ??
response.value?.nonce.toBigIntFromHex.toInt(),
inputs: [],
outputs: [],
);
Address? address = await db.getAddress(
walletId,
addressString,
);
address ??= Address(
walletId: walletId,
value: addressString,
publicKey: [],
derivationIndex: -1,
derivationPath: null,
type: AddressType.ethereum,
subType: AddressSubType.nonWallet,
);
await db.addNewTransactionData(
[
Tuple2(transaction, address),
],
walletId,
);
2022-12-13 17:39:19 +00:00
}
@override
bool validateAddress(String address) {
return isValidEthereumAddress(address);
2022-12-13 17:39:19 +00:00
}
Future<void> _refreshTransactions() async {
String thisAddress = await currentReceivingAddress;
2023-01-06 15:25:28 +00:00
2023-03-31 16:15:42 +00:00
final response = await EthereumAPI.getEthTransactions(thisAddress);
if (response.value == null) {
Logging.instance.log(
"Failed to refresh transactions for ${coin.prettyName} $walletName "
"$walletId: ${response.exception}",
level: LogLevel.Warning,
);
return;
}
final txsResponse =
await EthereumAPI.getEthTransactionNonces(response.value!);
2023-01-13 09:21:10 +00:00
if (txsResponse.value != null) {
final allTxs = txsResponse.value!;
final List<Tuple2<Transaction, Address?>> txnsData = [];
2023-03-31 16:15:42 +00:00
for (final tuple in allTxs) {
final element = tuple.item1;
2023-03-27 14:41:59 +00:00
Amount transactionAmount = element.value;
2023-01-06 15:25:28 +00:00
bool isIncoming;
bool txFailed = false;
if (checksumEthereumAddress(element.from) == thisAddress) {
if (element.isError != 0) {
txFailed = true;
}
isIncoming = false;
2023-01-06 15:25:28 +00:00
} else {
isIncoming = true;
2023-01-06 15:25:28 +00:00
}
2023-01-05 11:07:46 +00:00
2023-01-06 15:25:28 +00:00
//Calculate fees (GasLimit * gasPrice)
// int txFee = element.gasPrice * element.gasUsed;
2023-03-27 14:41:59 +00:00
Amount txFee = element.gasCost;
final String addressString = checksumEthereumAddress(element.to);
final int height = element.blockNumber;
final txn = Transaction(
walletId: walletId,
txid: element.hash,
timestamp: element.timestamp,
type:
isIncoming ? TransactionType.incoming : TransactionType.outgoing,
subType: TransactionSubType.none,
2023-03-27 14:41:59 +00:00
amount: transactionAmount.raw.toInt(),
amountString: transactionAmount.toJsonString(),
fee: txFee.raw.toInt(),
height: height,
isCancelled: txFailed,
isLelantus: false,
slateId: null,
otherData: null,
2023-03-31 16:15:42 +00:00
nonce: tuple.item2,
inputs: [],
outputs: [],
);
Address? transactionAddress = await db
.getAddresses(walletId)
.filter()
.valueEqualTo(addressString)
.findFirst();
if (transactionAddress == null) {
if (isIncoming) {
transactionAddress = Address(
walletId: walletId,
value: addressString,
publicKey: [],
derivationIndex: 0,
derivationPath: DerivationPath()..value = "$hdPathEthereum/0",
type: AddressType.ethereum,
subType: AddressSubType.receiving,
);
} else {
final myRcvAddr = await currentReceivingAddress;
final isSentToSelf = myRcvAddr == addressString;
transactionAddress = Address(
walletId: walletId,
value: addressString,
publicKey: [],
derivationIndex: isSentToSelf ? 0 : -1,
derivationPath: isSentToSelf
? (DerivationPath()..value = "$hdPathEthereum/0")
: null,
type: AddressType.ethereum,
subType: isSentToSelf
? AddressSubType.receiving
: AddressSubType.nonWallet,
);
2023-01-06 15:25:28 +00:00
}
}
txnsData.add(Tuple2(txn, transactionAddress));
2023-01-06 15:25:28 +00:00
}
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,
),
);
}
} else {
Logging.instance.log(
2023-03-31 16:15:42 +00:00
"Failed to refresh transactions with nonces for ${coin.prettyName} "
"$walletName $walletId: ${txsResponse.exception}",
level: LogLevel.Warning,
);
2023-01-05 16:04:45 +00:00
}
2022-12-14 10:15:22 +00:00
}
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
}