mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2025-01-10 20:54:33 +00:00
WIP frost wallet logic
This commit is contained in:
parent
85b66fd849
commit
8ae2faa91f
7 changed files with 1889 additions and 79 deletions
613
lib/services/frost.dart
Normal file
613
lib/services/frost.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue