2022-12-23 11:51:36 +00:00
|
|
|
import 'dart:async';
|
2023-01-25 12:09:07 +00:00
|
|
|
|
2023-02-14 17:43:48 +00:00
|
|
|
import 'package:bip39/bip39.dart' as bip39;
|
2022-12-13 17:39:19 +00:00
|
|
|
import 'package:decimal/decimal.dart';
|
2023-01-05 12:39:21 +00:00
|
|
|
import 'package:ethereum_addresses/ethereum_addresses.dart';
|
2023-02-14 17:43:48 +00:00
|
|
|
import 'package:http/http.dart';
|
|
|
|
import 'package:isar/isar.dart';
|
|
|
|
import 'package:stackwallet/db/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';
|
2023-02-14 17:43:48 +00:00
|
|
|
import 'package:stackwallet/services/coins/coin_service.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/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';
|
2023-02-14 17:43:48 +00:00
|
|
|
import 'package:stackwallet/utilities/assets.dart';
|
2023-01-05 12:39:21 +00:00
|
|
|
import 'package:stackwallet/utilities/constants.dart';
|
2023-02-14 17:43:48 +00:00
|
|
|
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';
|
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';
|
2023-02-14 17:43:48 +00:00
|
|
|
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
|
|
|
|
2023-02-14 17:43:48 +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-02-14 17:43:48 +00:00
|
|
|
|
2023-01-12 14:14:49 +00:00
|
|
|
final _gasLimit = 21000;
|
2023-01-11 13:35:51 +00:00
|
|
|
|
2023-02-14 17:43:48 +00:00
|
|
|
Timer? timer;
|
|
|
|
Timer? _networkAliveTimer;
|
|
|
|
|
2023-01-25 09:29:20 +00:00
|
|
|
@override
|
|
|
|
String get walletId => _walletId;
|
|
|
|
late String _walletId;
|
|
|
|
|
2023-02-14 17:43:48 +00:00
|
|
|
@override
|
|
|
|
String get walletName => _walletName;
|
2023-01-25 09:29:20 +00:00
|
|
|
late String _walletName;
|
2023-02-14 17:43:48 +00:00
|
|
|
|
|
|
|
@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) {
|
2023-02-14 17:43:48 +00:00
|
|
|
_isFavorite = markFavorite;
|
|
|
|
updateCachedIsFavorite(markFavorite);
|
2022-12-13 17:39:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2023-02-14 17:43:48 +00:00
|
|
|
bool get isFavorite => _isFavorite ??= getCachedIsFavorite();
|
|
|
|
bool? _isFavorite;
|
2022-12-13 17:39:19 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
Coin get coin => _coin;
|
2023-02-14 17:43:48 +00:00
|
|
|
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;
|
|
|
|
|
2023-02-14 17:43:48 +00:00
|
|
|
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
|
|
|
|
2023-02-14 17:43:48 +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
|
|
|
|
2023-02-14 17:43:48 +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
|
2023-02-14 17:43:48 +00:00
|
|
|
Future<List<UTXO>> get utxos => db.getUTXOs(walletId).findAll();
|
2022-12-13 17:39:19 +00:00
|
|
|
|
|
|
|
@override
|
2023-02-14 17:43:48 +00:00
|
|
|
Future<List<Transaction>> get transactions =>
|
|
|
|
db.getTransactions(walletId).sortByTimestampDesc().findAll();
|
2022-12-13 17:39:19 +00:00
|
|
|
|
|
|
|
@override
|
2023-02-14 17:43:48 +00:00
|
|
|
Future<String> get currentReceivingAddress async {
|
|
|
|
final address = await _currentReceivingAddress;
|
|
|
|
return address?.value ??
|
|
|
|
checksumEthereumAddress(_credentials.address.toString());
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Address?> get _currentReceivingAddress => db
|
|
|
|
.getAddresses(walletId)
|
|
|
|
.filter()
|
|
|
|
.typeEqualTo(AddressType.p2wpkh)
|
|
|
|
.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-14 16:09:24 +00:00
|
|
|
Future<String> confirmSend({required Map<String, dynamic> txData}) async {
|
2023-02-14 17:43:48 +00:00
|
|
|
web3.Web3Client client = getEthClient();
|
2023-01-13 14:36:50 +00:00
|
|
|
final int chainId = await client.getNetworkId();
|
2022-12-26 14:12:51 +00:00
|
|
|
final amount = txData['recipientAmt'];
|
2023-02-14 17:43:48 +00:00
|
|
|
final decimalAmount = Format.satoshisToAmount(amount as int, coin: coin);
|
|
|
|
final bigIntAmount = amountToBigInt(
|
|
|
|
decimalAmount.toDouble(),
|
|
|
|
Constants.decimalPlacesForCoin(coin),
|
|
|
|
);
|
|
|
|
|
|
|
|
final tx = web3.Transaction(
|
|
|
|
to: web3.EthereumAddress.fromHex(txData['address'] as String),
|
|
|
|
gasPrice: web3.EtherAmount.fromUnitAndValue(
|
|
|
|
web3.EtherUnit.wei, txData['feeInWei']),
|
2023-01-12 14:14:49 +00:00
|
|
|
maxGas: _gasLimit,
|
2023-02-14 17:43:48 +00:00
|
|
|
value: web3.EtherAmount.inWei(bigIntAmount));
|
2023-01-03 12:50:32 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2022-12-20 12:54:51 +00:00
|
|
|
Future<int> estimateFeeFor(int satoshiAmount, int feeRate) async {
|
2023-01-26 18:08:12 +00:00
|
|
|
final fee = estimateFee(feeRate, _gasLimit, 18);
|
|
|
|
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
|
2022-12-14 16:09:24 +00:00
|
|
|
Future<FeeObject> get fees => _feeObject ??= _getFees();
|
|
|
|
Future<FeeObject>? _feeObject;
|
|
|
|
|
|
|
|
Future<FeeObject> _getFees() async {
|
2023-01-26 18:08:12 +00:00
|
|
|
return await getFees();
|
2023-01-12 08:09:11 +00:00
|
|
|
}
|
|
|
|
|
2023-01-12 17:24:26 +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 {
|
2023-02-14 17:43:48 +00:00
|
|
|
Logging.instance.log(
|
|
|
|
"initializeExisting() ${coin.prettyName} wallet",
|
|
|
|
level: LogLevel.Info,
|
|
|
|
);
|
2023-01-03 12:50:32 +00:00
|
|
|
|
2023-02-14 17:43:48 +00:00
|
|
|
//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
|
|
|
|
2023-02-14 17:43:48 +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 {
|
2023-02-14 17:43:48 +00:00
|
|
|
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();
|
2023-02-14 17:43:48 +00:00
|
|
|
|
|
|
|
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);
|
2023-01-03 12:50:32 +00:00
|
|
|
await _secureStore.write(
|
2023-02-14 17:43:48 +00:00
|
|
|
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
|
2023-01-12 17:24:26 +00:00
|
|
|
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();
|
|
|
|
|
2023-02-14 17:43:48 +00:00
|
|
|
@override
|
|
|
|
Future<String?> get mnemonicString =>
|
|
|
|
_secureStore.read(key: '${_walletId}_mnemonic');
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<String?> get mnemonicPassphrase => _secureStore.read(
|
|
|
|
key: '${_walletId}_mnemonicPassphrase',
|
|
|
|
);
|
|
|
|
|
2022-12-23 11:51:36 +00:00
|
|
|
Future<int> get chainHeight async {
|
2023-02-14 17:43:48 +00:00
|
|
|
web3.Web3Client client = getEthClient();
|
2022-12-23 11:51:36 +00:00
|
|
|
try {
|
2023-02-14 17:43:48 +00:00
|
|
|
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;
|
2022-12-23 11:51:36 +00:00
|
|
|
} catch (e, s) {
|
|
|
|
Logging.instance.log("Exception caught in chainHeight: $e\n$s",
|
|
|
|
level: LogLevel.Error);
|
2023-02-14 17:43:48 +00:00
|
|
|
return storedChainHeight;
|
2022-12-23 11:51:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-14 17:43:48 +00:00
|
|
|
@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 {
|
2023-02-14 17:43:48 +00:00
|
|
|
final _mnemonicString = await mnemonicString;
|
|
|
|
if (_mnemonicString == null) {
|
2022-12-14 10:15:22 +00:00
|
|
|
return [];
|
|
|
|
}
|
2023-02-14 17:43:48 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-01-12 17:24:26 +00:00
|
|
|
final feeEstimate = await estimateFeeFor(satoshiAmount, fee);
|
2023-01-13 09:21:10 +00:00
|
|
|
|
|
|
|
bool isSendAll = false;
|
2023-02-14 17:43:48 +00:00
|
|
|
final availableBalance = balance.spendable;
|
|
|
|
if (satoshiAmount == availableBalance) {
|
2023-01-13 09:21:10 +00:00
|
|
|
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,
|
2023-01-12 17:24:26 +00:00
|
|
|
"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
|
2023-02-14 17:43:48 +00:00
|
|
|
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 {
|
2023-02-14 17:43:48 +00:00
|
|
|
// 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());
|
2023-02-14 17:43:48 +00:00
|
|
|
await _secureStore.write(
|
|
|
|
key: '${_walletId}_mnemonicPassphrase',
|
|
|
|
value: mnemonicPassphrase ?? "",
|
|
|
|
);
|
2023-01-08 15:19:58 +00:00
|
|
|
|
2023-02-14 17:43:48 +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
|
|
|
|
2023-02-14 17:43:48 +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);
|
|
|
|
}
|
|
|
|
|
2023-02-14 17:43:48 +00:00
|
|
|
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 {
|
2023-02-14 17:43:48 +00:00
|
|
|
web3.Web3Client client = 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();
|
2023-01-25 09:29:20 +00:00
|
|
|
AddressTransaction addressTransactions = await fetchAddressTransactions(
|
2023-02-14 17:43:48 +00:00
|
|
|
allOwnAddresses.elementAt(0).value, "txlist");
|
2023-01-13 14:36:50 +00:00
|
|
|
if (addressTransactions.message == "OK") {
|
|
|
|
final allTxs = addressTransactions.result;
|
2023-02-14 17:43:48 +00:00
|
|
|
for (final element in allTxs) {
|
|
|
|
final txid = element["hash"] as String;
|
|
|
|
if ((await db
|
|
|
|
.getTransactions(walletId)
|
|
|
|
.filter()
|
|
|
|
.txidMatches(txid)
|
|
|
|
.findFirst()) ==
|
|
|
|
null) {
|
2023-01-13 14:36:50 +00:00
|
|
|
Logging.instance.log(
|
2023-02-14 17:43:48 +00:00
|
|
|
" txid not found in address history already ${element['hash']}",
|
2023-01-13 14:36:50 +00:00
|
|
|
level: LogLevel.Info);
|
|
|
|
needsRefresh = true;
|
2023-02-14 17:43:48 +00:00
|
|
|
break;
|
2023-01-13 14:36:50 +00:00
|
|
|
}
|
2023-02-14 17:43:48 +00:00
|
|
|
}
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-14 17:43:48 +00:00
|
|
|
Future<void> getAllTxsToWatch() async {
|
2023-01-08 15:19:58 +00:00
|
|
|
if (_hasCalledExit) return;
|
2023-02-14 17:43:48 +00:00
|
|
|
List<Transaction> unconfirmedTxnsToNotifyPending = [];
|
|
|
|
List<Transaction> unconfirmedTxnsToNotifyConfirmed = [];
|
|
|
|
|
|
|
|
final currentChainHeight = await chainHeight;
|
2023-01-08 15:19:58 +00:00
|
|
|
|
2023-02-14 17:43:48 +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) {
|
2023-02-14 17:43:48 +00:00
|
|
|
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),
|
2023-02-14 17:43:48 +00:00
|
|
|
shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS,
|
2023-01-08 15:19:58 +00:00
|
|
|
coinName: coin.name,
|
|
|
|
txid: tx.txid,
|
2023-02-14 17:43:48 +00:00
|
|
|
confirmations: confirmations,
|
2023-01-08 15:19:58 +00:00
|
|
|
requiredConfirmations: MINIMUM_CONFIRMATIONS,
|
|
|
|
));
|
|
|
|
await txTracker.addNotifiedPending(tx.txid);
|
2023-02-14 17:43:48 +00:00
|
|
|
} 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),
|
2023-02-14 17:43:48 +00:00
|
|
|
shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS,
|
2023-01-08 15:19:58 +00:00
|
|
|
coinName: coin.name,
|
|
|
|
txid: tx.txid,
|
2023-02-14 17:43:48 +00:00
|
|
|
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) {
|
2023-02-14 17:43:48 +00:00
|
|
|
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);
|
2023-02-14 17:43:48 +00:00
|
|
|
} 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 {
|
2023-01-05 12:39:21 +00:00
|
|
|
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));
|
|
|
|
|
2022-12-23 11:51:36 +00:00
|
|
|
final currentHeight = await chainHeight;
|
2022-12-20 12:54:51 +00:00
|
|
|
const storedHeight = 1; //await storedChainHeight;
|
|
|
|
|
2022-12-23 11:51:36 +00:00
|
|
|
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));
|
|
|
|
|
2023-02-14 17:43:48 +00:00
|
|
|
final newTxDataFuture = _refreshTransactions();
|
2022-12-23 11:51:36 +00:00
|
|
|
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));
|
|
|
|
|
|
|
|
GlobalEventBus.instance
|
|
|
|
.fire(RefreshPercentChangedEvent(0.70, walletId));
|
|
|
|
_feeObject = Future(() => feeObj);
|
|
|
|
GlobalEventBus.instance
|
|
|
|
.fire(RefreshPercentChangedEvent(0.80, walletId));
|
|
|
|
|
2023-02-14 17:43:48 +00:00
|
|
|
final allTxsToWatch = getAllTxsToWatch();
|
2023-01-08 15:19:58 +00:00
|
|
|
await Future.wait([
|
2023-02-14 17:43:48 +00:00
|
|
|
updateBalance(),
|
|
|
|
newTxDataFuture,
|
2023-01-08 15:19:58 +00:00
|
|
|
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
|
2023-01-08 15:19:58 +00:00
|
|
|
Future<bool> testNetworkConnection() async {
|
2023-02-14 17:43:48 +00:00
|
|
|
web3.Web3Client client = 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
|
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
|
2023-01-05 12:39:21 +00:00
|
|
|
Future<void> updateSentCachedTxData(Map<String, dynamic> txData) async {
|
2023-01-30 13:44:30 +00:00
|
|
|
//Only used for Electrumx coins
|
2022-12-13 17:39:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
bool validateAddress(String address) {
|
2023-01-05 12:39:21 +00:00
|
|
|
return isValidEthereumAddress(address);
|
2022-12-13 17:39:19 +00:00
|
|
|
}
|
|
|
|
|
2023-02-14 17:43:48 +00:00
|
|
|
Future<void> _refreshTransactions() async {
|
2022-12-23 11:51:36 +00:00
|
|
|
String thisAddress = await currentReceivingAddress;
|
2023-01-06 15:25:28 +00:00
|
|
|
|
2023-01-25 09:29:20 +00:00
|
|
|
AddressTransaction txs =
|
|
|
|
await fetchAddressTransactions(thisAddress, "txlist");
|
2023-01-13 09:21:10 +00:00
|
|
|
|
2023-01-06 15:25:28 +00:00
|
|
|
if (txs.message == "OK") {
|
|
|
|
final allTxs = txs.result;
|
2023-02-14 17:43:48 +00:00
|
|
|
final List<Tuple2<Transaction, Address?>> txnsData = [];
|
|
|
|
for (final element in allTxs) {
|
2023-01-06 15:25:28 +00:00
|
|
|
int transactionAmount = int.parse(element['value'].toString());
|
|
|
|
|
2023-02-14 17:43:48 +00:00
|
|
|
bool isIncoming;
|
|
|
|
bool txFailed = false;
|
2023-01-06 15:25:28 +00:00
|
|
|
if (checksumEthereumAddress(element["from"].toString()) ==
|
|
|
|
thisAddress) {
|
2023-02-14 17:43:48 +00:00
|
|
|
if (!(int.parse(element["isError"] as String) == 0)) {
|
|
|
|
txFailed = true;
|
|
|
|
}
|
|
|
|
isIncoming = false;
|
2023-01-06 15:25:28 +00:00
|
|
|
} else {
|
2023-02-14 17:43:48 +00:00
|
|
|
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 = int.parse(element['gasPrice'].toString()) *
|
|
|
|
int.parse(element['gasUsed'].toString());
|
2023-01-05 12:39:21 +00:00
|
|
|
|
2023-02-14 17:43:48 +00:00
|
|
|
final String addressString = element["to"] as String;
|
|
|
|
final int height = int.parse(element['blockNumber'].toString());
|
|
|
|
|
|
|
|
final txn = Transaction(
|
|
|
|
walletId: walletId,
|
|
|
|
txid: element["hash"] as String,
|
|
|
|
timestamp: element["timeStamp"] as int,
|
|
|
|
type:
|
|
|
|
isIncoming ? TransactionType.incoming : TransactionType.outgoing,
|
|
|
|
subType: TransactionSubType.none,
|
|
|
|
amount: transactionAmount,
|
|
|
|
fee: txFee,
|
|
|
|
height: height,
|
|
|
|
isCancelled: txFailed,
|
|
|
|
isLelantus: false,
|
|
|
|
slateId: null,
|
|
|
|
otherData: null,
|
|
|
|
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
|
|
|
}
|
2023-02-14 17:43:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
txnsData.add(Tuple2(txn, transactionAddress));
|
2023-01-06 15:25:28 +00:00
|
|
|
}
|
2023-02-14 17:43:48 +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(
|
|
|
|
"Failed to refresh transactions for ${coin.prettyName} $walletName $walletId: $txs",
|
|
|
|
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
|
|
|
}
|