mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-23 03:49:22 +00:00
frost multi address functionality
This commit is contained in:
parent
367daad3c5
commit
7c3703ffd7
6 changed files with 516 additions and 103 deletions
|
@ -1 +1 @@
|
||||||
Subproject commit d539de2348bdbb87bac341dcaa6a0755f21d48e2
|
Subproject commit 2a74a97fb0f0e22a5280b22c010b710cdeec33bb
|
|
@ -92,7 +92,6 @@ class _FrostSendViewState extends ConsumerState<FrostSendView> {
|
||||||
|
|
||||||
final txData = await wallet.frostCreateSignConfig(
|
final txData = await wallet.frostCreateSignConfig(
|
||||||
txData: TxData(recipients: recipients),
|
txData: TxData(recipients: recipients),
|
||||||
changeAddress: (await wallet.getCurrentReceivingAddress())!.value,
|
|
||||||
feePerWeight: customFeeRate,
|
feePerWeight: customFeeRate,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -284,7 +284,13 @@ abstract class Frost {
|
||||||
|
|
||||||
static String createSignConfig({
|
static String createSignConfig({
|
||||||
required int network,
|
required int network,
|
||||||
required List<({UTXO utxo, Uint8List scriptPubKey})> inputs,
|
required List<
|
||||||
|
({
|
||||||
|
UTXO utxo,
|
||||||
|
Uint8List scriptPubKey,
|
||||||
|
AddressDerivationData addressDerivationData
|
||||||
|
})>
|
||||||
|
inputs,
|
||||||
required List<({String address, Amount amount, bool isChange})> outputs,
|
required List<({String address, Amount amount, bool isChange})> outputs,
|
||||||
required String changeAddress,
|
required String changeAddress,
|
||||||
required int feePerWeight,
|
required int feePerWeight,
|
||||||
|
@ -299,6 +305,7 @@ abstract class Frost {
|
||||||
vout: e.utxo.vout,
|
vout: e.utxo.vout,
|
||||||
value: e.utxo.value,
|
value: e.utxo.value,
|
||||||
scriptPubKey: e.scriptPubKey,
|
scriptPubKey: e.scriptPubKey,
|
||||||
|
addressDerivationData: e.addressDerivationData,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
|
|
@ -127,9 +127,21 @@ class BitcoinFrost extends FrostCurrency {
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String pubKeyToScriptHash({required Uint8List pubKey}) {
|
Uint8List addressToPubkey({required String address}) {
|
||||||
try {
|
try {
|
||||||
return Bip39HDCurrency.convertBytesToScriptHash(pubKey);
|
final addr = coinlib.Address.fromString(address, networkParams);
|
||||||
|
return addr.program.script.compiled;
|
||||||
|
} catch (e) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String addressToScriptHash({required String address}) {
|
||||||
|
try {
|
||||||
|
return Bip39HDCurrency.convertBytesToScriptHash(
|
||||||
|
addressToPubkey(address: address),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,11 @@ import '../crypto_currency.dart';
|
||||||
abstract class FrostCurrency extends CryptoCurrency {
|
abstract class FrostCurrency extends CryptoCurrency {
|
||||||
FrostCurrency(super.network);
|
FrostCurrency(super.network);
|
||||||
|
|
||||||
String pubKeyToScriptHash({required Uint8List pubKey});
|
// String pubKeyToScriptHash({required Uint8List pubKey});
|
||||||
|
|
||||||
|
String addressToScriptHash({required String address});
|
||||||
|
|
||||||
|
Uint8List addressToPubkey({required String address});
|
||||||
|
|
||||||
Amount get dustLimit;
|
Amount get dustLimit;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:ffi';
|
import 'dart:ffi';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:frostdart/frostdart.dart' as frost;
|
import 'package:frostdart/frostdart.dart' as frost;
|
||||||
import 'package:frostdart/frostdart_bindings_generated.dart';
|
import 'package:frostdart/frostdart_bindings_generated.dart';
|
||||||
|
import 'package:frostdart/util.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
import '../../../electrumx_rpc/cached_electrumx_client.dart';
|
import '../../../electrumx_rpc/cached_electrumx_client.dart';
|
||||||
import '../../../electrumx_rpc/electrumx_client.dart';
|
import '../../../electrumx_rpc/electrumx_client.dart';
|
||||||
import '../../../models/balance.dart';
|
import '../../../models/balance.dart';
|
||||||
|
@ -24,10 +27,13 @@ import '../../../utilities/logger.dart';
|
||||||
import '../../crypto_currency/crypto_currency.dart';
|
import '../../crypto_currency/crypto_currency.dart';
|
||||||
import '../../crypto_currency/intermediate/frost_currency.dart';
|
import '../../crypto_currency/intermediate/frost_currency.dart';
|
||||||
import '../../isar/models/frost_wallet_info.dart';
|
import '../../isar/models/frost_wallet_info.dart';
|
||||||
|
import '../../isar/models/wallet_info.dart';
|
||||||
import '../../models/tx_data.dart';
|
import '../../models/tx_data.dart';
|
||||||
import '../wallet.dart';
|
import '../wallet.dart';
|
||||||
|
import '../wallet_mixin_interfaces/multi_address_interface.dart';
|
||||||
|
|
||||||
class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
|
||||||
|
with MultiAddressInterface {
|
||||||
BitcoinFrostWallet(CryptoCurrencyNetwork network)
|
BitcoinFrostWallet(CryptoCurrencyNetwork network)
|
||||||
: super(BitcoinFrost(network) as T);
|
: super(BitcoinFrost(network) as T);
|
||||||
|
|
||||||
|
@ -77,25 +83,10 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
await mainDB.isar.frostWalletInfo.put(frostWalletInfo);
|
await mainDB.isar.frostWalletInfo.put(frostWalletInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
final keys = frost.deserializeKeys(keys: serializedKeys);
|
final address = await _generateAddress(
|
||||||
|
change: 0,
|
||||||
final addressString = frost.addressForKeys(
|
index: 0,
|
||||||
network: cryptoCurrency.network == CryptoCurrencyNetwork.main
|
serializedKeys: serializedKeys,
|
||||||
? Network.Mainnet
|
|
||||||
: Network.Testnet,
|
|
||||||
keys: keys,
|
|
||||||
);
|
|
||||||
|
|
||||||
final publicKey = frost.scriptPubKeyForKeys(keys: keys);
|
|
||||||
|
|
||||||
final address = Address(
|
|
||||||
walletId: info.walletId,
|
|
||||||
value: addressString,
|
|
||||||
publicKey: publicKey.toUint8ListFromHex,
|
|
||||||
derivationIndex: 0,
|
|
||||||
derivationPath: null,
|
|
||||||
subType: AddressSubType.receiving,
|
|
||||||
type: AddressType.unknown,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await mainDB.putAddresses([address]);
|
await mainDB.putAddresses([address]);
|
||||||
|
@ -110,7 +101,6 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
|
|
||||||
Future<TxData> frostCreateSignConfig({
|
Future<TxData> frostCreateSignConfig({
|
||||||
required TxData txData,
|
required TxData txData,
|
||||||
required String changeAddress,
|
|
||||||
required int feePerWeight,
|
required int feePerWeight,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
|
@ -163,31 +153,41 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final serializedKeys = await getSerializedKeys();
|
|
||||||
final keys = frost.deserializeKeys(keys: serializedKeys!);
|
|
||||||
|
|
||||||
final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main
|
final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main
|
||||||
? Network.Mainnet
|
? Network.Mainnet
|
||||||
: Network.Testnet;
|
: Network.Testnet;
|
||||||
|
|
||||||
final publicKey = frost
|
final List<
|
||||||
.scriptPubKeyForKeys(
|
({
|
||||||
keys: keys,
|
UTXO utxo,
|
||||||
)
|
Uint8List scriptPubKey,
|
||||||
.toUint8ListFromHex;
|
({int account, int index, bool change}) addressDerivationData
|
||||||
|
})> inputs = [];
|
||||||
|
|
||||||
|
for (final utxo in utxosToUse) {
|
||||||
|
final dData = await getDerivationData(
|
||||||
|
utxo.address,
|
||||||
|
);
|
||||||
|
final publicKey = cryptoCurrency.addressToPubkey(
|
||||||
|
address: utxo.address!,
|
||||||
|
);
|
||||||
|
|
||||||
|
inputs.add(
|
||||||
|
(
|
||||||
|
utxo: utxo,
|
||||||
|
scriptPubKey: publicKey,
|
||||||
|
addressDerivationData: dData,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await checkChangeAddressForTransactions();
|
||||||
|
final changeAddress = await getCurrentChangeAddress();
|
||||||
|
|
||||||
final config = Frost.createSignConfig(
|
final config = Frost.createSignConfig(
|
||||||
network: network,
|
network: network,
|
||||||
inputs: utxosToUse
|
inputs: inputs,
|
||||||
.map(
|
|
||||||
(e) => (
|
|
||||||
utxo: e,
|
|
||||||
scriptPubKey: publicKey,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
outputs: txData.recipients!,
|
outputs: txData.recipients!,
|
||||||
changeAddress: (await getCurrentReceivingAddress())!.value,
|
changeAddress: changeAddress!.value,
|
||||||
feePerWeight: feePerWeight,
|
feePerWeight: feePerWeight,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -197,6 +197,44 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<({int account, int index, bool change})> getDerivationData(
|
||||||
|
String? address,
|
||||||
|
) async {
|
||||||
|
if (address == null) {
|
||||||
|
throw Exception("Missing address required for FROST signing");
|
||||||
|
}
|
||||||
|
|
||||||
|
final addr = await mainDB.getAddress(walletId, address);
|
||||||
|
if (addr == null) {
|
||||||
|
throw Exception("Missing address in DB required for FROST signing");
|
||||||
|
}
|
||||||
|
|
||||||
|
final dPath = addr.derivationPath?.value ?? "0/0/0";
|
||||||
|
|
||||||
|
try {
|
||||||
|
final components = dPath.split("/").map((e) => int.parse(e)).toList();
|
||||||
|
|
||||||
|
if (components.length != 3) {
|
||||||
|
throw Exception(
|
||||||
|
"Unexpected derivation data `$components` for FROST signing",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (components[1] != 0 && components[1] != 1) {
|
||||||
|
throw Exception(
|
||||||
|
"${components[1]} must be 1 or 0 for change",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
account: components[0],
|
||||||
|
change: components[1] == 1,
|
||||||
|
index: components[2],
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<
|
Future<
|
||||||
({
|
({
|
||||||
Pointer<TransactionSignMachineWrapper> machinePtr,
|
Pointer<TransactionSignMachineWrapper> machinePtr,
|
||||||
|
@ -324,16 +362,28 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> updateTransactions() async {
|
Future<void> updateTransactions() async {
|
||||||
final myAddress = (await getCurrentReceivingAddress())!;
|
// Get all addresses.
|
||||||
|
final List<Address> allAddressesOld =
|
||||||
|
await _fetchAddressesForElectrumXScan();
|
||||||
|
|
||||||
final scriptHash = cryptoCurrency.pubKeyToScriptHash(
|
// Separate receiving and change addresses.
|
||||||
pubKey: Uint8List.fromList(myAddress.publicKey),
|
final Set<String> receivingAddresses = allAddressesOld
|
||||||
);
|
.where((e) => e.subType == AddressSubType.receiving)
|
||||||
final allTxHashes =
|
.map((e) => e.value)
|
||||||
(await electrumXClient.getHistory(scripthash: scriptHash)).toSet();
|
.toSet();
|
||||||
|
final Set<String> changeAddresses = allAddressesOld
|
||||||
|
.where((e) => e.subType == AddressSubType.change)
|
||||||
|
.map((e) => e.value)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
// Remove duplicates.
|
||||||
|
final allAddressesSet = {...receivingAddresses, ...changeAddresses};
|
||||||
|
|
||||||
final currentHeight = await chainHeight;
|
final currentHeight = await chainHeight;
|
||||||
final coin = info.coin;
|
|
||||||
|
// Fetch history from ElectrumX.
|
||||||
|
final List<Map<String, dynamic>> allTxHashes =
|
||||||
|
await _fetchHistory(allAddressesSet);
|
||||||
|
|
||||||
final List<Map<String, dynamic>> allTransactions = [];
|
final List<Map<String, dynamic>> allTransactions = [];
|
||||||
|
|
||||||
|
@ -350,7 +400,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
final tx = await electrumXCachedClient.getTransaction(
|
final tx = await electrumXCachedClient.getTransaction(
|
||||||
txHash: txHash["tx_hash"] as String,
|
txHash: txHash["tx_hash"] as String,
|
||||||
verbose: true,
|
verbose: true,
|
||||||
cryptoCurrency: coin,
|
cryptoCurrency: cryptoCurrency,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) {
|
if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) {
|
||||||
|
@ -371,6 +421,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
|
|
||||||
// Parse inputs.
|
// Parse inputs.
|
||||||
BigInt amountReceivedInThisWallet = BigInt.zero;
|
BigInt amountReceivedInThisWallet = BigInt.zero;
|
||||||
|
BigInt changeAmountReceivedInThisWallet = BigInt.zero;
|
||||||
final List<InputV2> inputs = [];
|
final List<InputV2> inputs = [];
|
||||||
for (final jsonInput in txData["vin"] as List) {
|
for (final jsonInput in txData["vin"] as List) {
|
||||||
final map = Map<String, dynamic>.from(jsonInput as Map);
|
final map = Map<String, dynamic>.from(jsonInput as Map);
|
||||||
|
@ -421,7 +472,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if input was from this wallet.
|
// Check if input was from this wallet.
|
||||||
if (input.addresses.contains(myAddress.value)) {
|
if (allAddressesSet.intersection(input.addresses.toSet()).isNotEmpty) {
|
||||||
wasSentFromThisWallet = true;
|
wasSentFromThisWallet = true;
|
||||||
input = input.copyWith(walletOwns: true);
|
input = input.copyWith(walletOwns: true);
|
||||||
}
|
}
|
||||||
|
@ -441,10 +492,18 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
);
|
);
|
||||||
|
|
||||||
// If output was to my wallet, add value to amount received.
|
// If output was to my wallet, add value to amount received.
|
||||||
if (output.addresses.contains(myAddress.value)) {
|
if (receivingAddresses
|
||||||
|
.intersection(output.addresses.toSet())
|
||||||
|
.isNotEmpty) {
|
||||||
wasReceivedInThisWallet = true;
|
wasReceivedInThisWallet = true;
|
||||||
amountReceivedInThisWallet += output.value;
|
amountReceivedInThisWallet += output.value;
|
||||||
output = output.copyWith(walletOwns: true);
|
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);
|
outputs.add(output);
|
||||||
|
@ -478,7 +537,8 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
type = TransactionType.outgoing;
|
type = TransactionType.outgoing;
|
||||||
|
|
||||||
if (wasReceivedInThisWallet) {
|
if (wasReceivedInThisWallet) {
|
||||||
if (amountReceivedInThisWallet == totalOut) {
|
if (changeAmountReceivedInThisWallet + amountReceivedInThisWallet ==
|
||||||
|
totalOut) {
|
||||||
// Definitely sent all to self.
|
// Definitely sent all to self.
|
||||||
type = TransactionType.sentToSelf;
|
type = TransactionType.sentToSelf;
|
||||||
} else if (amountReceivedInThisWallet == BigInt.zero) {
|
} else if (amountReceivedInThisWallet == BigInt.zero) {
|
||||||
|
@ -488,6 +548,8 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
} else if (wasReceivedInThisWallet) {
|
} else if (wasReceivedInThisWallet) {
|
||||||
// Only found outputs owned by this wallet.
|
// Only found outputs owned by this wallet.
|
||||||
type = TransactionType.incoming;
|
type = TransactionType.incoming;
|
||||||
|
|
||||||
|
// TODO: [prio=none] Check for special Bitcoin outputs like ordinals.
|
||||||
} else {
|
} else {
|
||||||
Logging.instance.log(
|
Logging.instance.log(
|
||||||
"Unexpected tx found (ignoring it): $txData",
|
"Unexpected tx found (ignoring it): $txData",
|
||||||
|
@ -524,25 +586,10 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
if (address == null) {
|
if (address == null) {
|
||||||
final serializedKeys = await getSerializedKeys();
|
final serializedKeys = await getSerializedKeys();
|
||||||
if (serializedKeys != null) {
|
if (serializedKeys != null) {
|
||||||
final keys = frost.deserializeKeys(keys: serializedKeys);
|
final address = await _generateAddress(
|
||||||
|
change: 0,
|
||||||
final addressString = frost.addressForKeys(
|
index: 0,
|
||||||
network: cryptoCurrency.network == CryptoCurrencyNetwork.main
|
serializedKeys: serializedKeys,
|
||||||
? Network.Mainnet
|
|
||||||
: Network.Testnet,
|
|
||||||
keys: keys,
|
|
||||||
);
|
|
||||||
|
|
||||||
final publicKey = frost.scriptPubKeyForKeys(keys: keys);
|
|
||||||
|
|
||||||
final address = Address(
|
|
||||||
walletId: walletId,
|
|
||||||
value: addressString,
|
|
||||||
publicKey: publicKey.toUint8ListFromHex,
|
|
||||||
derivationIndex: 0,
|
|
||||||
derivationPath: null,
|
|
||||||
subType: AddressSubType.receiving,
|
|
||||||
type: AddressType.frostMS,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await mainDB.updateOrPutAddresses([address]);
|
await mainDB.updateOrPutAddresses([address]);
|
||||||
|
@ -729,30 +776,79 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
await mainDB.deleteWalletBlockchainData(walletId);
|
await mainDB.deleteWalletBlockchainData(walletId);
|
||||||
}
|
}
|
||||||
|
|
||||||
final keys = frost.deserializeKeys(keys: serializedKeys!);
|
|
||||||
await _saveSerializedKeys(serializedKeys!);
|
await _saveSerializedKeys(serializedKeys!);
|
||||||
await _saveMultisigConfig(multisigConfig!);
|
await _saveMultisigConfig(multisigConfig!);
|
||||||
|
|
||||||
final addressString = frost.addressForKeys(
|
const receiveChain = 0;
|
||||||
network: cryptoCurrency.network == CryptoCurrencyNetwork.main
|
const changeChain = 1;
|
||||||
? Network.Mainnet
|
final List<Future<({int index, List<Address> addresses})>>
|
||||||
: Network.Testnet,
|
receiveFutures = [
|
||||||
keys: keys,
|
_checkGapsLinearly(
|
||||||
|
serializedKeys,
|
||||||
|
receiveChain,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
final List<Future<({int index, List<Address> addresses})>>
|
||||||
|
changeFutures = [
|
||||||
|
_checkGapsLinearly(
|
||||||
|
serializedKeys,
|
||||||
|
changeChain,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
// io limitations may require running these linearly instead
|
||||||
|
final futuresResult = await Future.wait([
|
||||||
|
Future.wait(receiveFutures),
|
||||||
|
Future.wait(changeFutures),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final receiveResults = futuresResult[0];
|
||||||
|
final changeResults = futuresResult[1];
|
||||||
|
|
||||||
|
final List<Address> addressesToStore = [];
|
||||||
|
|
||||||
|
int highestReceivingIndexWithHistory = 0;
|
||||||
|
|
||||||
|
for (final tuple in receiveResults) {
|
||||||
|
if (tuple.addresses.isEmpty) {
|
||||||
|
await checkReceivingAddressForTransactions();
|
||||||
|
} else {
|
||||||
|
highestReceivingIndexWithHistory = max(
|
||||||
|
tuple.index,
|
||||||
|
highestReceivingIndexWithHistory,
|
||||||
|
);
|
||||||
|
addressesToStore.addAll(tuple.addresses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int highestChangeIndexWithHistory = 0;
|
||||||
|
// 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.
|
||||||
|
for (final tuple in changeResults) {
|
||||||
|
if (tuple.addresses.isEmpty) {
|
||||||
|
await checkChangeAddressForTransactions();
|
||||||
|
} else {
|
||||||
|
highestChangeIndexWithHistory = max(
|
||||||
|
tuple.index,
|
||||||
|
highestChangeIndexWithHistory,
|
||||||
|
);
|
||||||
|
addressesToStore.addAll(tuple.addresses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove extra addresses to help minimize risk of creating a large gap
|
||||||
|
addressesToStore.removeWhere(
|
||||||
|
(e) =>
|
||||||
|
e.subType == AddressSubType.change &&
|
||||||
|
e.derivationIndex > highestChangeIndexWithHistory,
|
||||||
|
);
|
||||||
|
addressesToStore.removeWhere(
|
||||||
|
(e) =>
|
||||||
|
e.subType == AddressSubType.receiving &&
|
||||||
|
e.derivationIndex > highestReceivingIndexWithHistory,
|
||||||
);
|
);
|
||||||
|
|
||||||
final publicKey = frost.scriptPubKeyForKeys(keys: keys);
|
await mainDB.updateOrPutAddresses(addressesToStore);
|
||||||
|
|
||||||
final address = Address(
|
|
||||||
walletId: walletId,
|
|
||||||
value: addressString,
|
|
||||||
publicKey: publicKey.toUint8ListFromHex,
|
|
||||||
derivationIndex: 0,
|
|
||||||
derivationPath: null,
|
|
||||||
subType: AddressSubType.receiving,
|
|
||||||
type: AddressType.frostMS,
|
|
||||||
);
|
|
||||||
|
|
||||||
await mainDB.updateOrPutAddresses([address]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
GlobalEventBus.instance.fire(
|
GlobalEventBus.instance.fire(
|
||||||
|
@ -868,23 +964,31 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> updateUTXOs() async {
|
Future<bool> updateUTXOs() async {
|
||||||
final address = await getCurrentReceivingAddress();
|
final allAddresses = await _fetchAddressesForElectrumXScan();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final scriptHash = cryptoCurrency.pubKeyToScriptHash(
|
final fetchedUtxoList = <List<Map<String, dynamic>>>[];
|
||||||
pubKey: Uint8List.fromList(address!.publicKey),
|
for (int i = 0; i < allAddresses.length; i++) {
|
||||||
);
|
final scriptHash = cryptoCurrency.addressToScriptHash(
|
||||||
|
address: allAddresses[i].value,
|
||||||
|
);
|
||||||
|
|
||||||
final utxos = await electrumXClient.getUTXOs(scripthash: scriptHash);
|
final utxos = await electrumXClient.getUTXOs(scripthash: scriptHash);
|
||||||
|
if (utxos.isNotEmpty) {
|
||||||
|
fetchedUtxoList.add(utxos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final List<UTXO> outputArray = [];
|
final List<UTXO> outputArray = [];
|
||||||
|
|
||||||
for (int i = 0; i < utxos.length; i++) {
|
for (int i = 0; i < fetchedUtxoList.length; i++) {
|
||||||
final utxo = await _parseUTXO(
|
for (int j = 0; j < fetchedUtxoList[i].length; j++) {
|
||||||
jsonUTXO: utxos[i],
|
final utxo = await _parseUTXO(
|
||||||
);
|
jsonUTXO: fetchedUtxoList[i][j],
|
||||||
|
);
|
||||||
|
|
||||||
outputArray.add(utxo);
|
outputArray.add(utxo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await mainDB.updateUTXOs(walletId, outputArray);
|
return await mainDB.updateUTXOs(walletId, outputArray);
|
||||||
|
@ -1174,4 +1278,291 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
|
||||||
|
|
||||||
return utxo;
|
return utxo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> checkChangeAddressForTransactions() async {
|
||||||
|
try {
|
||||||
|
final currentChange = await getCurrentChangeAddress();
|
||||||
|
|
||||||
|
final bool needsGenerate;
|
||||||
|
if (currentChange == null) {
|
||||||
|
// no addresses in db yet for some reason.
|
||||||
|
// Should not happen at this point...
|
||||||
|
|
||||||
|
needsGenerate = true;
|
||||||
|
} else {
|
||||||
|
final txCount = await _fetchTxCount(address: currentChange);
|
||||||
|
needsGenerate = txCount > 0 || currentChange.derivationIndex < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsGenerate) {
|
||||||
|
await generateNewChangeAddress();
|
||||||
|
|
||||||
|
// TODO: get rid of this? Could cause problems (long loading/infinite loop or something)
|
||||||
|
// keep checking until address with no tx history is set as current
|
||||||
|
await checkChangeAddressForTransactions();
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Exception rethrown from _checkChangeAddressForTransactions"
|
||||||
|
"($cryptoCurrency): $e\n$s",
|
||||||
|
level: LogLevel.Error,
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> checkReceivingAddressForTransactions() async {
|
||||||
|
if (info.otherData[WalletInfoKeys.reuseAddress] == true) {
|
||||||
|
try {
|
||||||
|
throw Exception();
|
||||||
|
} catch (_, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"checkReceivingAddressForTransactions called but reuse address flag set: $s",
|
||||||
|
level: LogLevel.Error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final currentReceiving = await getCurrentReceivingAddress();
|
||||||
|
|
||||||
|
final bool needsGenerate;
|
||||||
|
if (currentReceiving == null) {
|
||||||
|
// no addresses in db yet for some reason.
|
||||||
|
// Should not happen at this point...
|
||||||
|
|
||||||
|
needsGenerate = true;
|
||||||
|
} else {
|
||||||
|
final txCount = await _fetchTxCount(address: currentReceiving);
|
||||||
|
needsGenerate = txCount > 0 || currentReceiving.derivationIndex < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsGenerate) {
|
||||||
|
await generateNewReceivingAddress();
|
||||||
|
|
||||||
|
// TODO: [prio=low] Make sure we scan all addresses but only show one.
|
||||||
|
if (info.otherData[WalletInfoKeys.reuseAddress] != true) {
|
||||||
|
// TODO: get rid of this? Could cause problems (long loading/infinite loop or something)
|
||||||
|
// keep checking until address with no tx history is set as current
|
||||||
|
await checkReceivingAddressForTransactions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Exception rethrown from _checkReceivingAddressForTransactions"
|
||||||
|
"($cryptoCurrency): $e\n$s",
|
||||||
|
level: LogLevel.Error,
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> generateNewChangeAddress() async {
|
||||||
|
final current = await getCurrentChangeAddress();
|
||||||
|
int index = current == null ? 0 : current.derivationIndex + 1;
|
||||||
|
const chain = 1; // change address
|
||||||
|
|
||||||
|
final serializedKeys = (await getSerializedKeys())!;
|
||||||
|
|
||||||
|
Address? address;
|
||||||
|
while (address == null) {
|
||||||
|
try {
|
||||||
|
address = await _generateAddress(
|
||||||
|
change: chain,
|
||||||
|
index: index,
|
||||||
|
serializedKeys: serializedKeys,
|
||||||
|
);
|
||||||
|
} on FrostdartException catch (e) {
|
||||||
|
if (e.errorCode == 72) {
|
||||||
|
// rust doesn't like the addressDerivationData
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mainDB.updateOrPutAddresses([address]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> generateNewReceivingAddress() async {
|
||||||
|
final current = await getCurrentReceivingAddress();
|
||||||
|
int index = current == null ? 0 : current.derivationIndex + 1;
|
||||||
|
const chain = 0; // receiving address
|
||||||
|
|
||||||
|
final serializedKeys = (await getSerializedKeys())!;
|
||||||
|
|
||||||
|
Address? address;
|
||||||
|
while (address == null) {
|
||||||
|
try {
|
||||||
|
address = await _generateAddress(
|
||||||
|
change: chain,
|
||||||
|
index: index,
|
||||||
|
serializedKeys: serializedKeys,
|
||||||
|
);
|
||||||
|
} on FrostdartException catch (e) {
|
||||||
|
if (e.errorCode == 72) {
|
||||||
|
// rust doesn't like the addressDerivationData
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mainDB.updateOrPutAddresses([address]);
|
||||||
|
await info.updateReceivingAddress(
|
||||||
|
newAddress: address.value,
|
||||||
|
isar: mainDB.isar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Can and will often throw unless [index], [change], and [account] are zero.
|
||||||
|
/// Caller MUST handle exception!
|
||||||
|
Future<Address> _generateAddress({
|
||||||
|
int account = 0,
|
||||||
|
required int change,
|
||||||
|
required int index,
|
||||||
|
required String serializedKeys,
|
||||||
|
}) async {
|
||||||
|
final addressDerivationData = (
|
||||||
|
account: account,
|
||||||
|
change: change == 1,
|
||||||
|
index: index,
|
||||||
|
);
|
||||||
|
|
||||||
|
final keys = frost.deserializeKeys(keys: serializedKeys);
|
||||||
|
|
||||||
|
final addressString = frost.addressForKeys(
|
||||||
|
network: cryptoCurrency.network == CryptoCurrencyNetwork.main
|
||||||
|
? Network.Mainnet
|
||||||
|
: Network.Testnet,
|
||||||
|
keys: keys,
|
||||||
|
addressDerivationData: addressDerivationData,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Address(
|
||||||
|
walletId: info.walletId,
|
||||||
|
value: addressString,
|
||||||
|
publicKey: cryptoCurrency.addressToPubkey(address: addressString),
|
||||||
|
derivationIndex: index,
|
||||||
|
derivationPath: DerivationPath()..value = "$account/$change/$index",
|
||||||
|
subType: change == 0
|
||||||
|
? AddressSubType.receiving
|
||||||
|
: change == 1
|
||||||
|
? AddressSubType.change
|
||||||
|
: AddressSubType.unknown,
|
||||||
|
type: AddressType.frostMS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<({List<Address> addresses, int index})> _checkGapsLinearly(
|
||||||
|
String serializedKeys,
|
||||||
|
int chain,
|
||||||
|
) async {
|
||||||
|
final List<Address> addressArray = [];
|
||||||
|
int gapCounter = 0;
|
||||||
|
int index = 0;
|
||||||
|
for (; gapCounter < 20; index++) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"Frost index: $index, \t GapCounter chain=$chain: $gapCounter",
|
||||||
|
level: LogLevel.Info,
|
||||||
|
);
|
||||||
|
|
||||||
|
Address? address;
|
||||||
|
while (address == null) {
|
||||||
|
try {
|
||||||
|
address = await _generateAddress(
|
||||||
|
change: chain,
|
||||||
|
index: index,
|
||||||
|
serializedKeys: serializedKeys,
|
||||||
|
);
|
||||||
|
} on FrostdartException catch (e) {
|
||||||
|
if (e.errorCode == 72) {
|
||||||
|
// rust doesn't like the addressDerivationData
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get address tx count
|
||||||
|
final count = await _fetchTxCount(
|
||||||
|
address: address!,
|
||||||
|
);
|
||||||
|
|
||||||
|
// check and add appropriate addresses
|
||||||
|
if (count > 0) {
|
||||||
|
// add address to array
|
||||||
|
addressArray.add(address!);
|
||||||
|
// reset counter
|
||||||
|
gapCounter = 0;
|
||||||
|
// add info to derivations
|
||||||
|
} else {
|
||||||
|
// increase counter when no tx history found
|
||||||
|
gapCounter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (addresses: addressArray, index: index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> _fetchTxCount({required Address address}) async {
|
||||||
|
final transactions = await electrumXClient.getHistory(
|
||||||
|
scripthash: cryptoCurrency.addressToScriptHash(
|
||||||
|
address: address.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return transactions.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Address>> _fetchAddressesForElectrumXScan() async {
|
||||||
|
final allAddresses = await mainDB
|
||||||
|
.getAddresses(walletId)
|
||||||
|
.filter()
|
||||||
|
.not()
|
||||||
|
.group(
|
||||||
|
(q) => q
|
||||||
|
.typeEqualTo(AddressType.nonWallet)
|
||||||
|
.or()
|
||||||
|
.subTypeEqualTo(AddressSubType.nonWallet),
|
||||||
|
)
|
||||||
|
.findAll();
|
||||||
|
return allAddresses;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, dynamic>>> _fetchHistory(
|
||||||
|
Iterable<String> allAddresses,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final List<Map<String, dynamic>> allTxHashes = [];
|
||||||
|
for (int i = 0; i < allAddresses.length; i++) {
|
||||||
|
final addressString = allAddresses.elementAt(i);
|
||||||
|
final scriptHash = cryptoCurrency.addressToScriptHash(
|
||||||
|
address: addressString,
|
||||||
|
);
|
||||||
|
|
||||||
|
final response = await electrumXClient.getHistory(
|
||||||
|
scripthash: scriptHash,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (int j = 0; j < response.length; j++) {
|
||||||
|
response[j]["address"] = addressString;
|
||||||
|
if (!allTxHashes.contains(response[j])) {
|
||||||
|
allTxHashes.add(response[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allTxHashes;
|
||||||
|
} catch (e, s) {
|
||||||
|
Logging.instance.log(
|
||||||
|
"$runtimeType._fetchHistory: $e\n$s",
|
||||||
|
level: LogLevel.Error,
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue