mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-03-12 09:32:33 +00:00
feat: fee estimation, review comments
This commit is contained in:
parent
a07be3d863
commit
fb5aa9dc6d
22 changed files with 915 additions and 427 deletions
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue