mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-10 12:44:31 +00:00
WIP ecash new
This commit is contained in:
parent
226617c4c1
commit
279ed8196b
2 changed files with 453 additions and 0 deletions
446
lib/wallets/wallet/impl/ecash_wallet.dart
Normal file
446
lib/wallets/wallet/impl/ecash_wallet.dart
Normal file
|
@ -0,0 +1,446 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:bitbox/bitbox.dart' as bitbox;
|
||||
import 'package:coinlib_flutter/coinlib_flutter.dart' as coinlib;
|
||||
import 'package:isar/isar.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/services/coins/bitcoincash/bch_utils.dart';
|
||||
import 'package:stackwallet/services/coins/bitcoincash/cashtokens.dart'
|
||||
as cash_tokens;
|
||||
import 'package:stackwallet/utilities/amount/amount.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
|
||||
import 'package:stackwallet/utilities/extensions/extensions.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/ecash.dart';
|
||||
import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/mixins/electrumx_mixin.dart';
|
||||
|
||||
class EcashWallet extends Bip39HDWallet with ElectrumXMixin {
|
||||
@override
|
||||
int get isarTransactionVersion => 2;
|
||||
|
||||
EcashWallet(Ecash cryptoCurrency) : super(cryptoCurrency);
|
||||
|
||||
@override
|
||||
FilterOperation? get changeAddressFilterOperation => FilterGroup.and(
|
||||
[
|
||||
...standardChangeAddressFilters,
|
||||
const ObjectFilter(
|
||||
property: "derivationPath",
|
||||
filter: FilterCondition.startsWith(
|
||||
property: "value",
|
||||
value: "m/44'/899",
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
FilterOperation? get receivingAddressFilterOperation => FilterGroup.and(
|
||||
[
|
||||
...standardReceivingAddressFilters,
|
||||
const ObjectFilter(
|
||||
property: "derivationPath",
|
||||
filter: FilterCondition.startsWith(
|
||||
property: "value",
|
||||
value: "m/44'/899",
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
@override
|
||||
Future<List<Address>> fetchAllOwnAddresses() async {
|
||||
final allAddresses = await mainDB
|
||||
.getAddresses(walletId)
|
||||
.filter()
|
||||
.not()
|
||||
.typeEqualTo(AddressType.nonWallet)
|
||||
.and()
|
||||
.not()
|
||||
.subTypeEqualTo(AddressSubType.nonWallet)
|
||||
.findAll();
|
||||
return allAddresses;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
|
||||
@override
|
||||
Future<void> updateTransactions() async {
|
||||
List<Address> allAddressesOld = await fetchAllOwnAddresses();
|
||||
|
||||
Set<String> receivingAddresses = allAddressesOld
|
||||
.where((e) => e.subType == AddressSubType.receiving)
|
||||
.map((e) {
|
||||
if (bitbox.Address.detectFormat(e.value) == bitbox.Address.formatLegacy &&
|
||||
(cryptoCurrency.addressType(address: e.value) ==
|
||||
DerivePathType.bip44 ||
|
||||
cryptoCurrency.addressType(address: e.value) ==
|
||||
DerivePathType.eCash44)) {
|
||||
return bitbox.Address.toCashAddress(e.value);
|
||||
} else {
|
||||
return e.value;
|
||||
}
|
||||
}).toSet();
|
||||
|
||||
Set<String> changeAddresses = allAddressesOld
|
||||
.where((e) => e.subType == AddressSubType.change)
|
||||
.map((e) {
|
||||
if (bitbox.Address.detectFormat(e.value) == bitbox.Address.formatLegacy &&
|
||||
(cryptoCurrency.addressType(address: e.value) ==
|
||||
DerivePathType.bip44 ||
|
||||
cryptoCurrency.addressType(address: e.value) ==
|
||||
DerivePathType.eCash44)) {
|
||||
return bitbox.Address.toCashAddress(e.value);
|
||||
} else {
|
||||
return e.value;
|
||||
}
|
||||
}).toSet();
|
||||
|
||||
final allAddressesSet = {...receivingAddresses, ...changeAddresses};
|
||||
|
||||
final List<Map<String, dynamic>> allTxHashes =
|
||||
await fetchHistory(allAddressesSet);
|
||||
|
||||
List<Map<String, dynamic>> allTransactions = [];
|
||||
|
||||
for (final txHash in allTxHashes) {
|
||||
final storedTx = await mainDB.isar.transactionV2s
|
||||
.where()
|
||||
.txidWalletIdEqualTo(txHash["tx_hash"] as String, walletId)
|
||||
.findFirst();
|
||||
|
||||
if (storedTx == null ||
|
||||
storedTx.height == null ||
|
||||
(storedTx.height != null && storedTx.height! <= 0)) {
|
||||
final tx = await electrumXCached.getTransaction(
|
||||
txHash: txHash["tx_hash"] as String,
|
||||
verbose: true,
|
||||
coin: cryptoCurrency.coin,
|
||||
);
|
||||
|
||||
// check for duplicates before adding to list
|
||||
if (allTransactions
|
||||
.indexWhere((e) => e["txid"] == tx["txid"] as String) ==
|
||||
-1) {
|
||||
tx["height"] = txHash["height"];
|
||||
allTransactions.add(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final List<TransactionV2> txns = [];
|
||||
|
||||
for (final txData in allTransactions) {
|
||||
// set to true if any inputs were detected as owned by this wallet
|
||||
bool wasSentFromThisWallet = false;
|
||||
|
||||
// set to true if any outputs were detected as owned by this wallet
|
||||
bool wasReceivedInThisWallet = false;
|
||||
BigInt amountReceivedInThisWallet = BigInt.zero;
|
||||
BigInt changeAmountReceivedInThisWallet = BigInt.zero;
|
||||
|
||||
// parse inputs
|
||||
final List<InputV2> inputs = [];
|
||||
for (final jsonInput in txData["vin"] as List) {
|
||||
final map = Map<String, dynamic>.from(jsonInput as Map);
|
||||
|
||||
final List<String> addresses = [];
|
||||
String valueStringSats = "0";
|
||||
OutpointV2? outpoint;
|
||||
|
||||
final coinbase = map["coinbase"] as String?;
|
||||
|
||||
if (coinbase == null) {
|
||||
final txid = map["txid"] as String;
|
||||
final vout = map["vout"] as int;
|
||||
|
||||
final inputTx = await electrumXCached.getTransaction(
|
||||
txHash: txid,
|
||||
coin: cryptoCurrency.coin,
|
||||
);
|
||||
|
||||
final prevOutJson = Map<String, dynamic>.from(
|
||||
(inputTx["vout"] as List).firstWhere((e) => e["n"] == vout)
|
||||
as Map);
|
||||
|
||||
final prevOut = OutputV2.fromElectrumXJson(
|
||||
prevOutJson,
|
||||
decimalPlaces: cryptoCurrency.fractionDigits,
|
||||
walletOwns: false, // doesn't matter here as this is not saved
|
||||
);
|
||||
|
||||
outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor(
|
||||
txid: txid,
|
||||
vout: vout,
|
||||
);
|
||||
valueStringSats = prevOut.valueStringSats;
|
||||
addresses.addAll(prevOut.addresses);
|
||||
}
|
||||
|
||||
InputV2 input = InputV2.isarCantDoRequiredInDefaultConstructor(
|
||||
scriptSigHex: map["scriptSig"]?["hex"] as String?,
|
||||
sequence: map["sequence"] as int?,
|
||||
outpoint: outpoint,
|
||||
valueStringSats: valueStringSats,
|
||||
addresses: addresses,
|
||||
witness: map["witness"] as String?,
|
||||
coinbase: coinbase,
|
||||
innerRedeemScriptAsm: map["innerRedeemscriptAsm"] as String?,
|
||||
// don't know yet if wallet owns. Need addresses first
|
||||
walletOwns: false,
|
||||
);
|
||||
|
||||
if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) {
|
||||
wasSentFromThisWallet = true;
|
||||
input = input.copyWith(walletOwns: true);
|
||||
}
|
||||
|
||||
inputs.add(input);
|
||||
}
|
||||
|
||||
// parse outputs
|
||||
final List<OutputV2> outputs = [];
|
||||
for (final outputJson in txData["vout"] as List) {
|
||||
OutputV2 output = OutputV2.fromElectrumXJson(
|
||||
Map<String, dynamic>.from(outputJson as Map),
|
||||
decimalPlaces: cryptoCurrency.fractionDigits,
|
||||
// don't know yet if wallet owns. Need addresses first
|
||||
walletOwns: false,
|
||||
);
|
||||
|
||||
// if output was to my wallet, add value to amount received
|
||||
if (receivingAddresses
|
||||
.intersection(output.addresses.toSet())
|
||||
.isNotEmpty) {
|
||||
wasReceivedInThisWallet = true;
|
||||
amountReceivedInThisWallet += output.value;
|
||||
output = output.copyWith(walletOwns: true);
|
||||
} else if (changeAddresses
|
||||
.intersection(output.addresses.toSet())
|
||||
.isNotEmpty) {
|
||||
wasReceivedInThisWallet = true;
|
||||
changeAmountReceivedInThisWallet += output.value;
|
||||
output = output.copyWith(walletOwns: true);
|
||||
}
|
||||
|
||||
outputs.add(output);
|
||||
}
|
||||
|
||||
final totalOut = outputs
|
||||
.map((e) => e.value)
|
||||
.fold(BigInt.zero, (value, element) => value + element);
|
||||
|
||||
TransactionType type;
|
||||
TransactionSubType subType = TransactionSubType.none;
|
||||
|
||||
// at least one input was owned by this wallet
|
||||
if (wasSentFromThisWallet) {
|
||||
type = TransactionType.outgoing;
|
||||
|
||||
if (wasReceivedInThisWallet) {
|
||||
if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet ==
|
||||
totalOut) {
|
||||
// definitely sent all to self
|
||||
type = TransactionType.sentToSelf;
|
||||
} else if (amountReceivedInThisWallet == BigInt.zero) {
|
||||
// most likely just a typical send
|
||||
// do nothing here yet
|
||||
}
|
||||
|
||||
// check vout 0 for special scripts
|
||||
if (outputs.isNotEmpty) {
|
||||
final output = outputs.first;
|
||||
|
||||
// check for fusion
|
||||
if (BchUtils.isFUZE(output.scriptPubKeyHex.toUint8ListFromHex)) {
|
||||
subType = TransactionSubType.cashFusion;
|
||||
} else {
|
||||
// check other cases here such as SLP or cash tokens etc
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (wasReceivedInThisWallet) {
|
||||
// only found outputs owned by this wallet
|
||||
type = TransactionType.incoming;
|
||||
} else {
|
||||
Logging.instance.log(
|
||||
"Unexpected tx found (ignoring it): $txData",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
final tx = TransactionV2(
|
||||
walletId: walletId,
|
||||
blockHash: txData["blockhash"] as String?,
|
||||
hash: txData["hash"] as String,
|
||||
txid: txData["txid"] as String,
|
||||
height: txData["height"] as int?,
|
||||
version: txData["version"] as int,
|
||||
timestamp: txData["blocktime"] as int? ??
|
||||
DateTime.timestamp().millisecondsSinceEpoch ~/ 1000,
|
||||
inputs: List.unmodifiable(inputs),
|
||||
outputs: List.unmodifiable(outputs),
|
||||
type: type,
|
||||
subType: subType,
|
||||
);
|
||||
|
||||
txns.add(tx);
|
||||
}
|
||||
|
||||
await mainDB.updateOrPutTransactionV2s(txns);
|
||||
}
|
||||
|
||||
@override
|
||||
({String? blockedReason, bool blocked}) checkBlockUTXO(
|
||||
Map<String, dynamic> jsonUTXO,
|
||||
String? scriptPubKeyHex,
|
||||
Map<String, dynamic> jsonTX,
|
||||
) {
|
||||
bool blocked = false;
|
||||
String? blockedReason;
|
||||
|
||||
if (scriptPubKeyHex != null) {
|
||||
// check for cash tokens
|
||||
try {
|
||||
final ctOutput =
|
||||
cash_tokens.unwrap_spk(scriptPubKeyHex.toUint8ListFromHex);
|
||||
if (ctOutput.token_data != null) {
|
||||
// found a token!
|
||||
blocked = true;
|
||||
blockedReason = "Cash token output detected";
|
||||
}
|
||||
} catch (e, s) {
|
||||
// Probably doesn't contain a cash token so just log failure
|
||||
Logging.instance.log(
|
||||
"Script pub key \"$scriptPubKeyHex\" cash token"
|
||||
" parsing check failed: $e\n$s",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
}
|
||||
|
||||
// check for SLP tokens if not already blocked
|
||||
if (!blocked && BchUtils.isSLP(scriptPubKeyHex.toUint8ListFromHex)) {
|
||||
blocked = true;
|
||||
blockedReason = "SLP token output detected";
|
||||
}
|
||||
}
|
||||
|
||||
return (blockedReason: blockedReason, blocked: blocked);
|
||||
}
|
||||
|
||||
// TODO: correct formula for bch?
|
||||
@override
|
||||
Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) {
|
||||
return Amount(
|
||||
rawValue: BigInt.from(((181 * inputCount) + (34 * outputCount) + 10) *
|
||||
(feeRatePerKB / 1000).ceil()),
|
||||
fractionDigits: info.coin.decimals,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
int estimateTxFee({required int vSize, required int feeRatePerKB}) {
|
||||
return vSize * (feeRatePerKB / 1000).ceil();
|
||||
}
|
||||
|
||||
// not all coins need to override this. BCH does due to cash addr string formatting
|
||||
@override
|
||||
Future<({List<Address> addresses, int index})> checkGaps(
|
||||
int txCountBatchSize,
|
||||
coinlib.HDPrivateKey root,
|
||||
DerivePathType type,
|
||||
int chain,
|
||||
) async {
|
||||
List<Address> addressArray = [];
|
||||
int gapCounter = 0;
|
||||
int highestIndexWithHistory = 0;
|
||||
|
||||
// Scan addresses until the minimum required addresses have been scanned or
|
||||
// until the highest index with activity, plus the gap limit, whichever is
|
||||
// higher, so that we if there is activity above the minimum index, we don't
|
||||
// miss it.
|
||||
for (int index = 0;
|
||||
index <
|
||||
max(
|
||||
cryptoCurrency.maxNumberOfIndexesToCheck,
|
||||
highestIndexWithHistory +
|
||||
cryptoCurrency.maxUnusedAddressGap) &&
|
||||
gapCounter < cryptoCurrency.maxUnusedAddressGap;
|
||||
index += txCountBatchSize) {
|
||||
Logging.instance.log(
|
||||
"index: $index, \t GapCounter $chain ${type.name}: $gapCounter",
|
||||
level: LogLevel.Info);
|
||||
|
||||
final _id = "k_$index";
|
||||
Map<String, String> txCountCallArgs = {};
|
||||
|
||||
for (int j = 0; j < txCountBatchSize; j++) {
|
||||
final derivePath = cryptoCurrency.constructDerivePath(
|
||||
derivePathType: type,
|
||||
chain: chain,
|
||||
index: index + j,
|
||||
);
|
||||
|
||||
final keys = root.derivePath(derivePath);
|
||||
final addressData = cryptoCurrency.getAddressForPublicKey(
|
||||
publicKey: keys.publicKey,
|
||||
derivePathType: type,
|
||||
);
|
||||
|
||||
// bch specific
|
||||
final addressString = bitbox.Address.toCashAddress(
|
||||
addressData.address.toString(),
|
||||
);
|
||||
|
||||
final address = Address(
|
||||
walletId: walletId,
|
||||
value: addressString,
|
||||
publicKey: keys.publicKey.data,
|
||||
type: addressData.addressType,
|
||||
derivationIndex: index + j,
|
||||
derivationPath: DerivationPath()..value = derivePath,
|
||||
subType:
|
||||
chain == 0 ? AddressSubType.receiving : AddressSubType.change,
|
||||
);
|
||||
|
||||
addressArray.add(address);
|
||||
|
||||
txCountCallArgs.addAll({
|
||||
"${_id}_$j": addressString,
|
||||
});
|
||||
}
|
||||
|
||||
// get address tx counts
|
||||
final counts = await fetchTxCountBatched(addresses: txCountCallArgs);
|
||||
|
||||
// check and add appropriate addresses
|
||||
for (int k = 0; k < txCountBatchSize; k++) {
|
||||
int count = counts["${_id}_$k"]!;
|
||||
if (count > 0) {
|
||||
// update highest
|
||||
highestIndexWithHistory = index + k;
|
||||
|
||||
// reset counter
|
||||
gapCounter = 0;
|
||||
}
|
||||
|
||||
// increase counter when no tx history found
|
||||
if (count == 0) {
|
||||
gapCounter++;
|
||||
}
|
||||
}
|
||||
// // cache all the transactions while waiting for the current function to finish.
|
||||
// unawaited(getTransactionCacheEarly(addressArray));
|
||||
}
|
||||
return (index: highestIndexWithHistory, addresses: addressArray);
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ import 'package:stackwallet/utilities/logger.dart';
|
|||
import 'package:stackwallet/utilities/prefs.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoin.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/bitcoincash.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/ecash.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/epiccash.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/coins/wownero.dart';
|
||||
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
|
||||
|
@ -28,6 +29,7 @@ import 'package:stackwallet/wallets/isar/models/wallet_info.dart';
|
|||
import 'package:stackwallet/wallets/models/tx_data.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/bitcoin_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/bitcoincash_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/ecash_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/epiccash_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/impl/wownero_wallet.dart';
|
||||
import 'package:stackwallet/wallets/wallet/intermediate/bip39_hd_wallet.dart';
|
||||
|
@ -268,6 +270,11 @@ abstract class Wallet<T extends CryptoCurrency> {
|
|||
Bitcoincash(CryptoCurrencyNetwork.test),
|
||||
);
|
||||
|
||||
case Coin.eCash:
|
||||
return EcashWallet(
|
||||
Ecash(CryptoCurrencyNetwork.main),
|
||||
);
|
||||
|
||||
case Coin.epicCash:
|
||||
return EpiccashWallet(
|
||||
Epiccash(CryptoCurrencyNetwork.main),
|
||||
|
|
Loading…
Reference in a new issue