WIP frost wallet logic

This commit is contained in:
julian 2024-01-19 15:42:38 -06:00
parent 85b66fd849
commit 8ae2faa91f
7 changed files with 1889 additions and 79 deletions

613
lib/services/frost.dart Normal file
View file

@ -0,0 +1,613 @@
import 'dart:ffi';
import 'dart:typed_data';
import 'package:frostdart/frostdart.dart';
import 'package:frostdart/frostdart_bindings_generated.dart';
import 'package:frostdart/output.dart';
import 'package:frostdart/util.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/extensions/extensions.dart';
import 'package:stackwallet/utilities/logger.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
abstract class Frost {
//==================== utility ===============================================
static List<String> getParticipants({
required String multisigConfig,
}) {
try {
final numberOfParticipants = multisigParticipants(
multisigConfig: multisigConfig,
);
final List<String> participants = [];
for (int i = 0; i < numberOfParticipants; i++) {
participants.add(
multisigParticipant(
multisigConfig: multisigConfig,
index: i,
),
);
}
return participants;
} catch (e, s) {
Logging.instance.log(
"getParticipants failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static bool validateEncodedMultisigConfig({required String encodedConfig}) {
try {
decodeMultisigConfig(multisigConfig: encodedConfig);
return true;
} catch (e, s) {
Logging.instance.log(
"validateEncodedMultisigConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
return false;
}
}
static int getThreshold({
required String multisigConfig,
}) {
try {
final threshold = multisigThreshold(
multisigConfig: multisigConfig,
);
return threshold;
} catch (e, s) {
Logging.instance.log(
"getThreshold failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
List<({String address, Amount amount})> recipients,
String changeAddress,
int feePerWeight,
List<Output> inputs,
}) extractDataFromSignConfig({
required String signConfig,
required CryptoCurrency coin,
}) {
try {
final network = coin.network == CryptoCurrencyNetwork.test
? Network.Testnet
: Network.Mainnet;
final signConfigPointer = decodedSignConfig(
encodedConfig: signConfig,
network: network,
);
// get various data from config
final feePerWeight =
signFeePerWeight(signConfigPointer: signConfigPointer);
final changeAddress = signChange(signConfigPointer: signConfigPointer);
final recipientsCount = signPayments(
signConfigPointer: signConfigPointer,
);
// get tx recipient info
final List<({String address, Amount amount})> recipients = [];
for (int i = 0; i < recipientsCount; i++) {
final String address = signPaymentAddress(
signConfigPointer: signConfigPointer,
index: i,
);
final int amount = signPaymentAmount(
signConfigPointer: signConfigPointer,
index: i,
);
recipients.add(
(
address: address,
amount: Amount(
rawValue: BigInt.from(amount),
fractionDigits: coin.fractionDigits,
),
),
);
}
// get utxos
final count = signInputs(signConfigPointer: signConfigPointer);
final List<Output> outputs = [];
for (int i = 0; i < count; i++) {
final output = signInput(
signConfig: signConfig,
index: i,
network: network,
);
outputs.add(output);
}
return (
recipients: recipients,
changeAddress: changeAddress,
feePerWeight: feePerWeight,
inputs: outputs,
);
} catch (e, s) {
Logging.instance.log(
"extractDataFromSignConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
//==================== wallet creation =======================================
static String createMultisigConfig({
required String name,
required int threshold,
required List<String> participants,
}) {
try {
final config = newMultisigConfig(
name: name,
threshold: threshold,
participants: participants,
);
return config;
} catch (e, s) {
Logging.instance.log(
"createMultisigConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
String seed,
String commitments,
Pointer<MultisigConfigWithName> multisigConfigWithNamePtr,
Pointer<SecretShareMachineWrapper> secretShareMachineWrapperPtr,
}) startKeyGeneration({
required String multisigConfig,
required String myName,
}) {
try {
final startKeyGenResPtr = startKeyGen(
multisigConfig: multisigConfig,
myName: myName,
language: Language.english,
);
final seed = startKeyGenResPtr.ref.seed.toDartString();
final commitments = startKeyGenResPtr.ref.commitments.toDartString();
final configWithNamePtr = startKeyGenResPtr.ref.config;
final machinePtr = startKeyGenResPtr.ref.machine;
return (
seed: seed,
commitments: commitments,
multisigConfigWithNamePtr: configWithNamePtr,
secretShareMachineWrapperPtr: machinePtr,
);
} catch (e, s) {
Logging.instance.log(
"startKeyGeneration failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
String share,
Pointer<SecretSharesRes> secretSharesResPtr,
}) generateSecretShares({
required Pointer<MultisigConfigWithName> multisigConfigWithNamePtr,
required String mySeed,
required Pointer<SecretShareMachineWrapper> secretShareMachineWrapperPtr,
required List<String> commitments,
}) {
try {
final secretSharesResPtr = getSecretShares(
multisigConfigWithName: multisigConfigWithNamePtr,
seed: mySeed,
language: Language.english,
machine: secretShareMachineWrapperPtr,
commitments: commitments,
);
final share = secretSharesResPtr.ref.shares.toDartString();
return (share: share, secretSharesResPtr: secretSharesResPtr);
} catch (e, s) {
Logging.instance.log(
"generateSecretShares failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
Uint8List multisigId,
String recoveryString,
String serializedKeys,
}) completeKeyGeneration({
required Pointer<MultisigConfigWithName> multisigConfigWithNamePtr,
required Pointer<SecretSharesRes> secretSharesResPtr,
required List<String> shares,
}) {
try {
final keyGenResPtr = completeKeyGen(
multisigConfigWithName: multisigConfigWithNamePtr,
machineAndCommitments: secretSharesResPtr,
shares: shares,
);
final id = Uint8List.fromList(
List<int>.generate(
MULTISIG_ID_LENGTH,
(index) => keyGenResPtr.ref.multisig_id[index],
),
);
final recoveryString = keyGenResPtr.ref.recovery.toDartString();
final serializedKeys = serializeKeys(keys: keyGenResPtr.ref.keys);
return (
multisigId: id,
recoveryString: recoveryString,
serializedKeys: serializedKeys,
);
} catch (e, s) {
Logging.instance.log(
"completeKeyGeneration failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
//=================== transaction creation ===================================
static String createSignConfig({
required int network,
required List<({UTXO utxo, Uint8List scriptPubKey})> inputs,
required List<({String address, Amount amount, bool isChange})> outputs,
required String changeAddress,
required int feePerWeight,
}) {
try {
final signConfig = newSignConfig(
network: network,
outputs: inputs
.map(
(e) => Output(
hash: e.utxo.txid.toUint8ListFromHex,
vout: e.utxo.vout,
value: e.utxo.value,
scriptPubKey: e.scriptPubKey,
),
)
.toList(),
paymentAddresses: outputs.map((e) => e.address).toList(),
paymentAmounts: outputs.map((e) => e.amount.raw.toInt()).toList(),
change: changeAddress,
feePerWeight: feePerWeight,
);
return signConfig;
} catch (e, s) {
Logging.instance.log(
"createSignConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
Pointer<TransactionSignMachineWrapper> machinePtr,
String preprocess,
}) attemptSignConfig({
required int network,
required String config,
required String serializedKeys,
}) {
try {
final keys = deserializeKeys(keys: serializedKeys);
final attemptSignRes = attemptSign(
thresholdKeysWrapperPointer: keys,
network: network,
signConfig: config,
);
return (
preprocess: attemptSignRes.ref.preprocess.toDartString(),
machinePtr: attemptSignRes.ref.machine,
);
} catch (e, s) {
Logging.instance.log(
"attemptSignConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
Pointer<TransactionSignatureMachineWrapper> machinePtr,
String share,
}) continueSigning({
required Pointer<TransactionSignMachineWrapper> machinePtr,
required List<String> preprocesses,
}) {
try {
final continueSignRes = continueSign(
machine: machinePtr,
preprocesses: preprocesses,
);
return (
share: continueSignRes.ref.preprocess.toDartString(),
machinePtr: continueSignRes.ref.machine,
);
} catch (e, s) {
Logging.instance.log(
"continueSigning failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static String completeSigning({
required Pointer<TransactionSignatureMachineWrapper> machinePtr,
required List<String> shares,
}) {
try {
final rawTransaction = completeSign(
machine: machinePtr,
shares: shares,
);
return rawTransaction;
} catch (e, s) {
Logging.instance.log(
"completeSigning failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static Pointer<SignConfig> decodedSignConfig({
required String encodedConfig,
required int network,
}) {
try {
final configPtr =
decodeSignConfig(encodedSignConfig: encodedConfig, network: network);
return configPtr;
} catch (e, s) {
Logging.instance.log(
"decodedSignConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
//========================== resharing =======================================
static String createResharerConfig({
required int newThreshold,
required List<int> resharers,
required List<String> newParticipants,
}) {
try {
final config = newResharerConfig(
newThreshold: newThreshold,
newParticipants: newParticipants,
resharers: resharers,
);
return config;
} catch (e, s) {
Logging.instance.log(
"createResharerConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
String resharerStart,
Pointer<StartResharerRes> machine,
}) beginResharer({
required String serializedKeys,
required String config,
}) {
try {
final result = startResharer(
serializedKeys: serializedKeys,
config: config,
);
return (
resharerStart: result.encoded,
machine: result.machine,
);
} catch (e, s) {
Logging.instance.log(
"beginResharer failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
/// expects [resharerStarts] of length equal to resharers.
static ({
String resharedStart,
Pointer<StartResharedRes> prior,
}) beginReshared({
required String myName,
required String resharerConfig,
required List<String> resharerStarts,
}) {
try {
final result = startReshared(
newMultisigName: 'unused_property',
myName: myName,
resharerConfig: resharerConfig,
resharerStarts: resharerStarts,
);
return (
resharedStart: result.encoded,
prior: result.machine,
);
} catch (e, s) {
Logging.instance.log(
"beginReshared failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
/// expects [encryptionKeysOfResharedTo] of length equal to new participants
static String finishResharer({
required StartResharerRes machine,
required List<String> encryptionKeysOfResharedTo,
}) {
try {
final result = completeResharer(
machine: machine,
encryptionKeysOfResharedTo: encryptionKeysOfResharedTo,
);
return result;
} catch (e, s) {
Logging.instance.log(
"finishResharer failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
/// expects [resharerCompletes] of length equal to resharers
static ({
String multisigConfig,
String serializedKeys,
String resharedId,
}) finishReshared({
required StartResharedRes prior,
required List<String> resharerCompletes,
}) {
try {
final result = completeReshared(
prior: prior,
resharerCompletes: resharerCompletes,
);
return result;
} catch (e, s) {
Logging.instance.log(
"finishReshared failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static Pointer<ResharerConfig> decodedResharerConfig({
required String resharerConfig,
}) {
try {
final config = decodeResharerConfig(resharerConfig: resharerConfig);
return config;
} catch (e, s) {
Logging.instance.log(
"decodedResharerConfig failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
static ({
int newThreshold,
List<int> resharers,
List<String> newParticipants,
}) extractResharerConfigData({
required String resharerConfig,
}) {
try {
final newThreshold = resharerNewThreshold(
resharerConfigPointer: decodedResharerConfig(
resharerConfig: resharerConfig,
),
);
final resharersCount = resharerResharers(
resharerConfigPointer: decodedResharerConfig(
resharerConfig: resharerConfig,
),
);
final List<int> resharers = [];
for (int i = 0; i < resharersCount; i++) {
resharers.add(
resharerResharer(
resharerConfigPointer: decodedResharerConfig(
resharerConfig: resharerConfig,
),
index: i,
),
);
}
final newParticipantsCount = resharerNewParticipants(
resharerConfigPointer: decodedResharerConfig(
resharerConfig: resharerConfig,
),
);
final List<String> newParticipants = [];
for (int i = 0; i < newParticipantsCount; i++) {
newParticipants.add(
resharerNewParticipant(
resharerConfigPointer: decodedResharerConfig(
resharerConfig: resharerConfig,
),
index: i,
),
);
}
return (
newThreshold: newThreshold,
resharers: resharers,
newParticipants: newParticipants,
);
} catch (e, s) {
Logging.instance.log(
"extractResharerConfigData failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
}

View file

@ -1,6 +1,7 @@
import 'dart:typed_data';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
@ -48,6 +49,12 @@ class BitcoinFrost extends FrostCurrency {
}
}
@override
Amount get dustLimit => Amount(
rawValue: BigInt.from(294),
fractionDigits: fractionDigits,
);
@override
String pubKeyToScriptHash({required Uint8List pubKey}) {
try {

View file

@ -1,9 +1,12 @@
import 'dart:typed_data';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/wallets/crypto_currency/crypto_currency.dart';
abstract class FrostCurrency extends CryptoCurrency {
FrostCurrency(super.network);
String pubKeyToScriptHash({required Uint8List pubKey});
Amount get dustLimit;
}

View file

@ -12,27 +12,30 @@ class FrostWalletInfo implements IsarId {
final String walletId;
final List<String> knownSalts;
final List<String> participants;
final String myName;
final int threshold;
FrostWalletInfo({
required this.walletId,
required this.knownSalts,
required this.participants,
required this.myName,
required this.threshold,
});
FrostWalletInfo copyWith({
List<String>? knownSalts,
List<String>? participants,
String? myName,
int? threshold,
}) {
return FrostWalletInfo(
walletId: walletId,
knownSalts: knownSalts ?? this.knownSalts,
);
}
Future<void> updateKnownSalts(
List<String> knownSalts, {
required Isar isar,
}) async {
// await isar.writeTxn(() async {
// await isar.
// })
participants: participants ?? this.participants,
myName: myName ?? this.myName,
threshold: threshold ?? this.threshold,
)..id = id;
}
}

View file

@ -22,8 +22,23 @@ const FrostWalletInfoSchema = CollectionSchema(
name: r'knownSalts',
type: IsarType.stringList,
),
r'walletId': PropertySchema(
r'myName': PropertySchema(
id: 1,
name: r'myName',
type: IsarType.string,
),
r'participants': PropertySchema(
id: 2,
name: r'participants',
type: IsarType.stringList,
),
r'threshold': PropertySchema(
id: 3,
name: r'threshold',
type: IsarType.long,
),
r'walletId': PropertySchema(
id: 4,
name: r'walletId',
type: IsarType.string,
)
@ -69,6 +84,14 @@ int _frostWalletInfoEstimateSize(
bytesCount += value.length * 3;
}
}
bytesCount += 3 + object.myName.length * 3;
bytesCount += 3 + object.participants.length * 3;
{
for (var i = 0; i < object.participants.length; i++) {
final value = object.participants[i];
bytesCount += value.length * 3;
}
}
bytesCount += 3 + object.walletId.length * 3;
return bytesCount;
}
@ -80,7 +103,10 @@ void _frostWalletInfoSerialize(
Map<Type, List<int>> allOffsets,
) {
writer.writeStringList(offsets[0], object.knownSalts);
writer.writeString(offsets[1], object.walletId);
writer.writeString(offsets[1], object.myName);
writer.writeStringList(offsets[2], object.participants);
writer.writeLong(offsets[3], object.threshold);
writer.writeString(offsets[4], object.walletId);
}
FrostWalletInfo _frostWalletInfoDeserialize(
@ -91,7 +117,10 @@ FrostWalletInfo _frostWalletInfoDeserialize(
) {
final object = FrostWalletInfo(
knownSalts: reader.readStringList(offsets[0]) ?? [],
walletId: reader.readString(offsets[1]),
myName: reader.readString(offsets[1]),
participants: reader.readStringList(offsets[2]) ?? [],
threshold: reader.readLong(offsets[3]),
walletId: reader.readString(offsets[4]),
);
object.id = id;
return object;
@ -108,6 +137,12 @@ P _frostWalletInfoDeserializeProp<P>(
return (reader.readStringList(offset) ?? []) as P;
case 1:
return (reader.readString(offset)) as P;
case 2:
return (reader.readStringList(offset) ?? []) as P;
case 3:
return (reader.readLong(offset)) as P;
case 4:
return (reader.readString(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
@ -589,6 +624,423 @@ extension FrostWalletInfoQueryFilter
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
myNameEqualTo(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'myName',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
myNameGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'myName',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
myNameLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'myName',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
myNameBetween(
String lower,
String upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'myName',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
myNameStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'myName',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
myNameEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'myName',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
myNameContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'myName',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
myNameMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'myName',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
myNameIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'myName',
value: '',
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
myNameIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'myName',
value: '',
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsElementEqualTo(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'participants',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsElementGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'participants',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsElementLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'participants',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsElementBetween(
String lower,
String upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'participants',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsElementStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'participants',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsElementEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'participants',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsElementContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'participants',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsElementMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'participants',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsElementIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'participants',
value: '',
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsElementIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'participants',
value: '',
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsLengthEqualTo(int length) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'participants',
length,
true,
length,
true,
);
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'participants',
0,
true,
0,
true,
);
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'participants',
0,
false,
999999,
true,
);
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsLengthLessThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'participants',
0,
true,
length,
include,
);
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsLengthGreaterThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'participants',
length,
include,
999999,
true,
);
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
participantsLengthBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'participants',
lower,
includeLower,
upper,
includeUpper,
);
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
thresholdEqualTo(int value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'threshold',
value: value,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
thresholdGreaterThan(
int value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'threshold',
value: value,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
thresholdLessThan(
int value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'threshold',
value: value,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
thresholdBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'threshold',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterFilterCondition>
walletIdEqualTo(
String value, {
@ -734,6 +1186,33 @@ extension FrostWalletInfoQueryLinks
extension FrostWalletInfoQuerySortBy
on QueryBuilder<FrostWalletInfo, FrostWalletInfo, QSortBy> {
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterSortBy> sortByMyName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'myName', Sort.asc);
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterSortBy>
sortByMyNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'myName', Sort.desc);
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterSortBy>
sortByThreshold() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'threshold', Sort.asc);
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterSortBy>
sortByThresholdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'threshold', Sort.desc);
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterSortBy>
sortByWalletId() {
return QueryBuilder.apply(this, (query) {
@ -763,6 +1242,33 @@ extension FrostWalletInfoQuerySortThenBy
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterSortBy> thenByMyName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'myName', Sort.asc);
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterSortBy>
thenByMyNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'myName', Sort.desc);
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterSortBy>
thenByThreshold() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'threshold', Sort.asc);
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterSortBy>
thenByThresholdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'threshold', Sort.desc);
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QAfterSortBy>
thenByWalletId() {
return QueryBuilder.apply(this, (query) {
@ -787,6 +1293,27 @@ extension FrostWalletInfoQueryWhereDistinct
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QDistinct> distinctByMyName(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'myName', caseSensitive: caseSensitive);
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QDistinct>
distinctByParticipants() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'participants');
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QDistinct>
distinctByThreshold() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'threshold');
});
}
QueryBuilder<FrostWalletInfo, FrostWalletInfo, QDistinct> distinctByWalletId(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
@ -810,6 +1337,25 @@ extension FrostWalletInfoQueryProperty
});
}
QueryBuilder<FrostWalletInfo, String, QQueryOperations> myNameProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'myName');
});
}
QueryBuilder<FrostWalletInfo, List<String>, QQueryOperations>
participantsProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'participants');
});
}
QueryBuilder<FrostWalletInfo, int, QQueryOperations> thresholdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'threshold');
});
}
QueryBuilder<FrostWalletInfo, String, QQueryOperations> walletIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'walletId');

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:ffi';
import 'package:flutter/foundation.dart';
import 'package:frostdart/frostdart.dart' as frost;
@ -8,8 +9,15 @@ import 'package:stackwallet/electrumx_rpc/cached_electrumx_client.dart';
import 'package:stackwallet/electrumx_rpc/electrumx_client.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/address.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/transaction.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/input_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/output_v2.dart';
import 'package:stackwallet/models/isar/models/blockchain_data/v2/transaction_v2.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/frost.dart';
import 'package:stackwallet/utilities/amount/amount.dart';
import 'package:stackwallet/utilities/extensions/extensions.dart';
import 'package:stackwallet/utilities/logger.dart';
@ -19,21 +27,275 @@ import 'package:stackwallet/wallets/crypto_currency/intermediate/private_key_cur
import 'package:stackwallet/wallets/isar/models/frost_wallet_info.dart';
import 'package:stackwallet/wallets/models/tx_data.dart';
import 'package:stackwallet/wallets/wallet/wallet.dart';
import 'package:stackwallet/wallets/wallet/wallet_mixin_interfaces/private_key_interface.dart';
class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
with PrivateKeyInterface {
FrostWalletInfo get frostInfo => throw UnimplementedError();
class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T> {
BitcoinFrostWallet(CryptoCurrencyNetwork network)
: super(BitcoinFrost(network) as T);
FrostWalletInfo get frostInfo => mainDB.isar.frostWalletInfo
.where()
.walletIdEqualTo(walletId)
.findFirstSync()!;
late ElectrumXClient electrumXClient;
late CachedElectrumXClient electrumXCachedClient;
Future<void> initializeNewFrost({
required String mnemonic,
required String multisigConfig,
required String recoveryString,
required String serializedKeys,
required Uint8List multisigId,
required String myName,
required List<String> participants,
required int threshold,
}) async {
Logging.instance.log(
"Generating new FROST wallet.",
level: LogLevel.Info,
);
try {
final salt = frost
.multisigSalt(
multisigConfig: multisigConfig,
)
.toHex;
final FrostWalletInfo frostWalletInfo = FrostWalletInfo(
walletId: info.walletId,
knownSalts: [salt],
participants: participants,
myName: myName,
threshold: threshold,
);
await secureStorageInterface.write(
key: Wallet.mnemonicKey(walletId: info.walletId),
value: mnemonic,
);
await secureStorageInterface.write(
key: Wallet.mnemonicPassphraseKey(walletId: info.walletId),
value: "",
);
await _saveSerializedKeys(serializedKeys);
await _saveRecoveryString(recoveryString);
await _saveMultisigId(multisigId);
await _saveMultisigConfig(multisigConfig);
await mainDB.isar.frostWalletInfo.put(frostWalletInfo);
final keys = frost.deserializeKeys(keys: serializedKeys);
final addressString = frost.addressForKeys(
network: cryptoCurrency.network == CryptoCurrencyNetwork.main
? 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]);
} catch (e, s) {
Logging.instance.log(
"Exception rethrown from initializeNewFrost(): $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
}
Future<TxData> frostCreateSignConfig({
required TxData txData,
required String changeAddress,
required int feePerWeight,
}) async {
try {
if (txData.recipients == null || txData.recipients!.isEmpty) {
throw Exception("No recipients found!");
}
final total = txData.recipients!
.map((e) => e.amount)
.reduce((value, e) => value += e);
final utxos = await mainDB
.getUTXOs(walletId)
.filter()
.isBlockedEqualTo(false)
.findAll();
if (utxos.isEmpty) {
throw Exception("No UTXOs found");
} else {
final currentHeight = await chainHeight;
utxos.removeWhere(
(e) => !e.isConfirmed(
currentHeight,
cryptoCurrency.minConfirms,
),
);
if (utxos.isEmpty) {
throw Exception("No confirmed UTXOs found");
}
}
if (total.raw >
utxos.map((e) => BigInt.from(e.value)).reduce((v, e) => v += e)) {
throw Exception("Insufficient available funds");
}
Amount sum = Amount.zeroWith(
fractionDigits: cryptoCurrency.fractionDigits,
);
final Set<UTXO> utxosToUse = {};
for (final utxo in utxos) {
sum += Amount(
rawValue: BigInt.from(utxo.value),
fractionDigits: cryptoCurrency.fractionDigits,
);
utxosToUse.add(utxo);
if (sum > total) {
break;
}
}
final serializedKeys = await _getSerializedKeys();
final keys = frost.deserializeKeys(keys: serializedKeys!);
final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main
? Network.Mainnet
: Network.Testnet;
final publicKey = frost
.scriptPubKeyForKeys(
keys: keys,
)
.toUint8ListFromHex;
final config = Frost.createSignConfig(
network: network,
inputs: utxosToUse
.map((e) => (
utxo: e,
scriptPubKey: publicKey,
))
.toList(),
outputs: txData.recipients!,
changeAddress: (await getCurrentReceivingAddress())!.value,
feePerWeight: feePerWeight,
);
return txData.copyWith(frostMSConfig: config, utxos: utxosToUse);
} catch (_) {
rethrow;
}
}
Future<
({
Pointer<TransactionSignMachineWrapper> machinePtr,
String preprocess,
})> frostAttemptSignConfig({
required String config,
}) async {
final int network = cryptoCurrency.network == CryptoCurrencyNetwork.main
? Network.Mainnet
: Network.Testnet;
final serializedKeys = await _getSerializedKeys();
return Frost.attemptSignConfig(
network: network,
config: config,
serializedKeys: serializedKeys!,
);
}
Future<void> updateWithResharedData({
required String serializedKeys,
required String multisigConfig,
required bool isNewWallet,
}) async {
await _saveSerializedKeys(serializedKeys);
await _saveMultisigConfig(multisigConfig);
await _updateThreshold(
frost.getThresholdFromKeys(
serializedKeys: serializedKeys,
),
);
final myNameIndex = frost.getParticipantIndexFromKeys(
serializedKeys: serializedKeys,
);
final participants = Frost.getParticipants(
multisigConfig: multisigConfig,
);
final myName = participants[myNameIndex];
await _updateParticipants(participants);
await _updateMyName(myName);
if (isNewWallet) {
await recover(
serializedKeys: serializedKeys,
multisigConfig: multisigConfig,
isRescan: false,
);
}
}
Future<Amount> sweepAllEstimate(int feeRate) async {
int available = 0;
int inputCount = 0;
final height = await chainHeight;
for (final output in (await mainDB.getUTXOs(walletId).findAll())) {
if (!output.isBlocked &&
output.isConfirmed(height, cryptoCurrency.minConfirms)) {
available += output.value;
inputCount++;
}
}
// transaction will only have 1 output minus the fee
final estimatedFee = _roughFeeEstimate(inputCount, 1, feeRate);
return Amount(
rawValue: BigInt.from(available),
fractionDigits: cryptoCurrency.fractionDigits,
) -
estimatedFee;
}
// int _estimateTxFee({required int vSize, required int feeRatePerKB}) {
// return vSize * (feeRatePerKB / 1000).ceil();
// }
Amount _roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) {
return Amount(
rawValue: BigInt.from(
((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() *
(feeRatePerKB / 1000).ceil()),
fractionDigits: cryptoCurrency.fractionDigits,
);
}
// ==================== Overrides ============================================
@override
int get isarTransactionVersion => 2;
BitcoinFrostWallet(CryptoCurrencyNetwork network)
: super(BitcoinFrost(network) as T);
@override
FilterOperation? get changeAddressFilterOperation => FilterGroup.and(
[
@ -62,62 +324,321 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
],
);
// Future<List<Address>> fetchAddressesForElectrumXScan() async {
// final allAddresses = await mainDB
// .getAddresses(walletId)
// .filter()
// .typeEqualTo(AddressType.frostMS)
// .and()
// .group(
// (q) => q
// .subTypeEqualTo(AddressSubType.receiving)
// .or()
// .subTypeEqualTo(AddressSubType.change),
// )
// .findAll();
// return allAddresses;
// }
@override
Future<void> updateTransactions() async {
final myAddress = (await getCurrentReceivingAddress())!;
final scriptHash = cryptoCurrency.pubKeyToScriptHash(
pubKey: Uint8List.fromList(myAddress.publicKey),
);
final allTxHashes =
(await electrumXClient.getHistory(scripthash: scriptHash)).toSet();
final currentHeight = await chainHeight;
final coin = info.coin;
List<Map<String, dynamic>> allTransactions = [];
for (final txHash in allTxHashes) {
final storedTx = await mainDB.isar.transactionV2s
.where()
.walletIdEqualTo(walletId)
.filter()
.txidEqualTo(txHash["tx_hash"] as String)
.findFirst();
if (storedTx == null ||
!storedTx.isConfirmed(currentHeight, cryptoCurrency.minConfirms)) {
final tx = await electrumXCachedClient.getTransaction(
txHash: txHash["tx_hash"] as String,
verbose: true,
coin: coin,
);
if (!_duplicateTxCheck(allTransactions, tx["txid"] as String)) {
tx["height"] = txHash["height"];
allTransactions.add(tx);
}
}
}
// Parse all new txs.
final List<TransactionV2> txns = [];
for (final txData in allTransactions) {
bool wasSentFromThisWallet = false;
// Set to true if any inputs were detected as owned by this wallet.
bool wasReceivedInThisWallet = false;
// Set to true if any outputs were detected as owned by this wallet.
// Parse inputs.
BigInt amountReceivedInThisWallet = BigInt.zero;
final List<InputV2> inputs = [];
for (final jsonInput in txData["vin"] as List) {
final map = Map<String, dynamic>.from(jsonInput as Map);
final List<String> addresses = [];
String valueStringSats = "0";
OutpointV2? outpoint;
final coinbase = map["coinbase"] as String?;
if (coinbase == null) {
// Not a coinbase (ie a typical input).
final txid = map["txid"] as String;
final vout = map["vout"] as int;
final inputTx = await electrumXCachedClient.getTransaction(
txHash: txid,
coin: cryptoCurrency.coin,
);
final prevOutJson = Map<String, dynamic>.from(
(inputTx["vout"] as List).firstWhere((e) => e["n"] == vout)
as Map);
final prevOut = OutputV2.fromElectrumXJson(
prevOutJson,
decimalPlaces: cryptoCurrency.fractionDigits,
isFullAmountNotSats: true,
walletOwns: false, // Doesn't matter here as this is not saved.
);
outpoint = OutpointV2.isarCantDoRequiredInDefaultConstructor(
txid: txid,
vout: vout,
);
valueStringSats = prevOut.valueStringSats;
addresses.addAll(prevOut.addresses);
}
InputV2 input = InputV2.fromElectrumxJson(
json: map,
outpoint: outpoint,
valueStringSats: valueStringSats,
addresses: addresses,
coinbase: coinbase,
// Need addresses before we can know if the wallet owns this input.
walletOwns: false,
);
// Check if input was from this wallet.
if (input.addresses.contains(myAddress.value)) {
wasSentFromThisWallet = true;
input = input.copyWith(walletOwns: true);
}
inputs.add(input);
}
// Parse outputs.
final List<OutputV2> outputs = [];
for (final outputJson in txData["vout"] as List) {
OutputV2 output = OutputV2.fromElectrumXJson(
Map<String, dynamic>.from(outputJson as Map),
decimalPlaces: cryptoCurrency.fractionDigits,
isFullAmountNotSats: true,
// Need addresses before we can know if the wallet owns this input.
walletOwns: false,
);
// If output was to my wallet, add value to amount received.
if (output.addresses.contains(myAddress.value)) {
wasReceivedInThisWallet = true;
amountReceivedInThisWallet += output.value;
output = output.copyWith(walletOwns: true);
}
outputs.add(output);
}
final totalOut = outputs
.map((e) => e.value)
.fold(BigInt.zero, (value, element) => value + element);
TransactionType type;
TransactionSubType subType = TransactionSubType.none;
if (outputs.length > 1 && inputs.isNotEmpty) {
for (int i = 0; i < outputs.length; i++) {
List<String>? scriptChunks = outputs[i].scriptPubKeyAsm?.split(" ");
if (scriptChunks?.length == 2 && scriptChunks?[0] == "OP_RETURN") {
final blindedPaymentCode = scriptChunks![1];
final bytes = blindedPaymentCode.toUint8ListFromHex;
// https://en.bitcoin.it/wiki/BIP_0047#Sending
if (bytes.length == 80 && bytes.first == 1) {
subType = TransactionSubType.bip47Notification;
break;
}
}
}
}
// At least one input was owned by this wallet.
if (wasSentFromThisWallet) {
type = TransactionType.outgoing;
if (wasReceivedInThisWallet) {
if (amountReceivedInThisWallet == totalOut) {
// Definitely sent all to self.
type = TransactionType.sentToSelf;
} else if (amountReceivedInThisWallet == BigInt.zero) {
// Most likely just a typical send, do nothing here yet.
}
}
} else if (wasReceivedInThisWallet) {
// Only found outputs owned by this wallet.
type = TransactionType.incoming;
} else {
Logging.instance.log(
"Unexpected tx found (ignoring it): $txData",
level: LogLevel.Error,
);
continue;
}
final tx = TransactionV2(
walletId: walletId,
blockHash: txData["blockhash"] as String?,
hash: txData["hash"] as String,
txid: txData["txid"] as String,
height: txData["height"] as int?,
version: txData["version"] as int,
timestamp: txData["blocktime"] as int? ??
DateTime.timestamp().millisecondsSinceEpoch ~/ 1000,
inputs: List.unmodifiable(inputs),
outputs: List.unmodifiable(outputs),
type: type,
subType: subType,
otherData: null,
);
txns.add(tx);
}
await mainDB.updateOrPutTransactionV2s(txns);
}
@override
Future<void> updateTransactions() {
// TODO: implement updateTransactions
throw UnimplementedError();
Future<void> checkSaveInitialReceivingAddress() async {
// should not be needed for frost as we explicitly save the address
// on new init and restore
}
int estimateTxFee({required int vSize, required int feeRatePerKB}) {
return vSize * (feeRatePerKB / 1000).ceil();
@override
Future<TxData> confirmSend({required TxData txData}) async {
try {
Logging.instance.log("confirmSend txData: $txData", level: LogLevel.Info);
final hex = txData.raw!;
final txHash = await electrumXClient.broadcastTransaction(rawTx: hex);
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
// mark utxos as used
final usedUTXOs = txData.utxos!.map((e) => e.copyWith(used: true));
await mainDB.putUTXOs(usedUTXOs.toList());
txData = txData.copyWith(
utxos: usedUTXOs.toSet(),
txHash: txHash,
txid: txHash,
);
return txData;
} catch (e, s) {
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
level: LogLevel.Error);
rethrow;
}
}
Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) {
return Amount(
rawValue: BigInt.from(
((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() *
(feeRatePerKB / 1000).ceil()),
@override
Future<Amount> estimateFeeFor(Amount amount, int feeRate) async {
final available = info.cachedBalance.spendable;
if (available == amount) {
return amount - (await sweepAllEstimate(feeRate));
} else if (amount <= Amount.zero || amount > available) {
return _roughFeeEstimate(1, 2, feeRate);
}
Amount runningBalance = Amount(
rawValue: BigInt.zero,
fractionDigits: cryptoCurrency.fractionDigits,
);
int inputCount = 0;
for (final output in (await mainDB.getUTXOs(walletId).findAll())) {
if (!output.isBlocked) {
runningBalance += Amount(
rawValue: BigInt.from(output.value),
fractionDigits: cryptoCurrency.fractionDigits,
);
inputCount++;
if (runningBalance > amount) {
break;
}
}
}
final oneOutPutFee = _roughFeeEstimate(inputCount, 1, feeRate);
final twoOutPutFee = _roughFeeEstimate(inputCount, 2, feeRate);
if (runningBalance - amount > oneOutPutFee) {
if (runningBalance - amount > oneOutPutFee + cryptoCurrency.dustLimit) {
final change = runningBalance - amount - twoOutPutFee;
if (change > cryptoCurrency.dustLimit &&
runningBalance - amount - change == twoOutPutFee) {
return runningBalance - amount - change;
} else {
return runningBalance - amount;
}
} else {
return runningBalance - amount;
}
} else if (runningBalance - amount == oneOutPutFee) {
return oneOutPutFee;
} else {
return twoOutPutFee;
}
}
@override
Future<void> checkSaveInitialReceivingAddress() {
// TODO: implement checkSaveInitialReceivingAddress
throw UnimplementedError();
}
Future<FeeObject> get fees async {
try {
// adjust numbers for different speeds?
const int f = 1, m = 5, s = 20;
@override
Future<TxData> confirmSend({required TxData txData}) {
// TODO: implement confirmSend
throw UnimplementedError();
}
final fast = await electrumXClient.estimateFee(blocks: f);
final medium = await electrumXClient.estimateFee(blocks: m);
final slow = await electrumXClient.estimateFee(blocks: s);
@override
Future<Amount> estimateFeeFor(Amount amount, int feeRate) {
// TODO: implement estimateFeeFor
throw UnimplementedError();
}
final feeObject = FeeObject(
numberOfBlocksFast: f,
numberOfBlocksAverage: m,
numberOfBlocksSlow: s,
fast: Amount.fromDecimal(
fast,
fractionDigits: cryptoCurrency.fractionDigits,
).raw.toInt(),
medium: Amount.fromDecimal(
medium,
fractionDigits: cryptoCurrency.fractionDigits,
).raw.toInt(),
slow: Amount.fromDecimal(
slow,
fractionDigits: cryptoCurrency.fractionDigits,
).raw.toInt(),
);
@override
// TODO: implement fees
Future<FeeObject> get fees => throw UnimplementedError();
Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info);
return feeObject;
} catch (e) {
Logging.instance
.log("Exception rethrown from _getFees(): $e", level: LogLevel.Error);
rethrow;
}
}
@override
Future<TxData> prepareSend({required TxData txData}) {
@ -138,6 +659,16 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
);
}
final coin = info.coin;
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.syncing,
walletId,
coin,
),
);
try {
await refreshMutex.protect(() async {
if (!isRescan) {
@ -146,12 +677,16 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
multisigConfig: multisigConfig,
)
.toHex;
final knownSalts = frostInfo.knownSalts;
final knownSalts = _getKnownSalts();
if (knownSalts.contains(salt)) {
throw Exception("Known frost multisig salt found!");
}
knownSalts.add(salt);
await frostInfo.updateKnownSalts(knownSalts, isar: mainDB.isar);
await _updateKnownSalts(knownSalts);
} else {
// clear cache
await electrumXCachedClient.clearSharedTransactionCache(coin: coin);
await mainDB.deleteWalletBlockchainData(walletId);
}
final keys = frost.deserializeKeys(keys: serializedKeys);
@ -180,12 +715,27 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
await mainDB.updateOrPutAddresses([address]);
});
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.synced,
walletId,
coin,
),
);
unawaited(refresh());
} catch (e, s) {
Logging.instance.log(
"recoverFromSerializedKeys failed: $e\n$s",
level: LogLevel.Fatal,
);
GlobalEventBus.instance.fire(
WalletSyncStatusChangedEvent(
WalletSyncStatus.unableToSync,
walletId,
coin,
),
);
rethrow;
}
}
@ -309,12 +859,15 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
// =================== Secure storage ========================================
Future<String?> get getSerializedKeys async =>
Future<String?> _getSerializedKeys() async =>
await secureStorageInterface.read(
key: "{$walletId}_serializedFROSTKeys",
);
Future<void> _saveSerializedKeys(String keys) async {
final current = await getSerializedKeys;
Future<void> _saveSerializedKeys(
String keys,
) async {
final current = await _getSerializedKeys();
if (current == null) {
// do nothing
@ -334,20 +887,24 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
);
}
Future<String?> get getSerializedKeysPrevGen async =>
Future<String?> _getSerializedKeysPrevGen() async =>
await secureStorageInterface.read(
key: "{$walletId}_serializedFROSTKeysPrevGen",
);
Future<String?> get multisigConfig async => await secureStorageInterface.read(
Future<String?> _multisigConfig() async => await secureStorageInterface.read(
key: "{$walletId}_multisigConfig",
);
Future<String?> get multisigConfigPrevGen async =>
Future<String?> _multisigConfigPrevGen() async =>
await secureStorageInterface.read(
key: "{$walletId}_multisigConfigPrevGen",
);
Future<void> _saveMultisigConfig(String multisigConfig) async {
final current = await this.multisigConfig;
Future<void> _saveMultisigConfig(
String multisigConfig,
) async {
final current = await _multisigConfig();
if (current == null) {
// do nothing
@ -367,7 +924,7 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
);
}
Future<Uint8List?> get multisigId async {
Future<Uint8List?> _multisigId() async {
final id = await secureStorageInterface.read(
key: "{$walletId}_multisigIdFROST",
);
@ -378,21 +935,92 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
}
}
Future<void> saveMultisigId(Uint8List id) async =>
Future<void> _saveMultisigId(
Uint8List id,
) async =>
await secureStorageInterface.write(
key: "{$walletId}_multisigIdFROST",
value: id.toHex,
);
Future<String?> get recoveryString async => await secureStorageInterface.read(
Future<String?> _recoveryString() async => await secureStorageInterface.read(
key: "{$walletId}_recoveryStringFROST",
);
Future<void> saveRecoveryString(String recoveryString) async =>
Future<void> _saveRecoveryString(
String recoveryString,
) async =>
await secureStorageInterface.write(
key: "{$walletId}_recoveryStringFROST",
value: recoveryString,
);
// =================== DB ====================================================
List<String> _getKnownSalts() => mainDB.isar.frostWalletInfo
.where()
.walletIdEqualTo(walletId)
.knownSaltsProperty()
.findFirstSync()!;
Future<void> _updateKnownSalts(List<String> knownSalts) async {
await mainDB.isar.writeTxn(() async {
final info = frostInfo;
await mainDB.isar.frostWalletInfo.delete(info.id);
await mainDB.isar.frostWalletInfo.put(
info.copyWith(knownSalts: knownSalts),
);
});
}
List<String> _getParticipants() => mainDB.isar.frostWalletInfo
.where()
.walletIdEqualTo(walletId)
.participantsProperty()
.findFirstSync()!;
Future<void> _updateParticipants(List<String> participants) async {
await mainDB.isar.writeTxn(() async {
final info = frostInfo;
await mainDB.isar.frostWalletInfo.delete(info.id);
await mainDB.isar.frostWalletInfo.put(
info.copyWith(participants: participants),
);
});
}
int _getThreshold() => mainDB.isar.frostWalletInfo
.where()
.walletIdEqualTo(walletId)
.thresholdProperty()
.findFirstSync()!;
Future<void> _updateThreshold(int threshold) async {
await mainDB.isar.writeTxn(() async {
final info = frostInfo;
await mainDB.isar.frostWalletInfo.delete(info.id);
await mainDB.isar.frostWalletInfo.put(
info.copyWith(threshold: threshold),
);
});
}
String _getMyName() => mainDB.isar.frostWalletInfo
.where()
.walletIdEqualTo(walletId)
.myNameProperty()
.findFirstSync()!;
Future<void> _updateMyName(String myName) async {
await mainDB.isar.writeTxn(() async {
final info = frostInfo;
await mainDB.isar.frostWalletInfo.delete(info.id);
await mainDB.isar.frostWalletInfo.put(
info.copyWith(myName: myName),
);
});
}
// =================== Private ===============================================
Future<ElectrumXNode> _getCurrentElectrumXNode() async {
@ -430,6 +1058,16 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
);
}
bool _duplicateTxCheck(
List<Map<String, dynamic>> allTransactions, String txid) {
for (int i = 0; i < allTransactions.length; i++) {
if (allTransactions[i]["txid"] == txid) {
return true;
}
}
return false;
}
Future<UTXO> _parseUTXO({
required Map<String, dynamic> jsonUTXO,
}) async {
@ -443,12 +1081,12 @@ class BitcoinFrostWallet<T extends FrostCurrency> extends Wallet<T>
final outputs = txn["vout"] as List;
String? scriptPubKey;
// String? scriptPubKey;
String? utxoOwnerAddress;
// get UTXO owner address
for (final output in outputs) {
if (output["n"] == vout) {
scriptPubKey = output["scriptPubKey"]?["hex"] as String?;
// scriptPubKey = output["scriptPubKey"]?["hex"] as String?;
utxoOwnerAddress =
output["scriptPubKey"]?["addresses"]?[0] as String? ??
output["scriptPubKey"]?["address"] as String?;

View file

@ -289,7 +289,7 @@ abstract class Wallet<T extends CryptoCurrency> {
wallet.prefs = prefs;
wallet.nodeService = nodeService;
if (wallet is ElectrumXInterface) {
if (wallet is ElectrumXInterface || wallet is BitcoinFrostWallet) {
// initialize electrumx instance
await wallet.updateNode();
}