between 2-4x speed increase for restore, first refresh, and nth refresh

This commit is contained in:
Marco 2022-09-09 23:07:27 +08:00
parent 691c4a772d
commit d8bc213749
4 changed files with 752 additions and 693 deletions

View file

@ -399,6 +399,138 @@ class BitcoinWallet extends CoinServiceAPI {
level: LogLevel.Info); level: LogLevel.Info);
} }
Future<Map<String, dynamic>> _checkGaps(
int maxNumberOfIndexesToCheck,
int maxUnusedAddressGap,
int txCountBatchSize,
bip32.BIP32 root,
DerivePathType type,
int account) async {
List<String> addressArray = [];
int returningIndex = -1;
Map<String, Map<String, String>> derivations = {};
int gapCounter = 0;
for (int index = 0;
index < maxNumberOfIndexesToCheck && gapCounter < maxUnusedAddressGap;
index += txCountBatchSize) {
List<String> iterationsAddressArray = [];
Logging.instance.log(
"index: $index, \t GapCounter $account ${type.name}: $gapCounter",
level: LogLevel.Info);
final ID = "k_$index";
Map<String, String> txCountCallArgs = {};
final Map<String, dynamic> receivingNodes = {};
for (int j = 0; j < txCountBatchSize; j++) {
final node = await compute(
getBip32NodeFromRootWrapper,
Tuple4(
account,
index + j,
root,
type,
),
);
String? address;
switch (type) {
case DerivePathType.bip44:
address = P2PKH(
data: PaymentData(pubkey: node.publicKey),
network: _network)
.data
.address!;
break;
case DerivePathType.bip49:
address = P2SH(
data: PaymentData(
redeem: P2WPKH(
data: PaymentData(pubkey: node.publicKey),
network: _network)
.data),
network: _network)
.data
.address!;
break;
case DerivePathType.bip84:
address = P2WPKH(
network: _network,
data: PaymentData(pubkey: node.publicKey))
.data
.address!;
break;
default:
throw Exception("No Path type $type exists");
}
receivingNodes.addAll({
"${ID}_$j": {
"node": node,
"address": address,
}
});
txCountCallArgs.addAll({
"${ID}_$j": address,
});
}
// get address tx counts
final counts = await _getBatchTxCount(addresses: txCountCallArgs);
// check and add appropriate addresses
for (int k = 0; k < txCountBatchSize; k++) {
int count = counts["${ID}_$k"]!;
if (count > 0) {
final node = receivingNodes["${ID}_$k"];
// add address to array
addressArray.add(node["address"] as String);
iterationsAddressArray.add(node["address"] as String);
// set current index
returningIndex = index + k;
// reset counter
gapCounter = 0;
// add info to derivations
derivations[node["address"] as String] = {
"pubKey": Format.uint8listToString(
(node["node"] as bip32.BIP32).publicKey),
"wif": (node["node"] as bip32.BIP32).toWIF(),
};
}
// 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(iterationsAddressArray));
}
return {
"addressArray": addressArray,
"index": returningIndex,
"derivations": derivations
};
}
Future<void> getTransactionCacheEarly(List<String> allAddresses) async {
try {
final List<Map<String, dynamic>> allTxHashes =
await _fetchHistory(allAddresses);
for (final txHash in allTxHashes) {
try {
unawaited(cachedElectrumXClient.getTransaction(
txHash: txHash["tx_hash"] as String,
verbose: true,
coin: coin,
));
} catch (e) {
continue;
}
}
} catch (e, s) {
//
}
}
Future<void> _recoverWalletFromBIP32SeedPhrase({ Future<void> _recoverWalletFromBIP32SeedPhrase({
required String mnemonic, required String mnemonic,
int maxUnusedAddressGap = 20, int maxUnusedAddressGap = 20,
@ -429,10 +561,6 @@ class BitcoinWallet extends CoinServiceAPI {
int p2shChangeIndex = -1; int p2shChangeIndex = -1;
int p2wpkhChangeIndex = -1; int p2wpkhChangeIndex = -1;
// The gap limit will be capped at [maxUnusedAddressGap]
int receivingGapCounter = 0;
int changeGapCounter = 0;
// actual size is 36 due to p2pkh, p2sh, and p2wpkh so 12x3 // actual size is 36 due to p2pkh, p2sh, and p2wpkh so 12x3
const txCountBatchSize = 12; const txCountBatchSize = 12;
@ -440,323 +568,71 @@ class BitcoinWallet extends CoinServiceAPI {
// receiving addresses // receiving addresses
Logging.instance Logging.instance
.log("checking receiving addresses...", level: LogLevel.Info); .log("checking receiving addresses...", level: LogLevel.Info);
for (int index = 0; Future resultReceive44 = _checkGaps(maxNumberOfIndexesToCheck,
index < maxNumberOfIndexesToCheck && maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 0);
receivingGapCounter < maxUnusedAddressGap;
index += txCountBatchSize) {
Logging.instance.log(
"index: $index, \t receivingGapCounter: $receivingGapCounter",
level: LogLevel.Info);
final receivingP2pkhID = "k_$index"; Future resultReceive49 = _checkGaps(maxNumberOfIndexesToCheck,
final receivingP2shID = "s_$index"; maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 0);
final receivingP2wpkhID = "w_$index";
Map<String, String> txCountCallArgs = {};
final Map<String, dynamic> receivingNodes = {};
for (int j = 0; j < txCountBatchSize; j++) { Future resultReceive84 = _checkGaps(maxNumberOfIndexesToCheck,
// bip44 / P2PKH maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 0);
final node44 = await compute(
getBip32NodeFromRootWrapper,
Tuple4(
0,
index + j,
root,
DerivePathType.bip44,
),
);
final p2pkhReceiveAddress = P2PKH(
data: PaymentData(pubkey: node44.publicKey),
network: _network)
.data
.address!;
receivingNodes.addAll({
"${receivingP2pkhID}_$j": {
"node": node44,
"address": p2pkhReceiveAddress,
}
});
txCountCallArgs.addAll({
"${receivingP2pkhID}_$j": p2pkhReceiveAddress,
});
// bip49 / P2SH
final node49 = await compute(
getBip32NodeFromRootWrapper,
Tuple4(
0,
index + j,
root,
DerivePathType.bip49,
),
);
final p2shReceiveAddress = P2SH(
data: PaymentData(
redeem: P2WPKH(
data: PaymentData(pubkey: node49.publicKey),
network: _network)
.data),
network: _network)
.data
.address!;
receivingNodes.addAll({
"${receivingP2shID}_$j": {
"node": node49,
"address": p2shReceiveAddress,
}
});
txCountCallArgs.addAll({
"${receivingP2shID}_$j": p2shReceiveAddress,
});
// bip84 / P2WPKH
final node84 = await compute(
getBip32NodeFromRootWrapper,
Tuple4(
0,
index + j,
root,
DerivePathType.bip84,
),
);
final p2wpkhReceiveAddress = P2WPKH(
network: _network,
data: PaymentData(pubkey: node84.publicKey))
.data
.address!;
receivingNodes.addAll({
"${receivingP2wpkhID}_$j": {
"node": node84,
"address": p2wpkhReceiveAddress,
}
});
txCountCallArgs.addAll({
"${receivingP2wpkhID}_$j": p2wpkhReceiveAddress,
});
}
// get address tx counts
final counts = await _getBatchTxCount(addresses: txCountCallArgs);
// check and add appropriate addresses
for (int k = 0; k < txCountBatchSize; k++) {
int p2pkhTxCount = counts["${receivingP2pkhID}_$k"]!;
if (p2pkhTxCount > 0) {
final node = receivingNodes["${receivingP2pkhID}_$k"];
// add address to array
p2pkhReceiveAddressArray.add(node["address"] as String);
// set current index
p2pkhReceiveIndex = index + k;
// reset counter
receivingGapCounter = 0;
// add info to derivations
p2pkhReceiveDerivations[node["address"] as String] = {
"pubKey": Format.uint8listToString(
(node["node"] as bip32.BIP32).publicKey),
"wif": (node["node"] as bip32.BIP32).toWIF(),
};
}
int p2shTxCount = counts["${receivingP2shID}_$k"]!;
if (p2shTxCount > 0) {
final node = receivingNodes["${receivingP2shID}_$k"];
// add address to array
p2shReceiveAddressArray.add(node["address"] as String);
// set current index
p2shReceiveIndex = index + k;
// reset counter
receivingGapCounter = 0;
// add info to derivations
p2shReceiveDerivations[node["address"] as String] = {
"pubKey": Format.uint8listToString(
(node["node"] as bip32.BIP32).publicKey),
"wif": (node["node"] as bip32.BIP32).toWIF(),
};
}
int p2wpkhTxCount = counts["${receivingP2wpkhID}_$k"]!;
if (p2wpkhTxCount > 0) {
final node = receivingNodes["${receivingP2wpkhID}_$k"];
// add address to array
p2wpkhReceiveAddressArray.add(node["address"] as String);
// set current index
p2wpkhReceiveIndex = index + k;
// reset counter
receivingGapCounter = 0;
// add info to derivations
p2wpkhReceiveDerivations[node["address"] as String] = {
"pubKey": Format.uint8listToString(
(node["node"] as bip32.BIP32).publicKey),
"wif": (node["node"] as bip32.BIP32).toWIF(),
};
}
// increase counter when no tx history found
if (p2wpkhTxCount == 0 && p2pkhTxCount == 0 && p2shTxCount == 0) {
receivingGapCounter++;
}
}
}
Logging.instance Logging.instance
.log("checking change addresses...", level: LogLevel.Info); .log("checking change addresses...", level: LogLevel.Info);
// change addresses // change addresses
for (int index = 0; Future resultChange44 = _checkGaps(maxNumberOfIndexesToCheck,
index < maxNumberOfIndexesToCheck && maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 1);
changeGapCounter < maxUnusedAddressGap;
index += txCountBatchSize) {
Logging.instance.log(
"index: $index, \t changeGapCounter: $changeGapCounter",
level: LogLevel.Info);
final changeP2pkhID = "k_$index";
final changeP2shID = "s_$index";
final changeP2wpkhID = "w_$index";
Map<String, String> args = {};
final Map<String, dynamic> changeNodes = {};
for (int j = 0; j < txCountBatchSize; j++) { Future resultChange49 = _checkGaps(maxNumberOfIndexesToCheck,
// bip44 / P2PKH maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 1);
final node44 = await compute(
getBip32NodeFromRootWrapper,
Tuple4(
1,
index + j,
root,
DerivePathType.bip44,
),
);
final p2pkhChangeAddress = P2PKH(
data: PaymentData(pubkey: node44.publicKey),
network: _network)
.data
.address!;
changeNodes.addAll({
"${changeP2pkhID}_$j": {
"node": node44,
"address": p2pkhChangeAddress,
}
});
args.addAll({
"${changeP2pkhID}_$j": p2pkhChangeAddress,
});
// bip49 / P2SH Future resultChange84 = _checkGaps(maxNumberOfIndexesToCheck,
final node49 = await compute( maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip84, 1);
getBip32NodeFromRootWrapper,
Tuple4(
1,
index + j,
root,
DerivePathType.bip49,
),
);
final p2shChangeAddress = P2SH(
data: PaymentData(
redeem: P2WPKH(
data: PaymentData(pubkey: node49.publicKey),
network: _network)
.data),
network: _network)
.data
.address!;
changeNodes.addAll({
"${changeP2shID}_$j": {
"node": node49,
"address": p2shChangeAddress,
}
});
args.addAll({
"${changeP2shID}_$j": p2shChangeAddress,
});
// bip84 / P2WPKH await Future.wait([
final node84 = await compute( resultReceive44,
getBip32NodeFromRootWrapper, resultReceive49,
Tuple4( resultReceive84,
1, resultChange44,
index + j, resultChange49,
root, resultChange84
DerivePathType.bip84, ]);
),
);
final p2wpkhChangeAddress = P2WPKH(
network: _network,
data: PaymentData(pubkey: node84.publicKey))
.data
.address!;
changeNodes.addAll({
"${changeP2wpkhID}_$j": {
"node": node84,
"address": p2wpkhChangeAddress,
}
});
args.addAll({
"${changeP2wpkhID}_$j": p2wpkhChangeAddress,
});
}
// get address tx counts p2pkhReceiveAddressArray =
final counts = await _getBatchTxCount(addresses: args); (await resultReceive44)['addressArray'] as List<String>;
p2pkhReceiveIndex = (await resultReceive44)['index'] as int;
p2pkhReceiveDerivations = (await resultReceive44)['derivations']
as Map<String, Map<String, String>>;
// check and add appropriate addresses p2shReceiveAddressArray =
for (int k = 0; k < txCountBatchSize; k++) { (await resultReceive49)['addressArray'] as List<String>;
int p2pkhTxCount = counts["${changeP2pkhID}_$k"]!; p2shReceiveIndex = (await resultReceive49)['index'] as int;
if (p2pkhTxCount > 0) { p2shReceiveDerivations = (await resultReceive49)['derivations']
final node = changeNodes["${changeP2pkhID}_$k"]; as Map<String, Map<String, String>>;
// add address to array
p2pkhChangeAddressArray.add(node["address"] as String);
// set current index
p2pkhChangeIndex = index + k;
// reset counter
changeGapCounter = 0;
// add info to derivations
p2pkhChangeDerivations[node["address"] as String] = {
"pubKey": Format.uint8listToString(
(node["node"] as bip32.BIP32).publicKey),
"wif": (node["node"] as bip32.BIP32).toWIF(),
};
}
int p2shTxCount = counts["${changeP2shID}_$k"]!; p2wpkhReceiveAddressArray =
if (p2shTxCount > 0) { (await resultReceive84)['addressArray'] as List<String>;
final node = changeNodes["${changeP2shID}_$k"]; p2wpkhReceiveIndex = (await resultReceive84)['index'] as int;
// add address to array p2wpkhReceiveDerivations = (await resultReceive84)['derivations']
p2shChangeAddressArray.add(node["address"] as String); as Map<String, Map<String, String>>;
// set current index
p2shChangeIndex = index + k;
// reset counter
changeGapCounter = 0;
// add info to derivations
p2shChangeDerivations[node["address"] as String] = {
"pubKey": Format.uint8listToString(
(node["node"] as bip32.BIP32).publicKey),
"wif": (node["node"] as bip32.BIP32).toWIF(),
};
}
int p2wpkhTxCount = counts["${changeP2wpkhID}_$k"]!; p2pkhChangeAddressArray =
if (p2wpkhTxCount > 0) { (await resultChange44)['addressArray'] as List<String>;
final node = changeNodes["${changeP2wpkhID}_$k"]; p2pkhChangeIndex = (await resultChange44)['index'] as int;
// add address to array p2pkhChangeDerivations = (await resultChange44)['derivations']
p2wpkhChangeAddressArray.add(node["address"] as String); as Map<String, Map<String, String>>;
// set current index
p2wpkhChangeIndex = index + k;
// reset counter
changeGapCounter = 0;
// add info to derivations
p2wpkhChangeDerivations[node["address"] as String] = {
"pubKey": Format.uint8listToString(
(node["node"] as bip32.BIP32).publicKey),
"wif": (node["node"] as bip32.BIP32).toWIF(),
};
}
// increase counter when no tx history found p2shChangeAddressArray =
if (p2wpkhTxCount == 0 && p2pkhTxCount == 0 && p2shTxCount == 0) { (await resultChange49)['addressArray'] as List<String>;
changeGapCounter++; p2shChangeIndex = (await resultChange49)['index'] as int;
} p2shChangeDerivations = (await resultChange49)['derivations']
} as Map<String, Map<String, String>>;
}
p2wpkhChangeAddressArray =
(await resultChange84)['addressArray'] as List<String>;
p2wpkhChangeIndex = (await resultChange84)['index'] as int;
p2wpkhChangeDerivations = (await resultChange84)['derivations']
as Map<String, Map<String, String>>;
// save the derivations (if any) // save the derivations (if any)
if (p2pkhReceiveDerivations.isNotEmpty) { if (p2pkhReceiveDerivations.isNotEmpty) {
@ -1098,10 +974,12 @@ class BitcoinWallet extends CoinServiceAPI {
} }
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId));
await _checkChangeAddressForTransactions(DerivePathType.bip84); Future changeAddressForTransactions =
_checkChangeAddressForTransactions(DerivePathType.bip84);
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId)); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
await _checkCurrentReceivingAddressesForTransactions(); Future currentReceivingAddressesForTransactions =
_checkCurrentReceivingAddressesForTransactions();
final newTxData = _fetchTransactionData(); final newTxData = _fetchTransactionData();
GlobalEventBus.instance GlobalEventBus.instance
@ -1121,7 +999,15 @@ class BitcoinWallet extends CoinServiceAPI {
GlobalEventBus.instance GlobalEventBus.instance
.fire(RefreshPercentChangedEvent(0.80, walletId)); .fire(RefreshPercentChangedEvent(0.80, walletId));
await getAllTxsToWatch(await newTxData); Future allTxsToWatch = getAllTxsToWatch(await newTxData);
await Future.wait([
newTxData,
changeAddressForTransactions,
currentReceivingAddressesForTransactions,
newUtxoData,
feeObj,
allTxsToWatch,
]);
GlobalEventBus.instance GlobalEventBus.instance
.fire(RefreshPercentChangedEvent(0.90, walletId)); .fire(RefreshPercentChangedEvent(0.90, walletId));
} }
@ -1137,7 +1023,7 @@ class BitcoinWallet extends CoinServiceAPI {
); );
if (shouldAutoSync) { if (shouldAutoSync) {
timer ??= Timer.periodic(const Duration(seconds: 150), (timer) async { timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async {
Logging.instance.log( Logging.instance.log(
"Periodic refresh check for $walletId $walletName in object instance: $hashCode", "Periodic refresh check for $walletId $walletName in object instance: $hashCode",
level: LogLevel.Info); level: LogLevel.Info);
@ -1972,7 +1858,7 @@ class BitcoinWallet extends CoinServiceAPI {
final fetchedUtxoList = <List<Map<String, dynamic>>>[]; final fetchedUtxoList = <List<Map<String, dynamic>>>[];
final Map<int, Map<String, List<dynamic>>> batches = {}; final Map<int, Map<String, List<dynamic>>> batches = {};
const batchSizeMax = 10; const batchSizeMax = 100;
int batchNumber = 0; int batchNumber = 0;
for (int i = 0; i < allAddresses.length; i++) { for (int i = 0; i < allAddresses.length; i++) {
if (batches[batchNumber] == null) { if (batches[batchNumber] == null) {
@ -2357,7 +2243,7 @@ class BitcoinWallet extends CoinServiceAPI {
final Map<int, Map<String, List<dynamic>>> batches = {}; final Map<int, Map<String, List<dynamic>>> batches = {};
final Map<String, String> requestIdToAddressMap = {}; final Map<String, String> requestIdToAddressMap = {};
const batchSizeMax = 10; const batchSizeMax = 100;
int batchNumber = 0; int batchNumber = 0;
for (int i = 0; i < allAddresses.length; i++) { for (int i = 0; i < allAddresses.length; i++) {
if (batches[batchNumber] == null) { if (batches[batchNumber] == null) {
@ -2404,6 +2290,43 @@ class BitcoinWallet extends CoinServiceAPI {
return false; return false;
} }
Future<List<Map<String, dynamic>>> fastFetch(List<String> allTxHashes) async {
List<Map<String, dynamic>> allTransactions = [];
const futureLimit = 30;
List<Future<Map<String, dynamic>>> transactionFutures = [];
int currentFutureCount = 0;
for (final txHash in allTxHashes) {
Future<Map<String, dynamic>> transactionFuture =
cachedElectrumXClient.getTransaction(
txHash: txHash,
verbose: true,
coin: coin,
);
transactionFutures.add(transactionFuture);
currentFutureCount++;
if (currentFutureCount > futureLimit) {
currentFutureCount = 0;
await Future.wait(transactionFutures);
for (final fTx in transactionFutures) {
final tx = await fTx;
allTransactions.add(tx);
}
}
}
if (currentFutureCount != 0) {
currentFutureCount = 0;
await Future.wait(transactionFutures);
for (final fTx in transactionFutures) {
final tx = await fTx;
allTransactions.add(tx);
}
}
return allTransactions;
}
Future<TransactionData> _fetchTransactionData() async { Future<TransactionData> _fetchTransactionData() async {
final List<String> allAddresses = await _fetchAllOwnAddresses(); final List<String> allAddresses = await _fetchAllOwnAddresses();
@ -2451,6 +2374,11 @@ class BitcoinWallet extends CoinServiceAPI {
} }
} }
Set<String> hashes = {};
for (var element in allTxHashes) {
hashes.add(element['tx_hash'] as String);
}
await fastFetch(hashes.toList());
List<Map<String, dynamic>> allTransactions = []; List<Map<String, dynamic>> allTransactions = [];
for (final txHash in allTxHashes) { for (final txHash in allTxHashes) {
@ -2480,6 +2408,16 @@ class BitcoinWallet extends CoinServiceAPI {
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
final List<Map<String, dynamic>> midSortedArray = []; final List<Map<String, dynamic>> midSortedArray = [];
Set<String> vHashes = {};
for (final txObject in allTransactions) {
for (int i = 0; i < (txObject["vin"] as List).length; i++) {
final input = txObject["vin"]![i] as Map;
final prevTxid = input["txid"] as String;
vHashes.add(prevTxid);
}
}
await fastFetch(vHashes.toList());
for (final txObject in allTransactions) { for (final txObject in allTransactions) {
List<String> sendersArray = []; List<String> sendersArray = [];
List<String> recipientsArray = []; List<String> recipientsArray = [];

View file

@ -346,6 +346,120 @@ class DogecoinWallet extends CoinServiceAPI {
level: LogLevel.Info); level: LogLevel.Info);
} }
Future<Map<String, dynamic>> _checkGaps(
int maxNumberOfIndexesToCheck,
int maxUnusedAddressGap,
int txCountBatchSize,
bip32.BIP32 root,
DerivePathType type,
int account) async {
List<String> addressArray = [];
int returningIndex = -1;
Map<String, Map<String, String>> derivations = {};
int gapCounter = 0;
for (int index = 0;
index < maxNumberOfIndexesToCheck && gapCounter < maxUnusedAddressGap;
index += txCountBatchSize) {
List<String> iterationsAddressArray = [];
Logging.instance.log(
"index: $index, \t GapCounter $account ${type.name}: $gapCounter",
level: LogLevel.Info);
final ID = "k_$index";
Map<String, String> txCountCallArgs = {};
final Map<String, dynamic> receivingNodes = {};
for (int j = 0; j < txCountBatchSize; j++) {
final node = await compute(
getBip32NodeFromRootWrapper,
Tuple4(
account,
index + j,
root,
type,
),
);
String? address;
switch (type) {
case DerivePathType.bip44:
address = P2PKH(
data: PaymentData(pubkey: node.publicKey),
network: _network)
.data
.address!;
break;
default:
throw Exception("No Path type $type exists");
}
receivingNodes.addAll({
"${ID}_$j": {
"node": node,
"address": address,
}
});
txCountCallArgs.addAll({
"${ID}_$j": address,
});
}
// get address tx counts
final counts = await _getBatchTxCount(addresses: txCountCallArgs);
// check and add appropriate addresses
for (int k = 0; k < txCountBatchSize; k++) {
int count = counts["${ID}_$k"]!;
if (count > 0) {
final node = receivingNodes["${ID}_$k"];
// add address to array
addressArray.add(node["address"] as String);
iterationsAddressArray.add(node["address"] as String);
// set current index
returningIndex = index + k;
// reset counter
gapCounter = 0;
// add info to derivations
derivations[node["address"] as String] = {
"pubKey": Format.uint8listToString(
(node["node"] as bip32.BIP32).publicKey),
"wif": (node["node"] as bip32.BIP32).toWIF(),
};
}
// 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(iterationsAddressArray));
}
return {
"addressArray": addressArray,
"index": returningIndex,
"derivations": derivations
};
}
Future<void> getTransactionCacheEarly(List<String> allAddresses) async {
try {
final List<Map<String, dynamic>> allTxHashes =
await _fetchHistory(allAddresses);
for (final txHash in allTxHashes) {
try {
unawaited(cachedElectrumXClient.getTransaction(
txHash: txHash["tx_hash"] as String,
verbose: true,
coin: coin,
));
} catch (e) {
continue;
}
}
} catch (e, s) {
//
}
}
Future<void> _recoverWalletFromBIP32SeedPhrase({ Future<void> _recoverWalletFromBIP32SeedPhrase({
required String mnemonic, required String mnemonic,
int maxUnusedAddressGap = 20, int maxUnusedAddressGap = 20,
@ -364,10 +478,6 @@ class DogecoinWallet extends CoinServiceAPI {
List<String> p2pkhChangeAddressArray = []; List<String> p2pkhChangeAddressArray = [];
int p2pkhChangeIndex = -1; int p2pkhChangeIndex = -1;
// The gap limit will be capped at [maxUnusedAddressGap]
int receivingGapCounter = 0;
int changeGapCounter = 0;
// actual size is 12 due to p2pkh so 12x1 // actual size is 12 due to p2pkh so 12x1
const txCountBatchSize = 12; const txCountBatchSize = 12;
@ -375,143 +485,31 @@ class DogecoinWallet extends CoinServiceAPI {
// receiving addresses // receiving addresses
Logging.instance Logging.instance
.log("checking receiving addresses...", level: LogLevel.Info); .log("checking receiving addresses...", level: LogLevel.Info);
for (int index = 0; Future resultReceive44 = _checkGaps(maxNumberOfIndexesToCheck,
index < maxNumberOfIndexesToCheck && maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 0);
receivingGapCounter < maxUnusedAddressGap;
index += txCountBatchSize) {
Logging.instance.log(
"index: $index, \t receivingGapCounter: $receivingGapCounter",
level: LogLevel.Info);
final receivingP2pkhID = "k_$index";
Map<String, String> txCountCallArgs = {};
final Map<String, dynamic> receivingNodes = {};
for (int j = 0; j < txCountBatchSize; j++) {
// bip44 / P2PKH
final node44 = await compute(
getBip32NodeFromRootWrapper,
Tuple4(
0,
index + j,
root,
DerivePathType.bip44,
),
);
final p2pkhReceiveAddress = P2PKH(
data: PaymentData(pubkey: node44.publicKey),
network: _network)
.data
.address!;
receivingNodes.addAll({
"${receivingP2pkhID}_$j": {
"node": node44,
"address": p2pkhReceiveAddress,
}
});
txCountCallArgs.addAll({
"${receivingP2pkhID}_$j": p2pkhReceiveAddress,
});
}
// get address tx counts
final counts = await _getBatchTxCount(addresses: txCountCallArgs);
// check and add appropriate addresses
for (int k = 0; k < txCountBatchSize; k++) {
int p2pkhTxCount = counts["${receivingP2pkhID}_$k"]!;
if (p2pkhTxCount > 0) {
final node = receivingNodes["${receivingP2pkhID}_$k"];
// add address to array
p2pkhReceiveAddressArray.add(node["address"] as String);
// set current index
p2pkhReceiveIndex = index + k;
// reset counter
receivingGapCounter = 0;
// add info to derivations
p2pkhReceiveDerivations[node["address"] as String] = {
"pubKey": Format.uint8listToString(
(node["node"] as bip32.BIP32).publicKey),
"wif": (node["node"] as bip32.BIP32).toWIF(),
};
}
// increase counter when no tx history found
if (p2pkhTxCount == 0) {
receivingGapCounter++;
}
}
}
Logging.instance Logging.instance
.log("checking change addresses...", level: LogLevel.Info); .log("checking change addresses...", level: LogLevel.Info);
// change addresses // change addresses
for (int index = 0; Future resultChange44 = _checkGaps(maxNumberOfIndexesToCheck,
index < maxNumberOfIndexesToCheck && maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 1);
changeGapCounter < maxUnusedAddressGap;
index += txCountBatchSize) {
Logging.instance.log(
"index: $index, \t changeGapCounter: $changeGapCounter",
level: LogLevel.Info);
final changeP2pkhID = "k_$index";
Map<String, String> args = {};
final Map<String, dynamic> changeNodes = {};
for (int j = 0; j < txCountBatchSize; j++) { await Future.wait([
// bip44 / P2PKH resultReceive44,
final node44 = await compute( resultChange44,
getBip32NodeFromRootWrapper, ]);
Tuple4(
1,
index + j,
root,
DerivePathType.bip44,
),
);
final p2pkhChangeAddress = P2PKH(
data: PaymentData(pubkey: node44.publicKey),
network: _network)
.data
.address!;
changeNodes.addAll({
"${changeP2pkhID}_$j": {
"node": node44,
"address": p2pkhChangeAddress,
}
});
args.addAll({
"${changeP2pkhID}_$j": p2pkhChangeAddress,
});
}
// get address tx counts p2pkhReceiveAddressArray =
final counts = await _getBatchTxCount(addresses: args); (await resultReceive44)['addressArray'] as List<String>;
p2pkhReceiveIndex = (await resultReceive44)['index'] as int;
p2pkhReceiveDerivations = (await resultReceive44)['derivations']
as Map<String, Map<String, String>>;
// check and add appropriate addresses p2pkhChangeAddressArray =
for (int k = 0; k < txCountBatchSize; k++) { (await resultChange44)['addressArray'] as List<String>;
int p2pkhTxCount = counts["${changeP2pkhID}_$k"]!; p2pkhChangeIndex = (await resultChange44)['index'] as int;
if (p2pkhTxCount > 0) { p2pkhChangeDerivations = (await resultChange44)['derivations']
final node = changeNodes["${changeP2pkhID}_$k"]; as Map<String, Map<String, String>>;
// add address to array
p2pkhChangeAddressArray.add(node["address"] as String);
// set current index
p2pkhChangeIndex = index + k;
// reset counter
changeGapCounter = 0;
// add info to derivations
p2pkhChangeDerivations[node["address"] as String] = {
"pubKey": Format.uint8listToString(
(node["node"] as bip32.BIP32).publicKey),
"wif": (node["node"] as bip32.BIP32).toWIF(),
};
}
// increase counter when no tx history found
if (p2pkhTxCount == 0) {
changeGapCounter++;
}
}
}
// save the derivations (if any) // save the derivations (if any)
if (p2pkhReceiveDerivations.isNotEmpty) { if (p2pkhReceiveDerivations.isNotEmpty) {
@ -815,7 +813,7 @@ class DogecoinWallet extends CoinServiceAPI {
refreshMutex = false; refreshMutex = false;
if (shouldAutoSync) { if (shouldAutoSync) {
timer ??= Timer.periodic(const Duration(seconds: 150), (timer) async { timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async {
// chain height check currently broken // chain height check currently broken
// if ((await chainHeight) != (await storedChainHeight)) { // if ((await chainHeight) != (await storedChainHeight)) {
if (await refreshIfThereIsNewData()) { if (await refreshIfThereIsNewData()) {
@ -1496,6 +1494,43 @@ class DogecoinWallet extends CoinServiceAPI {
await _secureStore.write(key: key, value: newReceiveDerivationsString); await _secureStore.write(key: key, value: newReceiveDerivationsString);
} }
Future<List<Map<String, dynamic>>> fastFetch(List<String> allTxHashes) async {
List<Map<String, dynamic>> allTransactions = [];
const futureLimit = 30;
List<Future<Map<String, dynamic>>> transactionFutures = [];
int currentFutureCount = 0;
for (final txHash in allTxHashes) {
Future<Map<String, dynamic>> transactionFuture =
cachedElectrumXClient.getTransaction(
txHash: txHash,
verbose: true,
coin: coin,
);
transactionFutures.add(transactionFuture);
currentFutureCount++;
if (currentFutureCount > futureLimit) {
currentFutureCount = 0;
await Future.wait(transactionFutures);
for (final fTx in transactionFutures) {
final tx = await fTx;
allTransactions.add(tx);
}
}
}
if (currentFutureCount != 0) {
currentFutureCount = 0;
await Future.wait(transactionFutures);
for (final fTx in transactionFutures) {
final tx = await fTx;
allTransactions.add(tx);
}
}
return allTransactions;
}
Future<UtxoData> _fetchUtxoData() async { Future<UtxoData> _fetchUtxoData() async {
final List<String> allAddresses = await _fetchAllOwnAddresses(); final List<String> allAddresses = await _fetchAllOwnAddresses();
@ -1503,7 +1538,7 @@ class DogecoinWallet extends CoinServiceAPI {
final fetchedUtxoList = <List<Map<String, dynamic>>>[]; final fetchedUtxoList = <List<Map<String, dynamic>>>[];
final Map<int, Map<String, List<dynamic>>> batches = {}; final Map<int, Map<String, List<dynamic>>> batches = {};
const batchSizeMax = 10; const batchSizeMax = 100;
int batchNumber = 0; int batchNumber = 0;
for (int i = 0; i < allAddresses.length; i++) { for (int i = 0; i < allAddresses.length; i++) {
if (batches[batchNumber] == null) { if (batches[batchNumber] == null) {
@ -1872,7 +1907,7 @@ class DogecoinWallet extends CoinServiceAPI {
final Map<int, Map<String, List<dynamic>>> batches = {}; final Map<int, Map<String, List<dynamic>>> batches = {};
final Map<String, String> requestIdToAddressMap = {}; final Map<String, String> requestIdToAddressMap = {};
const batchSizeMax = 10; const batchSizeMax = 100;
int batchNumber = 0; int batchNumber = 0;
for (int i = 0; i < allAddresses.length; i++) { for (int i = 0; i < allAddresses.length; i++) {
if (batches[batchNumber] == null) { if (batches[batchNumber] == null) {
@ -1954,6 +1989,11 @@ class DogecoinWallet extends CoinServiceAPI {
} }
} }
List<String> hashes = [];
for (var element in allTxHashes) {
hashes.add(element['tx_hash'] as String);
}
await fastFetch(hashes);
List<Map<String, dynamic>> allTransactions = []; List<Map<String, dynamic>> allTransactions = [];
for (final txHash in allTxHashes) { for (final txHash in allTxHashes) {
@ -1983,6 +2023,16 @@ class DogecoinWallet extends CoinServiceAPI {
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero; Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
final List<Map<String, dynamic>> midSortedArray = []; final List<Map<String, dynamic>> midSortedArray = [];
Set<String> vHashes = {};
for (final txObject in allTransactions) {
for (int i = 0; i < (txObject["vin"] as List).length; i++) {
final input = txObject["vin"]![i] as Map;
final prevTxid = input["txid"] as String;
vHashes.add(prevTxid);
}
}
await fastFetch(vHashes.toList());
for (final txObject in allTransactions) { for (final txObject in allTransactions) {
List<String> sendersArray = []; List<String> sendersArray = [];
List<String> recipientsArray = []; List<String> recipientsArray = [];

View file

@ -43,6 +43,7 @@ import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/prefs.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'package:uuid/uuid.dart';
const DUST_LIMIT = 1000; const DUST_LIMIT = 1000;
const MINIMUM_CONFIRMATIONS = 1; const MINIMUM_CONFIRMATIONS = 1;
@ -145,25 +146,17 @@ Future<void> executeNative(Map<String, dynamic> arguments) async {
final setDataMap = arguments['setDataMap'] as Map; final setDataMap = arguments['setDataMap'] as Map;
final usedSerialNumbers = arguments['usedSerialNumbers'] as List?; final usedSerialNumbers = arguments['usedSerialNumbers'] as List?;
final mnemonic = arguments['mnemonic'] as String; final mnemonic = arguments['mnemonic'] as String;
final transactionData =
arguments['transactionData'] as models.TransactionData;
final currency = arguments['currency'] as String;
final coin = arguments['coin'] as Coin; final coin = arguments['coin'] as Coin;
final network = arguments['network'] as NetworkType?; final network = arguments['network'] as NetworkType?;
final currentPrice = arguments['currentPrice'] as Decimal;
final locale = arguments['locale'] as String;
if (!(usedSerialNumbers == null || network == null)) { if (!(usedSerialNumbers == null || network == null)) {
var restoreData = await isolateRestore( var restoreData = await isolateRestore(
mnemonic, mnemonic,
transactionData, coin,
currency, latestSetId,
coin, setDataMap,
latestSetId, usedSerialNumbers,
setDataMap, network,
usedSerialNumbers, );
network,
currentPrice,
locale);
sendPort.send(restoreData); sendPort.send(restoreData);
return; return;
} }
@ -240,15 +233,11 @@ Future<Map<String, dynamic>> isolateDerive(
Future<Map<String, dynamic>> isolateRestore( Future<Map<String, dynamic>> isolateRestore(
String mnemonic, String mnemonic,
models.TransactionData data,
String currency,
Coin coin, Coin coin,
int _latestSetId, int _latestSetId,
Map<dynamic, dynamic> _setDataMap, Map<dynamic, dynamic> _setDataMap,
List<dynamic> _usedSerialNumbers, List<dynamic> _usedSerialNumbers,
NetworkType network, NetworkType network,
Decimal currentPrice,
String locale,
) async { ) async {
List<int> jindexes = []; List<int> jindexes = [];
List<Map<dynamic, LelantusCoin>> lelantusCoins = []; List<Map<dynamic, LelantusCoin>> lelantusCoins = [];
@ -373,6 +362,20 @@ Future<Map<String, dynamic>> isolateRestore(
result['_lelantus_coins'] = lelantusCoins; result['_lelantus_coins'] = lelantusCoins;
result['mintIndex'] = lastFoundIndex + 1; result['mintIndex'] = lastFoundIndex + 1;
result['jindex'] = jindexes; result['jindex'] = jindexes;
result['spendTxIds'] = spendTxIds;
return result;
}
Future<Map<dynamic, dynamic>> staticProcessRestore(
models.TransactionData data,
Map<dynamic, dynamic> result,
) async {
List<dynamic>? _l = result['_lelantus_coins'] as List?;
final List<Map<dynamic, LelantusCoin>> lelantusCoins = [];
for (var el in _l ?? []) {
lelantusCoins.add({el.keys.first: el.values.first as LelantusCoin});
}
// Edit the receive transactions with the mint fees. // Edit the receive transactions with the mint fees.
Map<String, models.Transaction> editedTransactions = Map<String, models.Transaction> editedTransactions =
@ -437,7 +440,6 @@ Future<Map<String, dynamic>> isolateRestore(
(value.height == -1 && !value.confirmedStatus)); (value.height == -1 && !value.confirmedStatus));
result['newTxMap'] = transactionMap; result['newTxMap'] = transactionMap;
result['spendTxIds'] = spendTxIds;
return result; return result;
} }
@ -2193,7 +2195,7 @@ class FiroWallet extends CoinServiceAPI {
final newTxData = _fetchTransactionData(); final newTxData = _fetchTransactionData();
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.35, walletId)); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.35, walletId));
final FeeObject feeObj = await _getFees(); final feeObj = _getFees();
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.50, walletId)); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.50, walletId));
_utxoData = Future(() => newUtxoData); _utxoData = Future(() => newUtxoData);
@ -2217,9 +2219,6 @@ class FiroWallet extends CoinServiceAPI {
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.95, walletId)); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.95, walletId));
final maxFee = await _fetchMaxFee();
_maxFee = Future(() => maxFee);
var txData = (await _txnData); var txData = (await _txnData);
var lTxData = (await lelantusTransactionData); var lTxData = (await lelantusTransactionData);
await getAllTxsToWatch(txData, lTxData); await getAllTxsToWatch(txData, lTxData);
@ -2236,7 +2235,7 @@ class FiroWallet extends CoinServiceAPI {
refreshMutex = false; refreshMutex = false;
if (isActive || shouldAutoSync) { if (isActive || shouldAutoSync) {
timer ??= Timer.periodic(const Duration(seconds: 150), (timer) async { timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async {
bool shouldNotify = await refreshIfThereIsNewData(); bool shouldNotify = await refreshIfThereIsNewData();
if (shouldNotify) { if (shouldNotify) {
await refresh(); await refresh();
@ -2744,7 +2743,7 @@ class FiroWallet extends CoinServiceAPI {
}; };
} }
Future<void> _refreshLelantusData() async { Future<models.TransactionData> _refreshLelantusData() async {
final List<Map<dynamic, LelantusCoin>> lelantusCoins = getLelantusCoinMap(); final List<Map<dynamic, LelantusCoin>> lelantusCoins = getLelantusCoinMap();
final jindexes = final jindexes =
DB.instance.get<dynamic>(boxName: walletId, key: 'jindex') as List?; DB.instance.get<dynamic>(boxName: walletId, key: 'jindex') as List?;
@ -2826,6 +2825,7 @@ class FiroWallet extends CoinServiceAPI {
_lelantusTransactionData = Future(() => newTxData); _lelantusTransactionData = Future(() => newTxData);
await DB.instance.put<dynamic>( await DB.instance.put<dynamic>(
boxName: walletId, key: 'latest_lelantus_tx_model', value: newTxData); boxName: walletId, key: 'latest_lelantus_tx_model', value: newTxData);
return newTxData;
} }
Future<String> _getMintHex(int amount, int index) async { Future<String> _getMintHex(int amount, int index) async {
@ -3122,24 +3122,44 @@ class FiroWallet extends CoinServiceAPI {
List<String> allAddresses) async { List<String> allAddresses) async {
try { try {
List<Map<String, dynamic>> allTxHashes = []; List<Map<String, dynamic>> allTxHashes = [];
// int latestTxnBlockHeight = 0;
for (final address in allAddresses) { final Map<int, Map<String, List<dynamic>>> batches = {};
final scripthash = AddressUtils.convertToScriptHash(address, _network); final Map<String, String> requestIdToAddressMap = {};
final txs = await electrumXClient.getHistory(scripthash: scripthash); const batchSizeMax = 100;
for (final map in txs) { int batchNumber = 0;
if (!allTxHashes.contains(map)) { for (int i = 0; i < allAddresses.length; i++) {
map['address'] = address; if (batches[batchNumber] == null) {
allTxHashes.add(map); batches[batchNumber] = {};
}
final scripthash =
AddressUtils.convertToScriptHash(allAddresses[i], _network);
final id = const Uuid().v1();
requestIdToAddressMap[id] = allAddresses[i];
batches[batchNumber]!.addAll({
id: [scripthash]
});
if (i % batchSizeMax == batchSizeMax - 1) {
batchNumber++;
}
}
for (int i = 0; i < batches.length; i++) {
final response =
await _electrumXClient.getBatchHistory(args: batches[i]!);
for (final entry in response.entries) {
for (int j = 0; j < entry.value.length; j++) {
entry.value[j]["address"] = requestIdToAddressMap[entry.key];
if (!allTxHashes.contains(entry.value[j])) {
allTxHashes.add(entry.value[j]);
}
} }
} }
} }
return allTxHashes; return allTxHashes;
} catch (e, s) { } catch (e, s) {
Logging.instance.log("Exception caught in _fetchHistory(): $e\n$s", Logging.instance.log("_fetchHistory: $e\n$s", level: LogLevel.Error);
level: LogLevel.Error); rethrow;
return [];
} }
} }
@ -3178,20 +3198,11 @@ class FiroWallet extends CoinServiceAPI {
} }
} }
List<Map<String, dynamic>> allTransactions = []; List<String> hashes = [];
for (var element in allTxHashes) {
for (final txHash in allTxHashes) { hashes.add(element['tx_hash'] as String);
final tx = await cachedElectrumXClient.getTransaction(
txHash: txHash["tx_hash"] as String,
verbose: true,
coin: coin,
);
// delete unused large parts
tx.remove("hex");
tx.remove("lelantusData");
allTransactions.add(tx);
} }
List<Map<String, dynamic>> allTransactions = await fastFetch(hashes);
Logging.instance.log("allTransactions length: ${allTransactions.length}", Logging.instance.log("allTransactions length: ${allTransactions.length}",
level: LogLevel.Info); level: LogLevel.Info);
@ -3441,81 +3452,83 @@ class FiroWallet extends CoinServiceAPI {
} }
Future<UtxoData> _fetchUtxoData() async { Future<UtxoData> _fetchUtxoData() async {
final List<String> allAddresses = []; final List<String> allAddresses = await _fetchAllOwnAddresses();
final receivingAddresses =
DB.instance.get<dynamic>(boxName: walletId, key: 'receivingAddresses')
as List<dynamic>;
final changeAddresses =
DB.instance.get<dynamic>(boxName: walletId, key: 'changeAddresses')
as List<dynamic>;
for (var i = 0; i < receivingAddresses.length; i++) {
if (!allAddresses.contains(receivingAddresses[i])) {
allAddresses.add(receivingAddresses[i] as String);
}
}
for (var i = 0; i < changeAddresses.length; i++) {
if (!allAddresses.contains(changeAddresses[i])) {
allAddresses.add(changeAddresses[i] as String);
}
}
try { try {
final utxoData = <List<Map<String, dynamic>>>[]; final fetchedUtxoList = <List<Map<String, dynamic>>>[];
final Map<int, Map<String, List<dynamic>>> batches = {};
const batchSizeMax = 100;
int batchNumber = 0;
for (int i = 0; i < allAddresses.length; i++) { for (int i = 0; i < allAddresses.length; i++) {
if (batches[batchNumber] == null) {
batches[batchNumber] = {};
}
final scripthash = final scripthash =
AddressUtils.convertToScriptHash(allAddresses[i], _network); AddressUtils.convertToScriptHash(allAddresses[i], _network);
final utxos = await electrumXClient.getUTXOs(scripthash: scripthash); batches[batchNumber]!.addAll({
if (utxos.isNotEmpty) { scripthash: [scripthash]
utxoData.add(utxos); });
if (i % batchSizeMax == batchSizeMax - 1) {
batchNumber++;
} }
} }
Decimal currentPrice = await firoPrice; for (int i = 0; i < batches.length; i++) {
final response =
await _electrumXClient.getBatchUTXOs(args: batches[i]!);
for (final entry in response.entries) {
if (entry.value.isNotEmpty) {
fetchedUtxoList.add(entry.value);
}
}
}
final priceData =
await _priceAPI.getPricesAnd24hChange(baseCurrency: _prefs.currency);
Decimal currentPrice = priceData[coin]?.item1 ?? Decimal.zero;
final List<Map<String, dynamic>> outputArray = []; final List<Map<String, dynamic>> outputArray = [];
int satoshiBalance = 0; int satoshiBalance = 0;
int satoshiBalancePending = 0; int satoshiBalancePending = 0;
for (int i = 0; i < utxoData.length; i++) { for (int i = 0; i < fetchedUtxoList.length; i++) {
for (int j = 0; j < utxoData[i].length; j++) { for (int j = 0; j < fetchedUtxoList[i].length; j++) {
int value = utxoData[i][j]["value"] as int; int value = fetchedUtxoList[i][j]["value"] as int;
satoshiBalance += value; satoshiBalance += value;
final txn = await cachedElectrumXClient.getTransaction( final txn = await cachedElectrumXClient.getTransaction(
txHash: utxoData[i][j]["tx_hash"] as String, txHash: fetchedUtxoList[i][j]["tx_hash"] as String,
verbose: true, verbose: true,
coin: coin, coin: coin,
); );
final Map<String, dynamic> tx = {}; final Map<String, dynamic> utxo = {};
final int confirmations = txn["confirmations"] as int? ?? 0; final int confirmations = txn["confirmations"] as int? ?? 0;
final bool confirmed = confirmations >= MINIMUM_CONFIRMATIONS; final bool confirmed = confirmations >= MINIMUM_CONFIRMATIONS;
if (!confirmed) { if (!confirmed) {
satoshiBalancePending += value; satoshiBalancePending += value;
} }
tx["txid"] = txn["txid"]; utxo["txid"] = txn["txid"];
tx["vout"] = utxoData[i][j]["tx_pos"]; utxo["vout"] = fetchedUtxoList[i][j]["tx_pos"];
tx["value"] = value; utxo["value"] = value;
tx["status"] = <String, dynamic>{}; utxo["status"] = <String, dynamic>{};
tx["status"]["confirmed"] = confirmed; utxo["status"]["confirmed"] = confirmed;
tx["status"]["confirmations"] = confirmations; utxo["status"]["confirmations"] = confirmations;
tx["status"]["confirmed"] = utxo["status"]["confirmed"] =
txn["confirmations"] == null ? false : txn["confirmations"] > 0; txn["confirmations"] == null ? false : txn["confirmations"] > 0;
tx["status"]["block_height"] = txn["height"]; utxo["status"]["block_height"] = fetchedUtxoList[i][j]["height"];
tx["status"]["block_hash"] = txn["blockhash"]; utxo["status"]["block_hash"] = txn["blockhash"];
tx["status"]["block_time"] = txn["blocktime"]; utxo["status"]["block_time"] = txn["blocktime"];
final fiatValue = ((Decimal.fromInt(value) * currentPrice) / final fiatValue = ((Decimal.fromInt(value) * currentPrice) /
Decimal.fromInt(Constants.satsPerCoin)) Decimal.fromInt(Constants.satsPerCoin))
.toDecimal(scaleOnInfinitePrecision: 2); .toDecimal(scaleOnInfinitePrecision: 2);
tx["rawWorth"] = fiatValue; utxo["rawWorth"] = fiatValue;
tx["fiatWorth"] = fiatValue.toString(); utxo["fiatWorth"] = fiatValue.toString();
tx["is_coinbase"] = txn['vin'][0]['coinbase'] != null; utxo["is_coinbase"] = txn['vin'][0]['coinbase'] != null;
outputArray.add(tx); outputArray.add(utxo);
} }
} }
@ -3538,13 +3551,19 @@ class FiroWallet extends CoinServiceAPI {
final dataModel = UtxoData.fromJson(result); final dataModel = UtxoData.fromJson(result);
final List<UtxoObject> allOutputs = dataModel.unspentOutputArray; final List<UtxoObject> allOutputs = dataModel.unspentOutputArray;
// Logging.instance.log('Outputs fetched: $allOutputs'); Logging.instance
.log('Outputs fetched: $allOutputs', level: LogLevel.Info);
await _sortOutputs(allOutputs); await _sortOutputs(allOutputs);
await DB.instance.put<dynamic>( await DB.instance.put<dynamic>(
boxName: walletId, key: 'latest_utxo_model', value: dataModel); boxName: walletId, key: 'latest_utxo_model', value: dataModel);
// await DB.instance.put<dynamic>(
// boxName: walletId,
// key: 'totalBalance',
// value: dataModel.satoshiBalance);
return dataModel; return dataModel;
} catch (e) { } catch (e, s) {
// Logging.instance.log("Output fetch unsuccessful: $e\n$s"); Logging.instance
.log("Output fetch unsuccessful: $e\n$s", level: LogLevel.Error);
final latestTxModel = final latestTxModel =
DB.instance.get<dynamic>(boxName: walletId, key: 'latest_utxo_model') DB.instance.get<dynamic>(boxName: walletId, key: 'latest_utxo_model')
as models.UtxoData?; as models.UtxoData?;
@ -4056,6 +4075,135 @@ class FiroWallet extends CoinServiceAPI {
bool longMutex = false; bool longMutex = false;
Future<Map<int, dynamic>> getSetDataMap(int latestSetId) async {
final Map<int, dynamic> setDataMap = {};
final anonymitySets = await fetchAnonymitySets();
for (int setId = 1; setId <= latestSetId; setId++) {
final setData = anonymitySets
.firstWhere((element) => element["setId"] == setId, orElse: () => {});
if (setData.isNotEmpty) {
setDataMap[setId] = setData;
}
}
return setDataMap;
}
Future<void> _makeDerivations(
String suppliedMnemonic, int maxUnusedAddressGap) async {
List<String> receivingAddressArray = [];
List<String> changeAddressArray = [];
int receivingIndex = -1;
int changeIndex = -1;
// The gap limit will be capped at 20
int receivingGapCounter = 0;
int changeGapCounter = 0;
await fillAddresses(suppliedMnemonic,
numberOfThreads: Platform.numberOfProcessors - isolates.length - 1);
final receiveDerivationsString =
await _secureStore.read(key: "${walletId}_receiveDerivations");
final changeDerivationsString =
await _secureStore.read(key: "${walletId}_changeDerivations");
final receiveDerivations = Map<String, dynamic>.from(
jsonDecode(receiveDerivationsString ?? "{}") as Map);
final changeDerivations = Map<String, dynamic>.from(
jsonDecode(changeDerivationsString ?? "{}") as Map);
// log("rcv: $receiveDerivations");
// log("chg: $changeDerivations");
// Deriving and checking for receiving addresses
for (var i = 0; i < receiveDerivations.length; i++) {
// Break out of loop when receivingGapCounter hits maxUnusedAddressGap
// Same gap limit for change as for receiving, breaks when it hits maxUnusedAddressGap
if (receivingGapCounter >= maxUnusedAddressGap &&
changeGapCounter >= maxUnusedAddressGap) {
break;
}
final receiveDerivation = receiveDerivations["$i"];
final address = receiveDerivation['address'] as String;
final changeDerivation = changeDerivations["$i"];
final _address = changeDerivation['address'] as String;
Future<int>? futureNumTxs;
Future<int>? _futureNumTxs;
if (receivingGapCounter < maxUnusedAddressGap) {
futureNumTxs = _getReceivedTxCount(address: address);
}
if (changeGapCounter < maxUnusedAddressGap) {
_futureNumTxs = _getReceivedTxCount(address: _address);
}
try {
if (futureNumTxs != null) {
int numTxs = await futureNumTxs;
if (numTxs >= 1) {
receivingIndex = i;
receivingAddressArray.add(address);
} else if (numTxs == 0) {
receivingGapCounter += 1;
}
}
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from recoverWalletFromBIP32SeedPhrase(): $e\n$s",
level: LogLevel.Error);
rethrow;
}
try {
if (_futureNumTxs != null) {
int numTxs = await _futureNumTxs;
if (numTxs >= 1) {
changeIndex = i;
changeAddressArray.add(_address);
} else if (numTxs == 0) {
changeGapCounter += 1;
}
}
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from recoverWalletFromBIP32SeedPhrase(): $e\n$s",
level: LogLevel.Error);
rethrow;
}
}
// If restoring a wallet that never received any funds, then set receivingArray manually
// If we didn't do this, it'd store an empty array
if (receivingIndex == -1) {
final String receivingAddress = await _generateAddressForChain(0, 0);
receivingAddressArray.add(receivingAddress);
}
// If restoring a wallet that never sent any funds with change, then set changeArray
// manually. If we didn't do this, it'd store an empty array.
if (changeIndex == -1) {
final String changeAddress = await _generateAddressForChain(1, 0);
changeAddressArray.add(changeAddress);
}
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'receivingAddresses',
value: receivingAddressArray);
await DB.instance.put<dynamic>(
boxName: walletId, key: 'changeAddresses', value: changeAddressArray);
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'receivingIndex',
value: receivingIndex == -1 ? 0 : receivingIndex);
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'changeIndex',
value: changeIndex == -1 ? 0 : changeIndex);
}
/// Recovers wallet from [suppliedMnemonic]. Expects a valid mnemonic. /// Recovers wallet from [suppliedMnemonic]. Expects a valid mnemonic.
Future<void> _recoverWalletFromBIP32SeedPhrase( Future<void> _recoverWalletFromBIP32SeedPhrase(
String suppliedMnemonic, int maxUnusedAddressGap) async { String suppliedMnemonic, int maxUnusedAddressGap) async {
@ -4063,137 +4211,20 @@ class FiroWallet extends CoinServiceAPI {
Logging.instance Logging.instance
.log("PROCESSORS ${Platform.numberOfProcessors}", level: LogLevel.Info); .log("PROCESSORS ${Platform.numberOfProcessors}", level: LogLevel.Info);
try { try {
final Map<int, dynamic> setDataMap = {};
final latestSetId = await getLatestSetId(); final latestSetId = await getLatestSetId();
final anonymitySets = await fetchAnonymitySets(); final setDataMap = getSetDataMap(latestSetId);
for (int setId = 1; setId <= latestSetId; setId++) {
final setData = anonymitySets.firstWhere(
(element) => element["setId"] == setId,
orElse: () => {});
if (setData.isNotEmpty) {
setDataMap[setId] = setData;
}
}
final usedSerialNumbers = getUsedCoinSerials(); final usedSerialNumbers = getUsedCoinSerials();
final makeDerivations =
_makeDerivations(suppliedMnemonic, maxUnusedAddressGap);
List<String> receivingAddressArray = [];
List<String> changeAddressArray = [];
int receivingIndex = -1;
int changeIndex = -1;
// The gap limit will be capped at 20
int receivingGapCounter = 0;
int changeGapCounter = 0;
await fillAddresses(suppliedMnemonic,
numberOfThreads: Platform.numberOfProcessors - isolates.length - 1);
final receiveDerivationsString =
await _secureStore.read(key: "${walletId}_receiveDerivations");
final changeDerivationsString =
await _secureStore.read(key: "${walletId}_changeDerivations");
final receiveDerivations = Map<String, dynamic>.from(
jsonDecode(receiveDerivationsString ?? "{}") as Map);
final changeDerivations = Map<String, dynamic>.from(
jsonDecode(changeDerivationsString ?? "{}") as Map);
// log("rcv: $receiveDerivations");
// log("chg: $changeDerivations");
// Deriving and checking for receiving addresses
for (var i = 0; i < receiveDerivations.length; i++) {
// Break out of loop when receivingGapCounter hits maxUnusedAddressGap
// Same gap limit for change as for receiving, breaks when it hits maxUnusedAddressGap
if (receivingGapCounter >= maxUnusedAddressGap &&
changeGapCounter >= maxUnusedAddressGap) {
break;
}
final receiveDerivation = receiveDerivations["$i"];
final address = receiveDerivation['address'] as String;
final changeDerivation = changeDerivations["$i"];
final _address = changeDerivation['address'] as String;
Future<int>? futureNumTxs;
Future<int>? _futureNumTxs;
if (receivingGapCounter < maxUnusedAddressGap) {
futureNumTxs = _getReceivedTxCount(address: address);
}
if (changeGapCounter < maxUnusedAddressGap) {
_futureNumTxs = _getReceivedTxCount(address: _address);
}
try {
if (futureNumTxs != null) {
int numTxs = await futureNumTxs;
if (numTxs >= 1) {
receivingIndex = i;
receivingAddressArray.add(address);
} else if (numTxs == 0) {
receivingGapCounter += 1;
}
}
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from recoverWalletFromBIP32SeedPhrase(): $e\n$s",
level: LogLevel.Error);
rethrow;
}
try {
if (_futureNumTxs != null) {
int numTxs = await _futureNumTxs;
if (numTxs >= 1) {
changeIndex = i;
changeAddressArray.add(_address);
} else if (numTxs == 0) {
changeGapCounter += 1;
}
}
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from recoverWalletFromBIP32SeedPhrase(): $e\n$s",
level: LogLevel.Error);
rethrow;
}
}
// If restoring a wallet that never received any funds, then set receivingArray manually
// If we didn't do this, it'd store an empty array
if (receivingIndex == -1) {
final String receivingAddress = await _generateAddressForChain(0, 0);
receivingAddressArray.add(receivingAddress);
}
// If restoring a wallet that never sent any funds with change, then set changeArray
// manually. If we didn't do this, it'd store an empty array.
if (changeIndex == -1) {
final String changeAddress = await _generateAddressForChain(1, 0);
changeAddressArray.add(changeAddress);
}
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'receivingAddresses',
value: receivingAddressArray);
await DB.instance.put<dynamic>(
boxName: walletId, key: 'changeAddresses', value: changeAddressArray);
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'receivingIndex',
value: receivingIndex == -1 ? 0 : receivingIndex);
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'changeIndex',
value: changeIndex == -1 ? 0 : changeIndex);
await DB.instance await DB.instance
.put<dynamic>(boxName: walletId, key: "id", value: _walletId); .put<dynamic>(boxName: walletId, key: "id", value: _walletId);
await DB.instance await DB.instance
.put<dynamic>(boxName: walletId, key: "isFavorite", value: false); .put<dynamic>(boxName: walletId, key: "isFavorite", value: false);
await _restore(latestSetId, setDataMap, await usedSerialNumbers); await Future.wait([usedSerialNumbers, setDataMap, makeDerivations]);
await _restore(latestSetId, await setDataMap, await usedSerialNumbers);
longMutex = false; longMutex = false;
} catch (e, s) { } catch (e, s) {
longMutex = false; longMutex = false;
@ -4207,27 +4238,23 @@ class FiroWallet extends CoinServiceAPI {
Future<void> _restore(int latestSetId, Map<dynamic, dynamic> setDataMap, Future<void> _restore(int latestSetId, Map<dynamic, dynamic> setDataMap,
dynamic usedSerialNumbers) async { dynamic usedSerialNumbers) async {
final mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic'); final mnemonic = await _secureStore.read(key: '${_walletId}_mnemonic');
models.TransactionData data = await _txnData; Future data = _txnData;
final String currency = _prefs.currency; final String currency = _prefs.currency;
final Decimal currentPrice = await firoPrice; final Decimal currentPrice = await firoPrice;
final locale = await Devicelocale.currentLocale;
ReceivePort receivePort = await getIsolate({ ReceivePort receivePort = await getIsolate({
"function": "restore", "function": "restore",
"mnemonic": mnemonic, "mnemonic": mnemonic,
"transactionData": data,
"currency": currency,
"coin": coin, "coin": coin,
"latestSetId": latestSetId, "latestSetId": latestSetId,
"setDataMap": setDataMap, "setDataMap": setDataMap,
"usedSerialNumbers": usedSerialNumbers, "usedSerialNumbers": usedSerialNumbers,
"network": _network, "network": _network,
"currentPrice": currentPrice,
"locale": locale,
}); });
var message = await receivePort.first; await Future.wait([data]);
if (message is String) { var result = await receivePort.first;
if (result is String) {
Logging.instance Logging.instance
.log("restore() ->> this is a string", level: LogLevel.Error); .log("restore() ->> this is a string", level: LogLevel.Error);
stop(receivePort); stop(receivePort);
@ -4235,6 +4262,10 @@ class FiroWallet extends CoinServiceAPI {
} }
stop(receivePort); stop(receivePort);
final message = await staticProcessRestore(
(await data) as models.TransactionData,
result as Map<dynamic, dynamic>);
await DB.instance.put<dynamic>( await DB.instance.put<dynamic>(
boxName: walletId, key: 'mintIndex', value: message['mintIndex']); boxName: walletId, key: 'mintIndex', value: message['mintIndex']);
await DB.instance.put<dynamic>( await DB.instance.put<dynamic>(
@ -4274,11 +4305,18 @@ class FiroWallet extends CoinServiceAPI {
final latestSetId = await getLatestSetId(); final latestSetId = await getLatestSetId();
final List<Map<String, dynamic>> sets = []; final List<Map<String, dynamic>> sets = [];
List<Future> anonFutures = [];
for (int i = 1; i <= latestSetId; i++) { for (int i = 1; i <= latestSetId; i++) {
Map<String, dynamic> set = await cachedElectrumXClient.getAnonymitySet( final set = cachedElectrumXClient.getAnonymitySet(
groupId: "$i", groupId: "$i",
coin: coin, coin: coin,
); );
anonFutures.add(set);
}
await Future.wait(anonFutures);
for (int i = 1; i <= latestSetId; i++) {
Map<String, dynamic> set =
(await anonFutures[i - 1]) as Map<String, dynamic>;
set["setId"] = i; set["setId"] = i;
sets.add(set); sets.add(set);
} }
@ -4567,6 +4605,49 @@ class FiroWallet extends CoinServiceAPI {
return available - estimatedFee; return available - estimatedFee;
} }
Future<List<Map<String, dynamic>>> fastFetch(List<String> allTxHashes) async {
List<Map<String, dynamic>> allTransactions = [];
const futureLimit = 30;
List<Future<Map<String, dynamic>>> transactionFutures = [];
int currentFutureCount = 0;
for (final txHash in allTxHashes) {
Future<Map<String, dynamic>> transactionFuture =
cachedElectrumXClient.getTransaction(
txHash: txHash,
verbose: true,
coin: coin,
);
transactionFutures.add(transactionFuture);
currentFutureCount++;
if (currentFutureCount > futureLimit) {
currentFutureCount = 0;
await Future.wait(transactionFutures);
for (final fTx in transactionFutures) {
final tx = await fTx;
// delete unused large parts
tx.remove("hex");
tx.remove("lelantusData");
allTransactions.add(tx);
}
}
}
if (currentFutureCount != 0) {
currentFutureCount = 0;
await Future.wait(transactionFutures);
for (final fTx in transactionFutures) {
final tx = await fTx;
// delete unused large parts
tx.remove("hex");
tx.remove("lelantusData");
allTransactions.add(tx);
}
}
return allTransactions;
}
Future<List<models.Transaction>> getJMintTransactions( Future<List<models.Transaction>> getJMintTransactions(
CachedElectrumX cachedClient, CachedElectrumX cachedClient,
List<String> transactions, List<String> transactions,
@ -4577,14 +4658,12 @@ class FiroWallet extends CoinServiceAPI {
) async { ) async {
try { try {
List<models.Transaction> txs = []; List<models.Transaction> txs = [];
List<Map<String, dynamic>> allTransactions =
await fastFetch(transactions);
for (int i = 0; i < transactions.length; i++) { for (int i = 0; i < allTransactions.length; i++) {
try { try {
final tx = await cachedClient.getTransaction( final tx = allTransactions[i];
txHash: transactions[i],
verbose: true,
coin: coin,
);
tx["confirmed_status"] = tx["confirmed_status"] =
tx["confirmations"] != null && tx["confirmations"] as int > 0; tx["confirmations"] != null && tx["confirmations"] as int > 0;

View file

@ -66,18 +66,15 @@ void main() {
return SampleGetTransactionData.txData7; return SampleGetTransactionData.txData7;
}); });
final result = await isolateRestore( final message = await isolateRestore(
TEST_MNEMONIC, TEST_MNEMONIC,
txData,
"USD",
Coin.firo, Coin.firo,
1, 1,
setData, setData,
usedSerials, usedSerials,
firoNetwork, firoNetwork,
Decimal.ten,
"en_US",
); );
final result = await staticProcessRestore(txData, message);
expect(result, isA<Map<String, dynamic>>()); expect(result, isA<Map<String, dynamic>>());
expect(result["mintIndex"], 8); expect(result["mintIndex"], 8);
@ -88,22 +85,17 @@ void main() {
}); });
test("isolateRestore throws", () async { test("isolateRestore throws", () async {
final txData = TransactionData();
final Map setData = {}; final Map setData = {};
final usedSerials = []; final usedSerials = [];
expect( expect(
() => isolateRestore( () => isolateRestore(
TEST_MNEMONIC, TEST_MNEMONIC,
txData,
"USD",
Coin.firo, Coin.firo,
1, 1,
setData, setData,
usedSerials, usedSerials,
firoNetwork, firoNetwork,
Decimal.ten,
"en_US",
), ),
throwsA(isA<Error>())); throwsA(isA<Error>()));
}); });