stack_wallet/lib/wallets/wallet/impl/ethereum_wallet.dart
2024-01-16 16:53:29 -06:00

489 lines
14 KiB
Dart

import 'dart:async';
import 'dart:convert';
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/models/balance.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/services/ethereum/ethereum_api.dart';
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
import 'package:stackwallet/utilities/eth_commons.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/wallets/crypto_currency/coins/ethereum.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/intermediate/bip39_wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/private_key_interface.dart';
import 'package:web3dart/web3dart.dart' as web3;
// Eth can not use tor with web3dart
class EthereumWallet extends Bip39Wallet with PrivateKeyInterface {
EthereumWallet(CryptoCurrencyNetwork network) : super(Ethereum(network));
Timer? timer;
web3.EthPrivateKey? _credentials;
Future<void> updateTokenContracts(List<String> contractAddresses) async {
await info.updateContractAddresses(
newContractAddresses: contractAddresses.toSet(),
isar: mainDB.isar,
);
GlobalEventBus.instance.fire(
UpdatedInBackgroundEvent(
"$contractAddresses updated/added for: $walletId ${info.name}",
walletId,
),
);
}
web3.Web3Client getEthClient() {
final node = getCurrentNode();
// Eth can not use tor with web3dart as Client does not support proxies
final client = Client();
return web3.Web3Client(node.host, client);
}
Amount estimateEthFee(int feeRate, int gasLimit, int decimals) {
final gweiAmount = feeRate.toDecimal() / (Decimal.ten.pow(9).toDecimal());
final fee = gasLimit.toDecimal() *
gweiAmount.toDecimal(
scaleOnInfinitePrecision: cryptoCurrency.fractionDigits,
);
//Convert gwei to ETH
final feeInWei = fee * Decimal.ten.pow(9).toDecimal();
final ethAmount = feeInWei / Decimal.ten.pow(decimals).toDecimal();
return Amount.fromDecimal(
ethAmount.toDecimal(
scaleOnInfinitePrecision: cryptoCurrency.fractionDigits,
),
fractionDigits: decimals,
);
}
// ==================== Private ==============================================
Future<void> _initCredentials() async {
final mnemonic = await getMnemonic();
final mnemonicPassphrase = await getMnemonicPassphrase();
final privateKey = getPrivateKey(mnemonic, mnemonicPassphrase);
_credentials = web3.EthPrivateKey.fromHex(privateKey);
}
// ==================== Overrides ============================================
@override
int get isarTransactionVersion => 2;
@override
FilterOperation? get transactionFilterOperation => FilterGroup.not(
const FilterCondition.equalTo(
property: r"subType",
value: TransactionSubType.ethToken,
),
);
@override
FilterOperation? get changeAddressFilterOperation =>
FilterGroup.and(standardChangeAddressFilters);
@override
FilterOperation? get receivingAddressFilterOperation =>
FilterGroup.and(standardReceivingAddressFilters);
@override
Future<void> checkSaveInitialReceivingAddress() async {
final address = await getCurrentReceivingAddress();
if (address == null) {
if (_credentials == null) {
await _initCredentials();
}
final address = Address(
walletId: walletId,
value: _credentials!.address.hexEip55,
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 mainDB.updateOrPutAddresses([address]);
}
}
@override
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
return estimateEthFee(
feeRate,
(cryptoCurrency as Ethereum).gasLimit,
cryptoCurrency.fractionDigits,
);
}
@override
Future<FeeObject> get fees => EthereumAPI.getFees();
@override
Future<bool> pingCheck() async {
web3.Web3Client client = getEthClient();
try {
await client.getBlockNumber();
return true;
} catch (_) {
return false;
}
}
@override
Future<void> updateBalance() async {
try {
final client = getEthClient();
final addressHex = (await getCurrentReceivingAddress())!.value;
final address = web3.EthereumAddress.fromHex(addressHex);
web3.EtherAmount ethBalance = await client.getBalance(address);
final balance = Balance(
total: Amount(
rawValue: ethBalance.getInWei,
fractionDigits: cryptoCurrency.fractionDigits,
),
spendable: Amount(
rawValue: ethBalance.getInWei,
fractionDigits: cryptoCurrency.fractionDigits,
),
blockedTotal: Amount.zeroWith(
fractionDigits: cryptoCurrency.fractionDigits,
),
pendingSpendable: Amount.zeroWith(
fractionDigits: cryptoCurrency.fractionDigits,
),
);
await info.updateBalance(
newBalance: balance,
isar: mainDB.isar,
);
} catch (e, s) {
Logging.instance.log(
"$runtimeType wallet failed to update balance: $e\n$s",
level: LogLevel.Warning,
);
}
}
@override
Future<void> updateChainHeight() async {
try {
final client = getEthClient();
final height = await client.getBlockNumber();
await info.updateCachedChainHeight(
newHeight: height,
isar: mainDB.isar,
);
} catch (e, s) {
Logging.instance.log(
"$runtimeType Exception caught in chainHeight: $e\n$s",
level: LogLevel.Warning,
);
}
}
@override
Future<void> updateNode() async {
// do nothing
}
@override
Future<void> updateTransactions({bool isRescan = false}) async {
final thisAddress = (await getCurrentReceivingAddress())!.value;
int firstBlock = 0;
if (!isRescan) {
firstBlock = await mainDB.isar.transactionV2s
.where()
.walletIdEqualTo(walletId)
.heightProperty()
.max() ??
0;
if (firstBlock > 10) {
// add some buffer
firstBlock -= 10;
}
}
final response = await EthereumAPI.getEthTransactions(
address: thisAddress,
firstBlock: isRescan ? 0 : firstBlock,
includeTokens: true,
);
if (response.value == null) {
Logging.instance.log(
"Failed to refresh transactions for ${cryptoCurrency.coin.prettyName} ${info.name} "
"$walletId: ${response.exception}",
level: LogLevel.Warning,
);
return;
}
if (response.value!.isEmpty) {
// no new transactions found
return;
}
final txsResponse =
await EthereumAPI.getEthTransactionNonces(response.value!);
if (txsResponse.value != null) {
final allTxs = txsResponse.value!;
final List<TransactionV2> txns = [];
for (final tuple in allTxs) {
final element = tuple.item1;
if (element.hasToken && !element.isError) {
continue;
}
//Calculate fees (GasLimit * gasPrice)
// int txFee = element.gasPrice * element.gasUsed;
final Amount txFee = element.gasCost;
final transactionAmount = element.value;
final addressFrom = checksumEthereumAddress(element.from);
final addressTo = checksumEthereumAddress(element.to);
bool isIncoming;
bool txFailed = false;
if (addressFrom == thisAddress) {
if (element.isError) {
txFailed = true;
}
isIncoming = false;
} else if (addressTo == thisAddress) {
isIncoming = true;
} else {
continue;
}
// hack eth tx data into inputs and outputs
final List<OutputV2> outputs = [];
final List<InputV2> inputs = [];
OutputV2 output = OutputV2.isarCantDoRequiredInDefaultConstructor(
scriptPubKeyHex: "00",
valueStringSats: transactionAmount.raw.toString(),
addresses: [
addressTo,
],
walletOwns: addressTo == thisAddress,
);
InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor(
scriptSigHex: null,
scriptSigAsm: null,
sequence: null,
outpoint: null,
addresses: [addressFrom],
valueStringSats: transactionAmount.raw.toString(),
witness: null,
innerRedeemScriptAsm: null,
coinbase: null,
walletOwns: addressFrom == thisAddress,
);
final TransactionType txType;
if (isIncoming) {
if (addressFrom == addressTo) {
txType = TransactionType.sentToSelf;
} else {
txType = TransactionType.incoming;
}
} else {
txType = TransactionType.outgoing;
}
outputs.add(output);
inputs.add(input);
final otherData = {
"nonce": tuple.item2,
"isCancelled": txFailed,
"overrideFee": txFee.toJsonString(),
};
final txn = TransactionV2(
walletId: walletId,
blockHash: element.blockHash,
hash: element.hash,
txid: element.hash,
timestamp: element.timestamp,
height: element.blockNumber,
inputs: List.unmodifiable(inputs),
outputs: List.unmodifiable(outputs),
version: -1,
type: txType,
subType: TransactionSubType.none,
otherData: jsonEncode(otherData),
);
txns.add(txn);
}
await mainDB.updateOrPutTransactionV2s(txns);
} else {
Logging.instance.log(
"Failed to refresh transactions with nonces for ${cryptoCurrency.coin.prettyName} "
"${info.name} $walletId: ${txsResponse.exception}",
level: LogLevel.Warning,
);
}
}
@override
Future<bool> updateUTXOs() async {
// not used in eth
return false;
}
@override
Future<TxData> prepareSend({required TxData txData}) async {
final int
rate; // TODO: use BigInt for feeObject whenever FeeObject gets redone
final feeObject = await fees;
switch (txData.feeRateType!) {
case FeeRateType.fast:
rate = feeObject.fast;
break;
case FeeRateType.average:
rate = feeObject.medium;
break;
case FeeRateType.slow:
rate = feeObject.slow;
break;
case FeeRateType.custom:
throw UnimplementedError("custom eth fees");
}
final feeEstimate = await estimateFeeFor(Amount.zero, rate);
// bool isSendAll = false;
// final availableBalance = balance.spendable;
// if (satoshiAmount == availableBalance) {
// isSendAll = true;
// }
//
// if (isSendAll) {
// //Subtract fee amount from send amount
// satoshiAmount -= feeEstimate;
// }
final client = getEthClient();
final myAddress = (await getCurrentReceivingAddress())!.value;
final myWeb3Address = web3.EthereumAddress.fromHex(myAddress);
final amount = txData.recipients!.first.amount;
final address = txData.recipients!.first.address;
// final est = await client.estimateGas(
// sender: myWeb3Address,
// to: web3.EthereumAddress.fromHex(address),
// gasPrice: web3.EtherAmount.fromUnitAndValue(
// web3.EtherUnit.wei,
// rate,
// ),
// amountOfGas: BigInt.from((cryptoCurrency as Ethereum).gasLimit),
// value: web3.EtherAmount.inWei(amount.raw),
// );
final nonce = txData.nonce ??
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,
rate,
),
maxGas: (cryptoCurrency as Ethereum).gasLimit,
value: web3.EtherAmount.inWei(amount.raw),
nonce: nonce,
);
return txData.copyWith(
nonce: tx.nonce,
web3dartTransaction: tx,
fee: feeEstimate,
feeInWei: BigInt.from(rate),
chainId: (await client.getChainId()),
);
}
@override
Future<TxData> confirmSend({required TxData txData}) async {
final client = getEthClient();
if (_credentials == null) {
await _initCredentials();
}
final txid = await client.sendTransaction(
_credentials!,
txData.web3dartTransaction!,
chainId: txData.chainId!.toInt(),
);
return txData.copyWith(
txid: txid,
txHash: txid,
);
}
@override
Future<void> recover({required bool isRescan}) async {
await refreshMutex.protect(() async {
if (isRescan) {
await mainDB.deleteWalletBlockchainData(walletId);
await checkSaveInitialReceivingAddress();
await updateBalance();
await updateTransactions(isRescan: true);
} else {
await checkSaveInitialReceivingAddress();
unawaited(updateBalance());
unawaited(updateTransactions());
}
});
}
@override
Future<void> exit() async {
timer?.cancel();
timer = null;
await super.exit();
}
}