feat: fee estimation, review comments

This commit is contained in:
Rafael Saes 2024-12-26 18:55:25 -03:00
parent a07be3d863
commit fb5aa9dc6d
22 changed files with 915 additions and 427 deletions

View file

@ -81,11 +81,13 @@ class BitcoinAddressRecord extends BaseBitcoinAddressRecord {
String? scriptHash,
BasedUtxoNetwork? network,
}) {
if (scriptHash == null && network == null) {
if (scriptHash != null) {
this.scriptHash = scriptHash;
} else if (network != null) {
this.scriptHash = BitcoinAddressUtils.scriptHash(address, network: network);
} else {
throw ArgumentError('either scriptHash or network must be provided');
}
this.scriptHash = scriptHash ?? BitcoinAddressUtils.scriptHash(address, network: network!);
}
factory BitcoinAddressRecord.fromJSON(String jsonSource) {

View file

@ -1,14 +1,10 @@
import 'package:cw_core/output_info.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/output_info.dart';
import 'package:cw_core/unspent_coin_type.dart';
class BitcoinTransactionCredentials {
BitcoinTransactionCredentials(
this.outputs, {
required this.priority,
this.feeRate,
this.coinTypeToSpendFrom = UnspentCoinType.any,
});
BitcoinTransactionCredentials(this.outputs,
{required this.priority, this.feeRate, this.coinTypeToSpendFrom = UnspentCoinType.any});
final List<OutputInfo> outputs;
final TransactionPriority? priority;

View file

@ -1,25 +1,25 @@
import 'package:cw_core/transaction_priority.dart';
class BitcoinTransactionPriority extends TransactionPriority {
const BitcoinTransactionPriority({required super.title, required super.raw});
class BitcoinAPITransactionPriority extends TransactionPriority {
const BitcoinAPITransactionPriority({required super.title, required super.raw});
// Unimportant: the lowest possible, confirms when it confirms no matter how long it takes
static const BitcoinTransactionPriority unimportant =
BitcoinTransactionPriority(title: 'Unimportant', raw: 0);
static const BitcoinAPITransactionPriority unimportant =
BitcoinAPITransactionPriority(title: 'Unimportant', raw: 0);
// Normal: low fee, confirms in a reasonable time, normal because in most cases more than this is not needed, gets you in the next 2-3 blocks (about 1 hour)
static const BitcoinTransactionPriority normal =
BitcoinTransactionPriority(title: 'Normal', raw: 1);
static const BitcoinAPITransactionPriority normal =
BitcoinAPITransactionPriority(title: 'Normal', raw: 1);
// Elevated: medium fee, confirms soon, elevated because it's higher than normal, gets you in the next 1-2 blocks (about 30 mins)
static const BitcoinTransactionPriority elevated =
BitcoinTransactionPriority(title: 'Elevated', raw: 2);
static const BitcoinAPITransactionPriority elevated =
BitcoinAPITransactionPriority(title: 'Elevated', raw: 2);
// Priority: high fee, expected in the next block (about 10 mins).
static const BitcoinTransactionPriority priority =
BitcoinTransactionPriority(title: 'Priority', raw: 3);
static const BitcoinAPITransactionPriority priority =
BitcoinAPITransactionPriority(title: 'Priority', raw: 3);
// Custom: any fee, user defined
static const BitcoinTransactionPriority custom =
BitcoinTransactionPriority(title: 'Custom', raw: 4);
static const BitcoinAPITransactionPriority custom =
BitcoinAPITransactionPriority(title: 'Custom', raw: 4);
static BitcoinTransactionPriority deserialize({required int raw}) {
static BitcoinAPITransactionPriority deserialize({required int raw}) {
switch (raw) {
case 0:
return unimportant;
@ -32,7 +32,7 @@ class BitcoinTransactionPriority extends TransactionPriority {
case 4:
return custom;
default:
throw Exception('Unexpected token: $raw for TransactionPriority deserialize');
throw Exception('Unexpected token: $raw for BitcoinTransactionPriority deserialize');
}
}
@ -41,19 +41,19 @@ class BitcoinTransactionPriority extends TransactionPriority {
var label = '';
switch (this) {
case BitcoinTransactionPriority.unimportant:
case BitcoinAPITransactionPriority.unimportant:
label = 'Unimportant ~24hrs+'; // '${S.current.transaction_priority_slow} ~24hrs';
break;
case BitcoinTransactionPriority.normal:
case BitcoinAPITransactionPriority.normal:
label = 'Normal ~1hr+'; // S.current.transaction_priority_medium;
break;
case BitcoinTransactionPriority.elevated:
case BitcoinAPITransactionPriority.elevated:
label = 'Elevated';
break; // S.current.transaction_priority_fast;
case BitcoinTransactionPriority.priority:
case BitcoinAPITransactionPriority.priority:
label = 'Priority';
break; // S.current.transaction_priority_fast;
case BitcoinTransactionPriority.custom:
case BitcoinAPITransactionPriority.custom:
label = 'Custom';
break;
default:
@ -65,7 +65,7 @@ class BitcoinTransactionPriority extends TransactionPriority {
String labelWithRate(int rate, int? customRate) {
final rateValue = this == custom ? customRate ??= 0 : rate;
return '${toString()} ($rateValue ${units}/byte)';
return '${toString()} ($rateValue ${getUnits(rateValue)}/byte)';
}
}
@ -127,7 +127,7 @@ class ElectrumTransactionPriority extends TransactionPriority {
String labelWithRate(int rate, int? customRate) {
final rateValue = this == custom ? customRate ??= 0 : rate;
return '${toString()} ($rateValue ${units}/byte)';
return '${toString()} ($rateValue ${getUnits(rateValue)}/byte)';
}
}
@ -145,8 +145,8 @@ class BitcoinCashTransactionPriority extends ElectrumTransactionPriority {
String get units => 'satoshi';
}
class BitcoinTransactionPriorities implements TransactionPriorities {
const BitcoinTransactionPriorities({
class BitcoinAPITransactionPriorities implements TransactionPriorities {
const BitcoinAPITransactionPriorities({
required this.unimportant,
required this.normal,
required this.elevated,
@ -163,15 +163,15 @@ class BitcoinTransactionPriorities implements TransactionPriorities {
@override
int operator [](TransactionPriority type) {
switch (type) {
case BitcoinTransactionPriority.unimportant:
case BitcoinAPITransactionPriority.unimportant:
return unimportant;
case BitcoinTransactionPriority.normal:
case BitcoinAPITransactionPriority.normal:
return normal;
case BitcoinTransactionPriority.elevated:
case BitcoinAPITransactionPriority.elevated:
return elevated;
case BitcoinTransactionPriority.priority:
case BitcoinAPITransactionPriority.priority:
return priority;
case BitcoinTransactionPriority.custom:
case BitcoinAPITransactionPriority.custom:
return custom;
default:
throw Exception('Unexpected token: $type for TransactionPriorities operator[]');
@ -182,7 +182,7 @@ class BitcoinTransactionPriorities implements TransactionPriorities {
String labelWithRate(TransactionPriority priorityType, [int? rate]) {
late int rateValue;
if (priorityType == BitcoinTransactionPriority.custom) {
if (priorityType == BitcoinAPITransactionPriority.custom) {
if (rate == null) {
throw Exception('Rate must be provided for custom transaction priority');
}
@ -191,7 +191,7 @@ class BitcoinTransactionPriorities implements TransactionPriorities {
rateValue = this[priorityType];
}
return '${priorityType.toString()} (${rateValue} ${priorityType.units}/byte)';
return '${priorityType.toString()} (${rateValue} ${priorityType.getUnits(rateValue)}/byte)';
}
@override
@ -205,8 +205,8 @@ class BitcoinTransactionPriorities implements TransactionPriorities {
};
}
static BitcoinTransactionPriorities fromJson(Map<String, dynamic> json) {
return BitcoinTransactionPriorities(
static BitcoinAPITransactionPriorities fromJson(Map<String, dynamic> json) {
return BitcoinAPITransactionPriorities(
unimportant: json['unimportant'] as int,
normal: json['normal'] as int,
elevated: json['elevated'] as int,
@ -247,7 +247,8 @@ class ElectrumTransactionPriorities implements TransactionPriorities {
@override
String labelWithRate(TransactionPriority priorityType, [int? rate]) {
return '${priorityType.toString()} (${this[priorityType]} ${priorityType.units}/byte)';
final rateValue = this[priorityType];
return '${priorityType.toString()} ($rateValue ${priorityType.getUnits(rateValue)}/byte)';
}
factory ElectrumTransactionPriorities.fromList(List<int> list) {
@ -286,7 +287,7 @@ class ElectrumTransactionPriorities implements TransactionPriorities {
TransactionPriorities deserializeTransactionPriorities(Map<String, dynamic> json) {
if (json.containsKey('unimportant')) {
return BitcoinTransactionPriorities.fromJson(json);
return BitcoinAPITransactionPriorities.fromJson(json);
} else if (json.containsKey('slow')) {
return ElectrumTransactionPriorities.fromJson(json);
} else {

View file

@ -22,7 +22,6 @@ import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
@ -412,7 +411,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
Future<BtcTransaction> buildHardwareWalletTransaction({
required List<BitcoinBaseOutput> outputs,
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
String? memo,
@ -921,14 +919,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
}
@override
Future<int> calcFee({
int calcFee({
required List<UtxoWithAddress> utxos,
required List<BitcoinBaseOutput> outputs,
String? memo,
required int feeRate,
List<ECPrivateInfo>? inputPrivKeyInfos,
List<Outpoint>? vinOutpoints,
}) async =>
}) =>
feeRate *
BitcoinTransactionBuilder.estimateTransactionSize(
utxos: utxos,
@ -945,7 +943,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
bool paysToSilentPayment = false,
int credentialsAmount = 0,
int? inputsCount,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) {
List<UtxoWithAddress> utxos = [];
List<Outpoint> vinOutpoints = [];
@ -957,6 +954,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
int leftAmount = credentialsAmount;
var availableInputs = unspentCoins.where((utx) {
// TODO: unspent coin isSending not toggled
if (!utx.isSending || utx.isFrozen) {
return false;
}
@ -1074,7 +1072,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
int feeRate, {
String? memo,
bool hasSilentPayment = false,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) async {
final utxoDetails = createUTXOS(sendAll: true, paysToSilentPayment: hasSilentPayment);
@ -1122,19 +1119,23 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
}
@override
Future<EstimatedTxResult> estimateTxForAmount(
EstimatedTxResult estimateTxForAmount(
int credentialsAmount,
List<BitcoinOutput> outputs,
int feeRate, {
List<BitcoinOutput> updatedOutputs = const [],
List<BitcoinOutput>? updatedOutputs,
int? inputsCount,
String? memo,
bool? useUnconfirmed,
bool hasSilentPayment = false,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) async {
bool isFakeTx = false,
}) {
if (updatedOutputs == null) {
updatedOutputs = outputs.map((output) => output).toList();
}
// Attempting to send less than the dust limit
if (isBelowDust(credentialsAmount)) {
if (!isFakeTx && isBelowDust(credentialsAmount)) {
throw BitcoinTransactionNoDustException();
}
@ -1163,13 +1164,14 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
inputsCount: utxoDetails.utxos.length + 1,
memo: memo,
hasSilentPayment: hasSilentPayment,
isFakeTx: isFakeTx,
);
}
throw BitcoinTransactionWrongBalanceException();
}
final changeAddress = await walletAddresses.getChangeAddress(
final changeAddress = walletAddresses.getChangeAddress(
inputs: utxoDetails.availableInputs,
outputs: updatedOutputs,
);
@ -1194,7 +1196,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
// for taproot addresses, but if more inputs are needed to make up for fees,
// the silent payment outputs need to be recalculated for the new inputs
var temp = outputs.map((output) => output).toList();
int fee = await calcFee(
int fee = calcFee(
utxos: utxoDetails.utxos,
// Always take only not updated bitcoin outputs here so for every estimation
// the SP outputs are re-generated to the proper taproot addresses
@ -1216,7 +1218,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
final lastOutput = updatedOutputs.last;
final amountLeftForChange = amountLeftForChangeAndFee - fee;
if (isBelowDust(amountLeftForChange)) {
if (!isFakeTx && isBelowDust(amountLeftForChange)) {
// If has change that is lower than dust, will end up with tx rejected by network rules
// so remove the change amount
updatedOutputs.removeLast();
@ -1233,6 +1235,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
memo: memo,
useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
hasSilentPayment: hasSilentPayment,
isFakeTx: isFakeTx,
);
} else {
throw BitcoinTransactionWrongBalanceException();
@ -1289,7 +1292,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
final hasMultiDestination = transactionCredentials.outputs.length > 1;
final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll;
final memo = transactionCredentials.outputs.first.memo;
final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom;
int credentialsAmount = 0;
bool hasSilentPayment = false;
@ -1353,7 +1355,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
feeRateInt,
memo: memo,
hasSilentPayment: hasSilentPayment,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
} else {
estimatedTx = await estimateTxForAmount(
@ -1363,7 +1364,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
updatedOutputs: updatedOutputs,
memo: memo,
hasSilentPayment: hasSilentPayment,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
}
@ -1373,7 +1373,6 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
outputs: updatedOutputs,
publicKeys: estimatedTx.publicKeys,
fee: BigInt.from(estimatedTx.fee),
network: network,
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
enableRBF: true,

View file

@ -35,7 +35,6 @@ import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_keys_file.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:ledger_flutter_plus/ledger_flutter_plus.dart' as ledger;
@ -201,9 +200,6 @@ abstract class ElectrumWalletBase
return Bip32Slip10Secp256k1.fromExtendedKey(xpub!, getKeyNetVersion(network));
}
int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
inputsCount * 68 + outputsCounts * 34 + 10;
static Bip32KeyNetVersions? getKeyNetVersion(BasedUtxoNetwork network) {
switch (network) {
case LitecoinNetwork.mainnet:
@ -244,19 +240,15 @@ abstract class ElectrumWalletBase
@observable
SyncStatus syncStatus;
List<String> get addressesSet => walletAddresses.allAddresses
.where((element) => element.addressType != SegwitAddresType.mweb)
.map((addr) => addr.address)
.toList();
List<String> get addressesSet =>
walletAddresses.allAddresses.map((addr) => addr.address).toList();
List<String> get scriptHashes => walletAddresses.addressesByReceiveType
.where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress)
.map((addr) => (addr as BitcoinAddressRecord).scriptHash)
.toList();
List<String> get publicScriptHashes => walletAddresses.allAddresses
.where((addr) => !addr.isChange)
.where((addr) => RegexUtils.addressTypeFromStr(addr.address, network) is! MwebAddress)
.map((addr) => addr.scriptHash)
.toList();
@ -298,8 +290,8 @@ abstract class ElectrumWalletBase
TransactionPriorities? feeRates;
int feeRate(TransactionPriority priority) {
if (priority is ElectrumTransactionPriority && feeRates is BitcoinTransactionPriorities) {
final rates = feeRates as BitcoinTransactionPriorities;
if (priority is ElectrumTransactionPriority && feeRates is BitcoinAPITransactionPriorities) {
final rates = feeRates as BitcoinAPITransactionPriorities;
switch (priority) {
case ElectrumTransactionPriority.slow:
@ -446,7 +438,6 @@ abstract class ElectrumWalletBase
required bool sendAll,
int credentialsAmount = 0,
int? inputsCount,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) {
List<UtxoWithAddress> utxos = [];
List<Outpoint> vinOutpoints = [];
@ -461,21 +452,10 @@ abstract class ElectrumWalletBase
return false;
}
switch (coinTypeToSpendFrom) {
case UnspentCoinType.mweb:
return utx.bitcoinAddressRecord.addressType == SegwitAddresType.mweb;
case UnspentCoinType.nonMweb:
return utx.bitcoinAddressRecord.addressType != SegwitAddresType.mweb;
case UnspentCoinType.any:
return true;
}
return true;
}).toList();
final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList();
// sort the unconfirmed coins so that mweb coins are first:
availableInputs
.sort((a, b) => a.bitcoinAddressRecord.addressType == SegwitAddresType.mweb ? -1 : 1);
for (int i = 0; i < availableInputs.length; i++) {
final utx = availableInputs[i];
if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0;
@ -563,9 +543,8 @@ abstract class ElectrumWalletBase
List<BitcoinOutput> outputs,
int feeRate, {
String? memo,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) async {
final utxoDetails = createUTXOS(sendAll: true, coinTypeToSpendFrom: coinTypeToSpendFrom);
final utxoDetails = createUTXOS(sendAll: true);
int fee = await calcFee(
utxos: utxoDetails.utxos,
@ -607,17 +586,17 @@ abstract class ElectrumWalletBase
);
}
Future<EstimatedTxResult> estimateTxForAmount(
EstimatedTxResult estimateTxForAmount(
int credentialsAmount,
List<BitcoinOutput> outputs,
int feeRate, {
int? inputsCount,
String? memo,
bool? useUnconfirmed,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) async {
bool isFakeTx = false,
}) {
// Attempting to send less than the dust limit
if (isBelowDust(credentialsAmount)) {
if (!isFakeTx && isBelowDust(credentialsAmount)) {
throw BitcoinTransactionNoDustException();
}
@ -625,7 +604,6 @@ abstract class ElectrumWalletBase
sendAll: false,
credentialsAmount: credentialsAmount,
inputsCount: inputsCount,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
final spendingAllCoins = utxoDetails.availableInputs.length == utxoDetails.utxos.length;
@ -644,14 +622,14 @@ abstract class ElectrumWalletBase
feeRate,
inputsCount: utxoDetails.utxos.length + 1,
memo: memo,
coinTypeToSpendFrom: coinTypeToSpendFrom,
isFakeTx: isFakeTx,
);
}
throw BitcoinTransactionWrongBalanceException();
}
final changeAddress = await walletAddresses.getChangeAddress(
final changeAddress = walletAddresses.getChangeAddress(
inputs: utxoDetails.availableInputs,
outputs: outputs,
);
@ -666,7 +644,7 @@ abstract class ElectrumWalletBase
utxoDetails.publicKeys[address.pubKeyHash()] =
PublicKeyWithDerivationPath('', changeDerivationPath);
int fee = await calcFee(
int fee = calcFee(
utxos: utxoDetails.utxos,
outputs: outputs,
memo: memo,
@ -681,7 +659,7 @@ abstract class ElectrumWalletBase
final lastOutput = outputs.last;
final amountLeftForChange = amountLeftForChangeAndFee - fee;
if (isBelowDust(amountLeftForChange)) {
if (!isFakeTx && isBelowDust(amountLeftForChange)) {
// If has change that is lower than dust, will end up with tx rejected by network rules
// so remove the change amount
outputs.removeLast();
@ -696,7 +674,7 @@ abstract class ElectrumWalletBase
inputsCount: utxoDetails.utxos.length + 1,
memo: memo,
useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
coinTypeToSpendFrom: coinTypeToSpendFrom,
isFakeTx: isFakeTx,
);
} else {
throw BitcoinTransactionWrongBalanceException();
@ -736,12 +714,12 @@ abstract class ElectrumWalletBase
}
}
Future<int> calcFee({
int calcFee({
required List<UtxoWithAddress> utxos,
required List<BitcoinBaseOutput> outputs,
String? memo,
required int feeRate,
}) async =>
}) =>
feeRate *
BitcoinTransactionBuilder.estimateTransactionSize(
utxos: utxos,
@ -750,81 +728,94 @@ abstract class ElectrumWalletBase
memo: memo,
);
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
try {
final outputs = <BitcoinOutput>[];
final transactionCredentials = credentials as BitcoinTransactionCredentials;
final hasMultiDestination = transactionCredentials.outputs.length > 1;
final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll;
final memo = transactionCredentials.outputs.first.memo;
final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom;
CreateTxData getCreateTxDataFromCredentials(Object credentials) {
final outputs = <BitcoinOutput>[];
final transactionCredentials = credentials as BitcoinTransactionCredentials;
final hasMultiDestination = transactionCredentials.outputs.length > 1;
final sendAll = !hasMultiDestination && transactionCredentials.outputs.first.sendAll;
final memo = transactionCredentials.outputs.first.memo;
int credentialsAmount = 0;
int credentialsAmount = 0;
for (final out in transactionCredentials.outputs) {
final outputAmount = out.formattedCryptoAmount!;
for (final out in transactionCredentials.outputs) {
final outputAmount = out.formattedCryptoAmount!;
if (!sendAll && isBelowDust(outputAmount)) {
throw BitcoinTransactionNoDustException();
}
if (!sendAll && isBelowDust(outputAmount)) {
throw BitcoinTransactionNoDustException();
}
if (hasMultiDestination) {
if (out.sendAll) {
throw BitcoinTransactionWrongBalanceException();
}
}
credentialsAmount += outputAmount;
final address = RegexUtils.addressTypeFromStr(
out.isParsedAddress ? out.extractedAddress! : out.address,
network,
);
if (sendAll) {
// The value will be changed after estimating the Tx size and deducting the fee from the total to be sent
outputs.add(BitcoinOutput(
address: address,
value: BigInt.from(0),
));
} else {
outputs.add(BitcoinOutput(
address: address,
value: BigInt.from(outputAmount),
));
if (hasMultiDestination) {
if (out.sendAll) {
throw BitcoinTransactionWrongBalanceException();
}
}
final feeRateInt = transactionCredentials.feeRate != null
? transactionCredentials.feeRate!
: feeRate(transactionCredentials.priority!);
credentialsAmount += outputAmount;
final address = RegexUtils.addressTypeFromStr(
out.isParsedAddress ? out.extractedAddress! : out.address,
network,
);
if (sendAll) {
outputs.add(
BitcoinOutput(
address: address,
// Send all: The value of the single existing output will be updated
// after estimating the Tx size and deducting the fee from the total to be sent
value: BigInt.from(0),
),
);
} else {
outputs.add(
BitcoinOutput(
address: address,
value: BigInt.from(outputAmount),
),
);
}
}
final feeRateInt = transactionCredentials.feeRate != null
? transactionCredentials.feeRate!
: feeRate(transactionCredentials.priority!);
return CreateTxData(
sendAll: sendAll,
amount: credentialsAmount,
outputs: outputs,
feeRate: feeRateInt,
memo: memo,
);
}
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
try {
final data = getCreateTxDataFromCredentials(credentials);
EstimatedTxResult estimatedTx;
if (sendAll) {
if (data.sendAll) {
estimatedTx = await estimateSendAllTx(
outputs,
feeRateInt,
memo: memo,
coinTypeToSpendFrom: coinTypeToSpendFrom,
data.outputs,
data.feeRate,
memo: data.memo,
);
} else {
estimatedTx = await estimateTxForAmount(
credentialsAmount,
outputs,
feeRateInt,
memo: memo,
coinTypeToSpendFrom: coinTypeToSpendFrom,
data.amount,
data.outputs,
data.feeRate,
memo: data.memo,
);
}
if (walletInfo.isHardwareWallet) {
final transaction = await buildHardwareWalletTransaction(
utxos: estimatedTx.utxos,
outputs: outputs,
outputs: data.outputs,
publicKeys: estimatedTx.publicKeys,
fee: BigInt.from(estimatedTx.fee),
network: network,
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
enableRBF: true,
@ -836,7 +827,7 @@ abstract class ElectrumWalletBase
sendWorker: sendWorker,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
feeRate: feeRateInt.toString(),
feeRate: data.feeRate.toString(),
hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot
@ -851,7 +842,7 @@ abstract class ElectrumWalletBase
if (network is BitcoinCashNetwork) {
txb = ForkedTransactionBuilder(
utxos: estimatedTx.utxos,
outputs: outputs,
outputs: data.outputs,
fee: BigInt.from(estimatedTx.fee),
network: network,
memo: estimatedTx.memo,
@ -861,7 +852,7 @@ abstract class ElectrumWalletBase
} else {
txb = BitcoinTransactionBuilder(
utxos: estimatedTx.utxos,
outputs: outputs,
outputs: data.outputs,
fee: BigInt.from(estimatedTx.fee),
network: network,
memo: estimatedTx.memo,
@ -912,7 +903,7 @@ abstract class ElectrumWalletBase
sendWorker: sendWorker,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
feeRate: feeRateInt.toString(),
feeRate: data.feeRate.toString(),
hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: hasTaprootInputs,
@ -936,11 +927,10 @@ abstract class ElectrumWalletBase
Future<BtcTransaction> buildHardwareWalletTransaction({
required List<BitcoinBaseOutput> outputs,
required BigInt fee,
required BasedUtxoNetwork network,
required List<UtxoWithAddress> utxos,
required Map<String, PublicKeyWithDerivationPath> publicKeys,
String? memo,
bool enableRBF = false,
bool enableRBF = true,
BitcoinOrdering inputOrdering = BitcoinOrdering.bip69,
BitcoinOrdering outputOrdering = BitcoinOrdering.bip69,
}) async =>
@ -961,62 +951,120 @@ abstract class ElectrumWalletBase
'unspents': unspentCoins.map((e) => e.toJson()).toList(),
});
int feeAmountForPriority(TransactionPriority priority, int inputsCount, int outputsCount,
{int? size}) =>
feeRate(priority) * (size ?? estimatedTransactionSize(inputsCount, outputsCount));
int estimatedTransactionSize({
required List<BitcoinAddressType> inputTypes,
required List<BitcoinAddressType> outputTypes,
String? memo,
bool enableRBF = true,
}) =>
BitcoinTransactionBuilder.estimateTransactionSizeFromTypes(
inputTypes: inputTypes,
outputTypes: outputTypes,
network: network,
memo: memo,
enableRBF: enableRBF,
);
int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount, {int? size}) =>
feeRate * (size ?? estimatedTransactionSize(inputsCount, outputsCount));
int feeAmountForPriority(
TransactionPriority priority, {
required List<BitcoinAddressType> inputTypes,
required List<BitcoinAddressType> outputTypes,
String? memo,
bool enableRBF = true,
}) =>
feeRate(priority) *
estimatedTransactionSize(
inputTypes: inputTypes,
outputTypes: outputTypes,
memo: memo,
enableRBF: enableRBF,
);
int feeAmountWithFeeRate(
int feeRate, {
required List<BitcoinAddressType> inputTypes,
required List<BitcoinAddressType> outputTypes,
String? memo,
bool enableRBF = true,
}) =>
feeRate *
estimatedTransactionSize(
inputTypes: inputTypes,
outputTypes: outputTypes,
memo: memo,
enableRBF: enableRBF,
);
@override
int calculateEstimatedFee(TransactionPriority? priority, int? amount,
{int? outputsCount, int? size}) {
if (priority is BitcoinTransactionPriority) {
return calculateEstimatedFeeWithFeeRate(
feeRate(priority),
amount,
outputsCount: outputsCount,
size: size,
);
}
return 0;
int estimatedFeeForOutputsWithPriority({
required TransactionPriority priority,
List<String> outputAddresses = const [],
String? memo,
bool enableRBF = true,
}) {
return estimatedFeeForOutputsWithFeeRate(
feeRate: feeRate(priority),
outputAddresses: outputAddresses,
memo: memo,
enableRBF: enableRBF,
);
}
int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) {
if (size != null) {
return feeAmountWithFeeRate(feeRate, 0, 0, size: size);
}
int estimatedFeeForOutputsWithFeeRate({
required int feeRate,
required List<String> outputAddresses,
String? memo,
bool enableRBF = true,
}) {
final fakePublicKey = ECPrivate.random().getPublic();
final fakeOutputs = <BitcoinOutput>[];
final outputTypes =
outputAddresses.map((e) => BitcoinAddressUtils.addressTypeFromStr(e, network)).toList();
int inputsCount = 0;
if (amount != null) {
int totalValue = 0;
for (final input in unspentCoins) {
if (totalValue >= amount) {
for (final outputType in outputTypes) {
late BitcoinBaseAddress address;
switch (outputType) {
case P2pkhAddressType.p2pkh:
address = fakePublicKey.toP2pkhAddress();
break;
}
if (input.isSending) {
totalValue += input.value;
inputsCount += 1;
}
case P2shAddressType.p2pkInP2sh:
address = fakePublicKey.toP2pkhInP2sh();
break;
case SegwitAddresType.p2wpkh:
address = fakePublicKey.toP2wpkhAddress();
break;
case P2shAddressType.p2pkhInP2sh:
address = fakePublicKey.toP2pkhInP2sh();
break;
case SegwitAddresType.p2wsh:
address = fakePublicKey.toP2wshAddress();
break;
case SegwitAddresType.p2tr:
address = fakePublicKey.toTaprootAddress();
break;
default:
throw const FormatException('Invalid output type');
}
if (totalValue < amount) return 0;
} else {
for (final input in unspentCoins) {
if (input.isSending) {
inputsCount += 1;
}
}
fakeOutputs.add(BitcoinOutput(address: address, value: BigInt.from(0)));
}
// If send all, then we have no change value
final _outputsCount = outputsCount ?? (amount != null ? 2 : 1);
final estimatedFakeTx = estimateTxForAmount(
0,
fakeOutputs,
feeRate,
memo: memo,
isFakeTx: true,
);
final inputTypes = estimatedFakeTx.utxos.map((e) => e.ownerDetails.address.type).toList();
return feeAmountWithFeeRate(feeRate, inputsCount, _outputsCount);
return feeAmountWithFeeRate(
feeRate,
inputTypes: inputTypes,
outputTypes: outputTypes,
memo: memo,
enableRBF: enableRBF,
);
}
@override
@ -1175,8 +1223,6 @@ abstract class ElectrumWalletBase
unspentCoinsInfo.values.where((record) => record.walletId == id);
for (final element in currentWalletUnspentCoins) {
if (RegexUtils.addressTypeFromStr(element.address, network) is MwebAddress) continue;
final existUnspentCoins = unspentCoins.where((coin) => element == coin);
if (existUnspentCoins.isEmpty) {
@ -1976,3 +2022,19 @@ class BitcoinUnspentCoins extends ObservableSet<BitcoinUnspent> {
}).toList();
}
}
class CreateTxData {
final int amount;
final int feeRate;
final List<BitcoinOutput> outputs;
final bool sendAll;
final String? memo;
CreateTxData({
required this.amount,
required this.feeRate,
required this.outputs,
required this.sendAll,
required this.memo,
});
}

View file

@ -3,7 +3,6 @@ import 'dart:io' show Platform;
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:blockchain_utils/blockchain_utils.dart';
import 'package:cw_bitcoin/bitcoin_address_record.dart';
import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_bitcoin/bitcoin_unspent.dart';
import 'package:cw_core/wallet_addresses.dart';
@ -174,11 +173,11 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
@action
Future<BitcoinAddressRecord> getChangeAddress({
BitcoinAddressRecord getChangeAddress({
List<BitcoinUnspent>? inputs,
List<BitcoinOutput>? outputs,
bool isPegIn = false,
}) async {
}) {
updateChangeAddresses();
final address = changeAddresses.firstWhere(

View file

@ -476,7 +476,7 @@ class ElectrumWorker {
try {
final recommendedFees = await ApiProvider.fromMempool(
_network!,
baseUrl: "http://mempool.cakewallet.com:8999/api",
baseUrl: "http://mempool.cakewallet.com:8999/api/v1",
).getRecommendedFeeRate();
final unimportantFee = recommendedFees.economyFee!.satoshis;
@ -498,7 +498,7 @@ class ElectrumWorker {
_sendResponse(
ElectrumWorkerGetFeesResponse(
result: BitcoinTransactionPriorities(
result: BitcoinAPITransactionPriorities(
unimportant: unimportantFee,
normal: normalFee,
elevated: elevatedFee,

View file

@ -7,9 +7,11 @@ import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_bitcoin/exceptions.dart';
// import 'package:cw_bitcoin/electrum_wallet_addresses.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/mweb_utxo.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/node.dart';
import 'package:cw_mweb/mwebd.pbgrpc.dart';
@ -518,8 +520,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
// reset coin balances and txCount to 0:
unspentCoins.forEach((coin) {
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
coin.bitcoinAddressRecord.balance = 0;
coin.bitcoinAddressRecord.balance = 0;
coin.bitcoinAddressRecord.txCount = 0;
});
@ -930,8 +931,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
coin.isFrozen = coinInfo.isFrozen;
coin.isSending = coinInfo.isSending;
coin.note = coinInfo.note;
if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
coin.bitcoinAddressRecord.balance += coinInfo.value;
coin.bitcoinAddressRecord.balance += coinInfo.value;
} else {
super.addCoinInfo(coin);
}
@ -1024,7 +1024,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
// coin.isFrozen = coinInfo.isFrozen;
// coin.isSending = coinInfo.isSending;
// coin.note = coinInfo.note;
// if (coin.bitcoinAddressRecord is! BitcoinSilentPaymentAddressRecord)
// coin.bitcoinAddressRecord.balance += coinInfo.value;
// } else {
// super.addCoinInfo(coin);
@ -1075,7 +1074,304 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
}
@override
Future<int> calcFee({
TxCreateUtxoDetails createUTXOS({
required bool sendAll,
int credentialsAmount = 0,
int? inputsCount,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) {
List<UtxoWithAddress> utxos = [];
List<Outpoint> vinOutpoints = [];
List<ECPrivateInfo> inputPrivKeyInfos = [];
final publicKeys = <String, PublicKeyWithDerivationPath>{};
int allInputsAmount = 0;
bool spendsUnconfirmedTX = false;
int leftAmount = credentialsAmount;
var availableInputs = unspentCoins.where((utx) {
if (!utx.isSending || utx.isFrozen) {
return false;
}
switch (coinTypeToSpendFrom) {
case UnspentCoinType.mweb:
return utx.bitcoinAddressRecord.addressType == SegwitAddresType.mweb;
case UnspentCoinType.nonMweb:
return utx.bitcoinAddressRecord.addressType != SegwitAddresType.mweb;
case UnspentCoinType.any:
return true;
}
}).toList();
final unconfirmedCoins = availableInputs.where((utx) => utx.confirmations == 0).toList();
// sort the unconfirmed coins so that mweb coins are first:
availableInputs
.sort((a, b) => a.bitcoinAddressRecord.addressType == SegwitAddresType.mweb ? -1 : 1);
for (int i = 0; i < availableInputs.length; i++) {
final utx = availableInputs[i];
if (!spendsUnconfirmedTX) spendsUnconfirmedTX = utx.confirmations == 0;
allInputsAmount += utx.value;
leftAmount = leftAmount - utx.value;
final address = RegexUtils.addressTypeFromStr(utx.address, network);
ECPrivate? privkey;
if (!isHardwareWallet) {
final addressRecord = (utx.bitcoinAddressRecord as BitcoinAddressRecord);
final path = addressRecord.derivationInfo.derivationPath
.addElem(Bip32KeyIndex(
BitcoinAddressUtils.getAccountFromChange(addressRecord.isChange),
))
.addElem(Bip32KeyIndex(addressRecord.index));
privkey = ECPrivate.fromBip32(bip32: bip32.derive(path));
}
vinOutpoints.add(Outpoint(txid: utx.hash, index: utx.vout));
String pubKeyHex;
if (privkey != null) {
inputPrivKeyInfos.add(ECPrivateInfo(privkey, address.type == SegwitAddresType.p2tr));
pubKeyHex = privkey.getPublic().toHex();
} else {
pubKeyHex = walletAddresses.hdWallet
.childKey(Bip32KeyIndex(utx.bitcoinAddressRecord.index))
.publicKey
.toHex();
}
if (utx.bitcoinAddressRecord is BitcoinAddressRecord) {
final derivationPath = (utx.bitcoinAddressRecord as BitcoinAddressRecord)
.derivationInfo
.derivationPath
.toString();
publicKeys[address.pubKeyHash()] = PublicKeyWithDerivationPath(pubKeyHex, derivationPath);
}
utxos.add(
UtxoWithAddress(
utxo: BitcoinUtxo(
txHash: utx.hash,
value: BigInt.from(utx.value),
vout: utx.vout,
scriptType: BitcoinAddressUtils.getScriptType(address),
),
ownerDetails: UtxoAddressDetails(
publicKey: pubKeyHex,
address: address,
),
),
);
// sendAll continues for all inputs
if (!sendAll) {
bool amountIsAcquired = leftAmount <= 0;
if ((inputsCount == null && amountIsAcquired) || inputsCount == i + 1) {
break;
}
}
}
if (utxos.isEmpty) {
throw BitcoinTransactionNoInputsException();
}
return TxCreateUtxoDetails(
availableInputs: availableInputs,
unconfirmedCoins: unconfirmedCoins,
utxos: utxos,
vinOutpoints: vinOutpoints,
inputPrivKeyInfos: inputPrivKeyInfos,
publicKeys: publicKeys,
allInputsAmount: allInputsAmount,
spendsUnconfirmedTX: spendsUnconfirmedTX,
);
}
Future<EstimatedTxResult> estimateSendAllTxMweb(
List<BitcoinOutput> outputs,
int feeRate, {
String? memo,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) async {
final utxoDetails = createUTXOS(sendAll: true, coinTypeToSpendFrom: coinTypeToSpendFrom);
int fee = await calcFeeMweb(
utxos: utxoDetails.utxos,
outputs: outputs,
memo: memo,
feeRate: feeRate,
);
if (fee == 0) {
throw BitcoinTransactionNoFeeException();
}
// Here, when sending all, the output amount equals to the input value - fee to fully spend every input on the transaction and have no amount left for change
int amount = utxoDetails.allInputsAmount - fee;
if (amount <= 0) {
throw BitcoinTransactionWrongBalanceException(amount: utxoDetails.allInputsAmount + fee);
}
// Attempting to send less than the dust limit
if (isBelowDust(amount)) {
throw BitcoinTransactionNoDustException();
}
if (outputs.length == 1) {
outputs[0] = BitcoinOutput(address: outputs.last.address, value: BigInt.from(amount));
}
return EstimatedTxResult(
utxos: utxoDetails.utxos,
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
publicKeys: utxoDetails.publicKeys,
fee: fee,
amount: amount,
isSendAll: true,
hasChange: false,
memo: memo,
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
);
}
Future<EstimatedTxResult> estimateTxForAmountMweb(
int credentialsAmount,
List<BitcoinOutput> outputs,
int feeRate, {
int? inputsCount,
String? memo,
bool? useUnconfirmed,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
}) async {
// Attempting to send less than the dust limit
if (isBelowDust(credentialsAmount)) {
throw BitcoinTransactionNoDustException();
}
final utxoDetails = createUTXOS(
sendAll: false,
credentialsAmount: credentialsAmount,
inputsCount: inputsCount,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
final spendingAllCoins = utxoDetails.availableInputs.length == utxoDetails.utxos.length;
final spendingAllConfirmedCoins = !utxoDetails.spendsUnconfirmedTX &&
utxoDetails.utxos.length ==
utxoDetails.availableInputs.length - utxoDetails.unconfirmedCoins.length;
// How much is being spent - how much is being sent
int amountLeftForChangeAndFee = utxoDetails.allInputsAmount - credentialsAmount;
if (amountLeftForChangeAndFee <= 0) {
if (!spendingAllCoins) {
return estimateTxForAmountMweb(
credentialsAmount,
outputs,
feeRate,
inputsCount: utxoDetails.utxos.length + 1,
memo: memo,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
}
throw BitcoinTransactionWrongBalanceException();
}
final changeAddress = await walletAddresses.getChangeAddress(
inputs: utxoDetails.availableInputs,
outputs: outputs,
);
final address = RegexUtils.addressTypeFromStr(changeAddress.address, network);
outputs.add(BitcoinOutput(
address: address,
value: BigInt.from(amountLeftForChangeAndFee),
isChange: true,
));
// Get Derivation path for change Address since it is needed in Litecoin and BitcoinCash hardware Wallets
final changeDerivationPath = changeAddress.derivationInfo.derivationPath.toString();
utxoDetails.publicKeys[address.pubKeyHash()] =
PublicKeyWithDerivationPath('', changeDerivationPath);
int fee = await calcFeeMweb(
utxos: utxoDetails.utxos,
// Always take only not updated bitcoin outputs here so for every estimation
// the SP outputs are re-generated to the proper taproot addresses
outputs: outputs,
memo: memo,
feeRate: feeRate,
);
if (fee == 0) {
throw BitcoinTransactionNoFeeException();
}
int amount = credentialsAmount;
final lastOutput = outputs.last;
final amountLeftForChange = amountLeftForChangeAndFee - fee;
if (isBelowDust(amountLeftForChange)) {
// If has change that is lower than dust, will end up with tx rejected by network rules
// so remove the change amount
outputs.removeLast();
outputs.removeLast();
if (amountLeftForChange < 0) {
if (!spendingAllCoins) {
return estimateTxForAmountMweb(
credentialsAmount,
outputs,
feeRate,
inputsCount: utxoDetails.utxos.length + 1,
memo: memo,
useUnconfirmed: useUnconfirmed ?? spendingAllConfirmedCoins,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
} else {
throw BitcoinTransactionWrongBalanceException();
}
}
return EstimatedTxResult(
utxos: utxoDetails.utxos,
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
publicKeys: utxoDetails.publicKeys,
fee: fee,
amount: amount,
hasChange: false,
isSendAll: spendingAllCoins,
memo: memo,
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
);
} else {
// Here, lastOutput already is change, return the amount left without the fee to the user's address.
outputs[outputs.length - 1] = BitcoinOutput(
address: lastOutput.address,
value: BigInt.from(amountLeftForChange),
isChange: true,
);
return EstimatedTxResult(
utxos: utxoDetails.utxos,
inputPrivKeyInfos: utxoDetails.inputPrivKeyInfos,
publicKeys: utxoDetails.publicKeys,
fee: fee,
amount: amount,
hasChange: true,
isSendAll: spendingAllCoins,
memo: memo,
spendsUnconfirmedTX: utxoDetails.spendsUnconfirmedTX,
);
}
}
Future<int> calcFeeMweb({
required List<UtxoWithAddress> utxos,
required List<BitcoinBaseOutput> outputs,
String? memo,
@ -1085,7 +1381,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
final paysToMweb = outputs
.any((output) => output.toOutput.scriptPubKey.getAddressType() == SegwitAddresType.mweb);
if (!spendsMweb && !paysToMweb) {
return await super.calcFee(utxos: utxos, outputs: outputs, memo: memo, feeRate: feeRate);
return super.calcFee(utxos: utxos, outputs: outputs, memo: memo, feeRate: feeRate);
}
if (!mwebEnabled) {
@ -1158,8 +1454,132 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
@override
Future<PendingTransaction> createTransaction(Object credentials) async {
final transactionCredentials = credentials as BitcoinTransactionCredentials;
try {
var tx = await super.createTransaction(credentials) as PendingBitcoinTransaction;
late PendingBitcoinTransaction tx;
try {
final data = getCreateTxDataFromCredentials(credentials);
final coinTypeToSpendFrom = transactionCredentials.coinTypeToSpendFrom;
EstimatedTxResult estimatedTx;
if (data.sendAll) {
estimatedTx = await estimateSendAllTxMweb(
data.outputs,
data.feeRate,
memo: data.memo,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
} else {
estimatedTx = await estimateTxForAmountMweb(
data.amount,
data.outputs,
data.feeRate,
memo: data.memo,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
}
if (walletInfo.isHardwareWallet) {
final transaction = await buildHardwareWalletTransaction(
utxos: estimatedTx.utxos,
outputs: data.outputs,
publicKeys: estimatedTx.publicKeys,
fee: BigInt.from(estimatedTx.fee),
network: network,
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
enableRBF: true,
);
tx = PendingBitcoinTransaction(
transaction,
type,
sendWorker: sendWorker,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
feeRate: data.feeRate.toString(),
hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: false, // ToDo: (Konsti) Support Taproot
)..addListener((transaction) async {
transactionHistory.addOne(transaction);
await updateBalance();
await updateAllUnspents();
});
} else {
final txb = BitcoinTransactionBuilder(
utxos: estimatedTx.utxos,
outputs: data.outputs,
fee: BigInt.from(estimatedTx.fee),
network: network,
memo: estimatedTx.memo,
outputOrdering: BitcoinOrdering.none,
enableRBF: !estimatedTx.spendsUnconfirmedTX,
);
bool hasTaprootInputs = false;
final transaction = txb.buildTransaction((txDigest, utxo, publicKey, sighash) {
String error = "Cannot find private key.";
ECPrivateInfo? key;
if (estimatedTx.inputPrivKeyInfos.isEmpty) {
error += "\nNo private keys generated.";
} else {
error += "\nAddress: ${utxo.ownerDetails.address.toAddress(network)}";
key = estimatedTx.inputPrivKeyInfos.firstWhereOrNull((element) {
final elemPubkey = element.privkey.getPublic().toHex();
if (elemPubkey == publicKey) {
return true;
} else {
error += "\nExpected: $publicKey";
error += "\nPubkey: $elemPubkey";
return false;
}
});
}
if (key == null) {
throw Exception(error);
}
if (utxo.utxo.isP2tr()) {
hasTaprootInputs = true;
return key.privkey.signTapRoot(txDigest, sighash: sighash);
} else {
return key.privkey.signInput(txDigest, sigHash: sighash);
}
});
tx = PendingBitcoinTransaction(
transaction,
type,
sendWorker: sendWorker,
amount: estimatedTx.amount,
fee: estimatedTx.fee,
feeRate: data.feeRate.toString(),
hasChange: estimatedTx.hasChange,
isSendAll: estimatedTx.isSendAll,
hasTaprootInputs: hasTaprootInputs,
utxos: estimatedTx.utxos,
)..addListener((transaction) async {
transactionHistory.addOne(transaction);
unspentCoins
.removeWhere((utxo) => estimatedTx.utxos.any((e) => e.utxo.txHash == utxo.hash));
await updateBalance();
await updateAllUnspents();
});
}
} catch (e) {
throw e;
}
tx.isMweb = mwebEnabled;
if (!mwebEnabled) {
@ -1178,9 +1598,6 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
));
final tx2 = BtcTransaction.fromRaw(hex.encode(resp.rawTx));
// check if the transaction doesn't contain any mweb inputs or outputs:
final transactionCredentials = credentials as BitcoinTransactionCredentials;
bool hasMwebInput = false;
bool hasMwebOutput = false;
bool hasRegularOutput = false;
@ -1249,12 +1666,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
witnesses: tx2.inputs.asMap().entries.map((e) {
final utxo = unspentCoins
.firstWhere((utxo) => utxo.hash == e.value.txId && utxo.vout == e.value.txIndex);
final addressRecord = (utxo.bitcoinAddressRecord as BitcoinAddressRecord);
final path = addressRecord.derivationInfo.derivationPath
.addElem(
Bip32KeyIndex(BitcoinAddressUtils.getAccountFromChange(addressRecord.isChange)))
.addElem(Bip32KeyIndex(addressRecord.index));
final key = ECPrivate.fromBip32(bip32: bip32.derive(path));
final key = ECPrivate.fromBip32(
bip32: bip32.derive((utxo.bitcoinAddressRecord as BitcoinAddressRecord)
.derivationInfo
.derivationPath));
final digest = tx2.getTransactionSegwitDigit(
txInIndex: e.key,
script: key.getPublic().toP2pkhAddress().toScriptPubKey(),

View file

@ -163,11 +163,11 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
@action
@override
Future<BitcoinAddressRecord> getChangeAddress({
BitcoinAddressRecord getChangeAddress({
List<BitcoinUnspent>? inputs,
List<BitcoinOutput>? outputs,
bool isPegIn = false,
}) async {
}) {
// use regular change address on peg in, otherwise use mweb for change address:
if (!mwebEnabled || isPegIn) {
@ -209,7 +209,8 @@ abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses with
}
if (mwebEnabled) {
await ensureMwebAddressUpToIndexExists(1);
// TODO:
// await ensureMwebAddressUpToIndexExists(1);
return BitcoinAddressRecord(
mwebAddrs[0],
index: 0,

View file

@ -184,27 +184,6 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
Uint8List.fromList(hd.childKey(Bip32KeyIndex(index)).privateKey.raw),
);
int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) {
int inputsCount = 0;
int totalValue = 0;
for (final input in unspentCoins) {
if (input.isSending) {
inputsCount++;
totalValue += input.value;
}
if (amount != null && totalValue >= amount) {
break;
}
}
if (amount != null && totalValue < amount) return 0;
final _outputsCount = outputsCount ?? (amount != null ? 2 : 1);
return feeAmountWithFeeRate(feeRate, inputsCount, _outputsCount);
}
@override
int feeRate(TransactionPriority priority) {
if (priority is ElectrumTransactionPriority) {
@ -242,12 +221,12 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
}
@override
Future<int> calcFee({
int calcFee({
required List<UtxoWithAddress> utxos,
required List<BitcoinBaseOutput> outputs,
String? memo,
required int feeRate,
}) async =>
}) =>
feeRate *
ForkedTransactionBuilder.estimateTransactionSize(
utxos: utxos,

View file

@ -4,6 +4,10 @@ abstract class TransactionPriority extends EnumerableItem<int> with Serializable
const TransactionPriority({required super.title, required super.raw});
String get units => '';
String getUnits(int rate) {
return rate == 1 ? units : '${units}s';
}
String toString() {
return title;
}

View file

@ -24,6 +24,9 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
WalletType get type => walletInfo.type;
bool get isElectrum =>
type == WalletType.bitcoin || type == WalletType.litecoin || type == WalletType.bitcoinCash;
CryptoCurrency get currency => currencyForWalletType(type, isTestnet: isTestnet);
String get id => walletInfo.id;
@ -71,7 +74,7 @@ abstract class WalletBase<BalanceType extends Balance, HistoryType extends Trans
Future<PendingTransaction> createTransaction(Object credentials);
int calculateEstimatedFee(TransactionPriority priority, int? amount);
int estimatedFeeForOutputsWithPriority({required TransactionPriority priority});
// void fetchTransactionsAsync(
// void Function(TransactionType transaction) onTransactionLoaded,

View file

@ -29,7 +29,6 @@ import 'package:cw_evm/evm_chain_transaction_model.dart';
import 'package:cw_evm/evm_chain_transaction_priority.dart';
import 'package:cw_evm/evm_chain_wallet_addresses.dart';
import 'package:cw_evm/evm_ledger_credentials.dart';
import 'package:flutter/foundation.dart';
import 'package:hex/hex.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
@ -188,7 +187,7 @@ abstract class EVMChainWalletBase
}
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) {
int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) {
{
try {
if (priority is EVMChainTransactionPriority) {

View file

@ -219,7 +219,7 @@ abstract class HavenWalletBase
}
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) {
int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) {
// FIXME: hardcoded value;
if (priority is MoneroTransactionPriority) {

View file

@ -7,7 +7,7 @@ import 'package:cw_core/pathForWallet.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/account.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/monero_amount_format.dart';
// import 'package:cw_core/monero_amount_format.dart';
import 'package:cw_core/monero_balance.dart';
import 'package:cw_core/monero_transaction_priority.dart';
import 'package:cw_core/monero_wallet_keys.dart';
@ -28,7 +28,7 @@ import 'package:cw_monero/api/transaction_history.dart' as transaction_history;
import 'package:cw_monero/api/wallet.dart' as monero_wallet;
import 'package:cw_monero/api/wallet_manager.dart';
import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart';
import 'package:cw_monero/exceptions/monero_transaction_no_inputs_exception.dart';
// import 'package:cw_monero/exceptions/monero_transaction_no_inputs_exception.dart';
import 'package:cw_monero/ledger.dart';
import 'package:cw_monero/monero_transaction_creation_credentials.dart';
import 'package:cw_monero/monero_transaction_history.dart';
@ -50,8 +50,8 @@ const MIN_RESTORE_HEIGHT = 1000;
class MoneroWallet = MoneroWalletBase with _$MoneroWallet;
abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
MoneroTransactionHistory, MoneroTransactionInfo> with Store {
abstract class MoneroWalletBase
extends WalletBase<MoneroBalance, MoneroTransactionHistory, MoneroTransactionInfo> with Store {
MoneroWalletBase(
{required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
@ -72,16 +72,13 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
transactionHistory = MoneroTransactionHistory();
walletAddresses = MoneroWalletAddresses(walletInfo, transactionHistory);
_onAccountChangeReaction =
reaction((_) => walletAddresses.account, (Account? account) {
_onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) {
if (account == null) return;
balance = ObservableMap<CryptoCurrency, MoneroBalance>.of(<CryptoCurrency,
MoneroBalance>{
balance = ObservableMap<CryptoCurrency, MoneroBalance>.of(<CryptoCurrency, MoneroBalance>{
currency: MoneroBalance(
fullBalance: monero_wallet.getFullBalance(accountIndex: account.id),
unlockedBalance:
monero_wallet.getUnlockedBalance(accountIndex: account.id))
unlockedBalance: monero_wallet.getUnlockedBalance(accountIndex: account.id))
});
_updateSubAddress(isEnabledAutoGenerateSubaddress, account: account);
_askForUpdateTransactionHistory();
@ -131,8 +128,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
publicSpendKey: monero_wallet.getPublicSpendKey(),
publicViewKey: monero_wallet.getPublicViewKey());
int? get restoreHeight =>
transactionHistory.transactions.values.firstOrNull?.height;
int? get restoreHeight => transactionHistory.transactions.values.firstOrNull?.height;
monero_wallet.SyncListener? _listener;
ReactionDisposer? _onAccountChangeReaction;
@ -145,13 +141,11 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
Future<void> init() async {
await walletAddresses.init();
balance = ObservableMap<CryptoCurrency, MoneroBalance>.of(<CryptoCurrency,
MoneroBalance>{
balance = ObservableMap<CryptoCurrency, MoneroBalance>.of(<CryptoCurrency, MoneroBalance>{
currency: MoneroBalance(
fullBalance: monero_wallet.getFullBalance(
accountIndex: walletAddresses.account!.id),
unlockedBalance: monero_wallet.getUnlockedBalance(
accountIndex: walletAddresses.account!.id))
fullBalance: monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id),
unlockedBalance:
monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id))
});
_setListeners();
await updateTransactions();
@ -160,15 +154,14 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
monero_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery);
if (monero_wallet.getCurrentHeight() <= 1) {
monero_wallet.setRefreshFromBlockHeight(
height: walletInfo.restoreHeight);
monero_wallet.setRefreshFromBlockHeight(height: walletInfo.restoreHeight);
}
}
_autoSaveTimer = Timer.periodic(
Duration(seconds: _autoSaveInterval), (_) async => await save());
_autoSaveTimer =
Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save());
// update transaction details after restore
walletAddresses.subaddressList.update(accountIndex: walletAddresses.account?.id??0);
walletAddresses.subaddressList.update(accountIndex: walletAddresses.account?.id ?? 0);
}
@override
@ -271,9 +264,9 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
final inputs = <String>[];
final outputs = _credentials.outputs;
final hasMultiDestination = outputs.length > 1;
final unlockedBalance = monero_wallet.getUnlockedBalance(
accountIndex: walletAddresses.account!.id);
var allInputsAmount = 0;
final unlockedBalance =
monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id);
// var allInputsAmount = 0;
PendingTransactionDescription pendingTransactionDescription;
@ -287,55 +280,44 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
for (final utx in unspentCoins) {
if (utx.isSending) {
allInputsAmount += utx.value;
// allInputsAmount += utx.value;
inputs.add(utx.keyImage!);
}
}
final spendAllCoins = inputs.length == unspentCoins.length;
// final spendAllCoins = inputs.length == unspentCoins.length;
if (hasMultiDestination) {
if (outputs.any(
(item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
throw MoneroTransactionCreationException(
'You do not have enough XMR to send this amount.');
if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) {
throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.');
}
final int totalAmount = outputs.fold(
0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
final int totalAmount =
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
final estimatedFee =
calculateEstimatedFee(_credentials.priority, totalAmount);
// final estimatedFee = estimatedFeeForOutputsWithPriority(priority: _credentials.priority);
if (unlockedBalance < totalAmount) {
throw MoneroTransactionCreationException(
'You do not have enough XMR to send this amount.');
throw MoneroTransactionCreationException('You do not have enough XMR to send this amount.');
}
if (inputs.isEmpty) MoneroTransactionCreationException(
'No inputs selected');
if (inputs.isEmpty) MoneroTransactionCreationException('No inputs selected');
final moneroOutputs = outputs.map((output) {
final outputAddress =
output.isParsedAddress ? output.extractedAddress : output.address;
final outputAddress = output.isParsedAddress ? output.extractedAddress : output.address;
return MoneroOutput(
address: outputAddress!,
amount: output.cryptoAmount!.replaceAll(',', '.'));
address: outputAddress!, amount: output.cryptoAmount!.replaceAll(',', '.'));
}).toList();
pendingTransactionDescription =
await transaction_history.createTransactionMultDest(
outputs: moneroOutputs,
priorityRaw: _credentials.priority.serialize(),
accountIndex: walletAddresses.account!.id,
preferredInputs: inputs);
pendingTransactionDescription = await transaction_history.createTransactionMultDest(
outputs: moneroOutputs,
priorityRaw: _credentials.priority.serialize(),
accountIndex: walletAddresses.account!.id,
preferredInputs: inputs);
} else {
final output = outputs.first;
final address =
output.isParsedAddress ? output.extractedAddress : output.address;
final amount =
output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.');
final formattedAmount =
output.sendAll ? null : output.formattedCryptoAmount;
final address = output.isParsedAddress ? output.extractedAddress : output.address;
final amount = output.sendAll ? null : output.cryptoAmount!.replaceAll(',', '.');
// final formattedAmount = output.sendAll ? null : output.formattedCryptoAmount;
// if ((formattedAmount != null && unlockedBalance < formattedAmount) ||
// (formattedAmount == null && unlockedBalance <= 0)) {
@ -345,17 +327,14 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
// 'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.');
// }
final estimatedFee =
calculateEstimatedFee(_credentials.priority, formattedAmount);
if (inputs.isEmpty) MoneroTransactionCreationException(
'No inputs selected');
pendingTransactionDescription =
await transaction_history.createTransaction(
address: address!,
amount: amount,
priorityRaw: _credentials.priority.serialize(),
accountIndex: walletAddresses.account!.id,
preferredInputs: inputs);
// final estimatedFee = estimatedFeeForOutputsWithPriority(priority: _credentials.priority);
if (inputs.isEmpty) MoneroTransactionCreationException('No inputs selected');
pendingTransactionDescription = await transaction_history.createTransaction(
address: address!,
amount: amount,
priorityRaw: _credentials.priority.serialize(),
accountIndex: walletAddresses.account!.id,
preferredInputs: inputs);
}
// final status = monero.PendingTransaction_status(pendingTransactionDescription);
@ -364,7 +343,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
}
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) {
int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) {
// FIXME: hardcoded value;
if (priority is MoneroTransactionPriority) {
@ -422,10 +401,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
}
try {
// -- rename the waller folder --
final currentWalletDir =
Directory(await pathForWalletDir(name: name, type: type));
final newWalletDirPath =
await pathForWalletDir(name: newWalletName, type: type);
final currentWalletDir = Directory(await pathForWalletDir(name: name, type: type));
final newWalletDirPath = await pathForWalletDir(name: newWalletName, type: type);
await currentWalletDir.rename(newWalletDirPath);
// -- use new waller folder to rename files with old names still --
@ -435,8 +412,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
final currentKeysFile = File('$renamedWalletPath.keys');
final currentAddressListFile = File('$renamedWalletPath.address.txt');
final newWalletPath =
await pathForWallet(name: newWalletName, type: type);
final newWalletPath = await pathForWallet(name: newWalletName, type: type);
if (currentCacheFile.existsSync()) {
await currentCacheFile.rename(newWalletPath);
@ -456,8 +432,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
final currentKeysFile = File('$currentWalletPath.keys');
final currentAddressListFile = File('$currentWalletPath.address.txt');
final newWalletPath =
await pathForWallet(name: newWalletName, type: type);
final newWalletPath = await pathForWallet(name: newWalletName, type: type);
// Copies current wallet files into new wallet name's dir and files
if (currentCacheFile.existsSync()) {
@ -476,8 +451,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
}
@override
Future<void> changePassword(String password) async =>
monero_wallet.setPasswordSync(password);
Future<void> changePassword(String password) async => monero_wallet.setPasswordSync(password);
Future<int> getNodeHeight() async => monero_wallet.getNodeHeight();
@ -512,7 +486,8 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
for (var i = 0; i < coinCount; i++) {
final coin = getCoin(i);
final coinSpent = monero.CoinsInfo_spent(coin);
if (coinSpent == false && monero.CoinsInfo_subaddrAccount(coin) == walletAddresses.account!.id) {
if (coinSpent == false &&
monero.CoinsInfo_subaddrAccount(coin) == walletAddresses.account!.id) {
final unspent = MoneroUnspent(
monero.CoinsInfo_address(coin),
monero.CoinsInfo_hash(coin),
@ -585,15 +560,13 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
Future<void> _refreshUnspentCoinsInfo() async {
try {
final List<dynamic> keys = <dynamic>[];
final currentWalletUnspentCoins = unspentCoinsInfo.values.where(
(element) =>
element.walletId.contains(id) &&
element.accountIndex == walletAddresses.account!.id);
final currentWalletUnspentCoins = unspentCoinsInfo.values.where((element) =>
element.walletId.contains(id) && element.accountIndex == walletAddresses.account!.id);
if (currentWalletUnspentCoins.isNotEmpty) {
currentWalletUnspentCoins.forEach((element) {
final existUnspentCoins = unspentCoins
.where((coin) => element.keyImage!.contains(coin.keyImage!));
final existUnspentCoins =
unspentCoins.where((coin) => element.keyImage!.contains(coin.keyImage!));
if (existUnspentCoins.isEmpty) {
keys.add(element.key);
@ -610,15 +583,13 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
}
String getTransactionAddress(int accountIndex, int addressIndex) =>
monero_wallet.getAddress(
accountIndex: accountIndex, addressIndex: addressIndex);
monero_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex);
@override
Future<Map<String, MoneroTransactionInfo>> fetchTransactions() async {
transaction_history.refreshTransactions();
return (await _getAllTransactionsOfAccount(walletAddresses.account?.id))
.fold<Map<String, MoneroTransactionInfo>>(
<String, MoneroTransactionInfo>{},
.fold<Map<String, MoneroTransactionInfo>>(<String, MoneroTransactionInfo>{},
(Map<String, MoneroTransactionInfo> acc, MoneroTransactionInfo tx) {
acc[tx.id] = tx;
return acc;
@ -647,15 +618,12 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
monero_wallet.getSubaddressLabel(accountIndex, addressIndex);
Future<List<MoneroTransactionInfo>> _getAllTransactionsOfAccount(int? accountIndex) async =>
(await transaction_history
.getAllTransactions())
(await transaction_history.getAllTransactions())
.map(
(row) => MoneroTransactionInfo(
row.hash,
row.blockheight,
row.isSpend
? TransactionDirection.outgoing
: TransactionDirection.incoming,
row.isSpend ? TransactionDirection.outgoing : TransactionDirection.incoming,
row.timeStamp,
row.isPending,
row.amount,
@ -704,8 +672,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
}
int _getHeightDistance(DateTime date) {
final distance =
DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch;
final distance = DateTime.now().millisecondsSinceEpoch - date.millisecondsSinceEpoch;
final daysTmp = (distance / 86400).round();
final days = daysTmp < 1 ? 1 : daysTmp;
@ -726,34 +693,28 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
void _askForUpdateBalance() {
final unlockedBalance = _getUnlockedBalance();
final fullBalance = monero_wallet.getFullBalance(
accountIndex: walletAddresses.account!.id);
final fullBalance = monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id);
final frozenBalance = _getFrozenBalance();
if (balance[currency]!.fullBalance != fullBalance ||
balance[currency]!.unlockedBalance != unlockedBalance ||
balance[currency]!.frozenBalance != frozenBalance) {
balance[currency] = MoneroBalance(
fullBalance: fullBalance,
unlockedBalance: unlockedBalance,
frozenBalance: frozenBalance);
fullBalance: fullBalance, unlockedBalance: unlockedBalance, frozenBalance: frozenBalance);
}
}
Future<void> _askForUpdateTransactionHistory() async =>
await updateTransactions();
Future<void> _askForUpdateTransactionHistory() async => await updateTransactions();
int _getFullBalance() =>
monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id);
// int _getFullBalance() => monero_wallet.getFullBalance(accountIndex: walletAddresses.account!.id);
int _getUnlockedBalance() => monero_wallet.getUnlockedBalance(
accountIndex: walletAddresses.account!.id);
int _getUnlockedBalance() =>
monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id);
int _getFrozenBalance() {
var frozenBalance = 0;
for (var coin in unspentCoinsInfo.values.where((element) =>
element.walletId == id &&
element.accountIndex == walletAddresses.account!.id)) {
element.walletId == id && element.accountIndex == walletAddresses.account!.id)) {
if (coin.isFrozen && !coin.isSending) frozenBalance += coin.value;
}
return frozenBalance;
@ -827,8 +788,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
}
void setLedgerConnection(LedgerConnection connection) {
final dummyWPtr = wptr ??
monero.WalletManager_openWallet(wmPtr, path: '', password: '');
final dummyWPtr = wptr ?? monero.WalletManager_openWallet(wmPtr, path: '', password: '');
enableLedgerExchange(dummyWPtr, connection);
}
}

View file

@ -144,7 +144,8 @@ abstract class NanoWalletBase
}
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0; // always 0 :)
int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) =>
0; // always 0 :)
@override
Future<void> changePassword(String password) => throw UnimplementedError("changePassword");

View file

@ -33,7 +33,6 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:solana/base58.dart';
import 'package:solana/metaplex.dart' as metaplex;
import 'package:solana/solana.dart';
import 'package:solana/src/crypto/ed25519_hd_keypair.dart';
part 'solana_wallet.g.dart';
@ -174,7 +173,7 @@ abstract class SolanaWalletBase
}
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0;
int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) => 0;
@override
Future<void> changePassword(String password) => throw UnimplementedError("changePassword");

View file

@ -211,7 +211,7 @@ abstract class TronWalletBase
}
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) => 0;
int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) => 0;
@override
Future<void> changePassword(String password) => throw UnimplementedError("changePassword");

View file

@ -51,7 +51,9 @@ abstract class WowneroWalletBase
extends WalletBase<WowneroBalance, WowneroTransactionHistory, WowneroTransactionInfo>
with Store {
WowneroWalletBase(
{required WalletInfo walletInfo, required Box<UnspentCoinsInfo> unspentCoinsInfo, required String password})
{required WalletInfo walletInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfo,
required String password})
: balance = ObservableMap<CryptoCurrency, WowneroBalance>.of({
CryptoCurrency.wow: WowneroBalance(
fullBalance: wownero_wallet.getFullBalance(accountIndex: 0),
@ -259,7 +261,7 @@ abstract class WowneroWalletBase
final int totalAmount =
outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0));
final estimatedFee = calculateEstimatedFee(_credentials.priority, totalAmount);
final estimatedFee = estimatedFeeForOutputsWithPriority(priority: _credentials.priority);
if (unlockedBalance < totalAmount) {
throw WowneroTransactionCreationException(
'You do not have enough WOW to send this amount.');
@ -295,7 +297,7 @@ abstract class WowneroWalletBase
'You do not have enough unlocked balance. Unlocked: $formattedBalance. Transaction amount: ${output.cryptoAmount}.');
}
final estimatedFee = calculateEstimatedFee(_credentials.priority, formattedAmount);
final estimatedFee = estimatedFeeForOutputsWithPriority(priority: _credentials.priority);
if (!spendAllCoins &&
((formattedAmount != null && allInputsAmount < (formattedAmount + estimatedFee)) ||
formattedAmount == null)) {
@ -314,7 +316,7 @@ abstract class WowneroWalletBase
}
@override
int calculateEstimatedFee(TransactionPriority priority, int? amount) {
int estimatedFeeForOutputsWithPriority({required TransactionPriority priority}) {
// FIXME: hardcoded value;
if (priority is MoneroTransactionPriority) {

View file

@ -472,31 +472,83 @@ class CWBitcoin extends Bitcoin {
}
@override
int getFeeAmountForPriority(
Object wallet, TransactionPriority priority, int inputsCount, int outputsCount,
{int? size}) {
int getFeeAmountForOutputsWithFeeRate(
Object wallet, {
required int feeRate,
required List<String> inputAddresses,
required List<String> outputAddresses,
String? memo,
bool enableRBF = true,
}) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.feeAmountForPriority(
priority as ElectrumTransactionPriority, inputsCount, outputsCount);
}
@override
int getEstimatedFeeWithFeeRate(Object wallet, int feeRate, int? amount,
{int? outputsCount, int? size}) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.calculateEstimatedFeeWithFeeRate(
return bitcoinWallet.feeAmountWithFeeRate(
feeRate,
amount,
outputsCount: outputsCount,
size: size,
inputTypes: inputAddresses
.map((addr) => BitcoinAddressUtils.addressTypeFromStr(addr, bitcoinWallet.network))
.toList(),
outputTypes: outputAddresses
.map((addr) => BitcoinAddressUtils.addressTypeFromStr(addr, bitcoinWallet.network))
.toList(),
memo: memo,
enableRBF: enableRBF,
);
}
@override
int feeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount,
{int? size}) {
int getFeeAmountForOutputsWithPriority(
Object wallet, {
required TransactionPriority priority,
required List<String> inputAddresses,
required List<String> outputAddresses,
String? memo,
bool enableRBF = true,
}) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.feeAmountWithFeeRate(feeRate, inputsCount, outputsCount, size: size);
return bitcoinWallet.feeAmountForPriority(
priority,
inputTypes: inputAddresses
.map((addr) => BitcoinAddressUtils.addressTypeFromStr(addr, bitcoinWallet.network))
.toList(),
outputTypes: outputAddresses
.map((addr) => BitcoinAddressUtils.addressTypeFromStr(addr, bitcoinWallet.network))
.toList(),
memo: memo,
enableRBF: enableRBF,
);
}
@override
int estimatedFeeForOutputsWithPriority(
Object wallet, {
required TransactionPriority priority,
required String outputAddress,
String? memo,
bool enableRBF = true,
}) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.estimatedFeeForOutputsWithPriority(
priority: priority,
outputAddresses: [outputAddress],
memo: memo,
enableRBF: enableRBF,
);
}
@override
int estimatedFeeForOutputWithFeeRate(
Object wallet, {
required int feeRate,
required String outputAddress,
String? memo,
bool enableRBF = true,
}) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.estimatedFeeForOutputsWithFeeRate(
feeRate: feeRate,
outputAddresses: [outputAddress],
memo: memo,
enableRBF: enableRBF,
);
}
@override
@ -505,7 +557,7 @@ class CWBitcoin extends Bitcoin {
final feeRates = electrumWallet.feeRates;
final maxFee = electrumWallet.feeRates is ElectrumTransactionPriorities
? ElectrumTransactionPriority.fast
: BitcoinTransactionPriority.priority;
: BitcoinAPITransactionPriority.priority;
return (electrumWallet.feeRate(maxFee) * 10).round();
}
@ -640,7 +692,12 @@ class CWBitcoin extends Bitcoin {
Future<int> getHeightByDate({required DateTime date, bool? bitcoinMempoolAPIEnabled}) async {
if (bitcoinMempoolAPIEnabled ?? false) {
try {
return await getBitcoinHeightByDateAPI(date: date);
final mempoolApi = ApiProvider.fromMempool(
BitcoinNetwork.mainnet,
baseUrl: "http://mempool.cakewallet.com:8999/api/v1",
);
return (await mempoolApi.getBlockTimestamp(date))["height"] as int;
} catch (_) {}
}
return await getBitcoinHeightByDate(date: date);

View file

@ -144,22 +144,29 @@ abstract class OutputBase with Store {
return solana!.getEstimateFees(_wallet) ?? 0.0;
}
int? fee = _wallet.calculateEstimatedFee(
_settingsStore.priority[_wallet.type]!, formattedCryptoAmount);
final transactionPriority = _settingsStore.priority[_wallet.type]!;
if (_wallet.type == WalletType.bitcoin) {
if (_settingsStore.priority[_wallet.type] ==
bitcoin!.getBitcoinTransactionPriorityCustom()) {
fee = bitcoin!.getEstimatedFeeWithFeeRate(
_wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount);
if (_wallet.isElectrum) {
late int fee;
if (transactionPriority == bitcoin!.getBitcoinTransactionPriorityCustom()) {
fee = bitcoin!.estimatedFeeForOutputWithFeeRate(
_wallet,
feeRate: _settingsStore.customBitcoinFeeRate,
outputAddress: address,
);
} else {
fee = bitcoin!.estimatedFeeForOutputsWithPriority(
_wallet,
priority: transactionPriority,
outputAddress: address,
);
}
return bitcoin!.formatterBitcoinAmountToDouble(amount: fee);
}
if (_wallet.type == WalletType.litecoin || _wallet.type == WalletType.bitcoinCash) {
return bitcoin!.formatterBitcoinAmountToDouble(amount: fee);
}
final fee = _wallet.estimatedFeeForOutputsWithPriority(priority: transactionPriority);
if (_wallet.type == WalletType.monero) {
return monero!.formatterMoneroAmountToDouble(amount: fee);

View file

@ -542,15 +542,13 @@ abstract class TransactionDetailsViewModelBase with Store {
void addBumpFeesListItems(TransactionInfo tx, String rawTransaction) {
transactionPriority = bitcoin!.getBitcoinTransactionPriorityMedium();
final inputsCount = (transactionInfo.inputAddresses?.isEmpty ?? true)
? 1
: transactionInfo.inputAddresses!.length;
final outputsCount = (transactionInfo.outputAddresses?.isEmpty ?? true)
? 1
: transactionInfo.outputAddresses!.length;
newFee = bitcoin!.getFeeAmountForPriority(
wallet, bitcoin!.getBitcoinTransactionPriorityMedium(), inputsCount, outputsCount);
newFee = bitcoin!.getFeeAmountForOutputsWithPriority(
wallet,
priority: bitcoin!.getBitcoinTransactionPriorityMedium(),
inputAddresses: transactionInfo.inputAddresses!,
outputAddresses: transactionInfo.outputAddresses!,
);
RBFListItems.add(
StandartListItem(
@ -677,17 +675,21 @@ abstract class TransactionDetailsViewModelBase with Store {
}
String setNewFee({double? value, required TransactionPriority priority}) {
newFee = priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null
? bitcoin!.feeAmountWithFeeRate(
wallet,
value.round(),
transactionInfo.inputAddresses?.length ?? 1,
transactionInfo.outputAddresses?.length ?? 1)
: bitcoin!.getFeeAmountForPriority(
wallet,
priority,
transactionInfo.inputAddresses?.length ?? 1,
transactionInfo.outputAddresses?.length ?? 1);
if (priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null) {
newFee = bitcoin!.getFeeAmountForOutputsWithFeeRate(
wallet,
feeRate: value.round(),
inputAddresses: transactionInfo.inputAddresses!,
outputAddresses: transactionInfo.outputAddresses!,
);
} else {
newFee = bitcoin!.getFeeAmountForOutputsWithPriority(
wallet,
priority: priority,
inputAddresses: transactionInfo.inputAddresses!,
outputAddresses: transactionInfo.outputAddresses!,
);
}
return bitcoin!.formatterBitcoinAmountToString(amount: newFee);
}