mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 12:29:51 +00:00
Cw 598 fixes for electrum based wallets (#1344)
* fix: address book addresses, bch builder, exchange all fee estimation, bch coin control * feat: new error framework for Electrum messages * build: cw_bitcoin.dart * feat: error improvements, localization, fix exchange amount mismatch * chore: misc comment & print [skip ci] * feat: refactor & simplify sendAll vs regular tx estimation and creation - Since there were so many conditions inside a single function to alter its behavior if sendAll or not, it is easier and more readable to have separate sendAll and estimateTx functions that behave separately * fix: wrong LTC dust * feat: fee rate confirmation * fix: wrong createTrade value when isSendAll is enabled * fix bitcoin cash address parsing [skip ci] * fix: form no amount validator, address book with multiple entries, exchange all below min error * fix: improve string, fix sending with dust inputs at the top * fix: two change outputs when re-estimating * fix: sendAll with a little dust adds fees * chore: sanity check [skip ci] * fix: if the fee is higher than estimated * Minor enhancement [skip ci] --------- Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
parent
78685b74f0
commit
fd9018bcc4
60 changed files with 984 additions and 431 deletions
|
@ -4,15 +4,10 @@ import 'package:bitbox/bitbox.dart' as bitbox;
|
|||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin;
|
||||
import 'package:cw_bitcoin/bitcoin_address_record.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_no_inputs_exception.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_transaction_wrong_balance_exception.dart';
|
||||
import 'package:cw_bitcoin/bitcoin_unspent.dart';
|
||||
import 'package:cw_bitcoin/electrum_balance.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet.dart';
|
||||
import 'package:cw_bitcoin/electrum_wallet_snapshot.dart';
|
||||
import 'package:cw_bitcoin_cash/src/pending_bitcoin_cash_transaction.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
|
@ -130,187 +125,9 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PendingBitcoinCashTransaction> createTransaction(Object credentials) async {
|
||||
const minAmount = 546;
|
||||
final transactionCredentials = credentials as BitcoinTransactionCredentials;
|
||||
final inputs = <BitcoinUnspent>[];
|
||||
final outputs = transactionCredentials.outputs;
|
||||
final hasMultiDestination = outputs.length > 1;
|
||||
|
||||
var allInputsAmount = 0;
|
||||
|
||||
final String? opReturnMemo = outputs.first.memo;
|
||||
|
||||
if (unspentCoins.isEmpty) await updateUnspent();
|
||||
|
||||
for (final utx in unspentCoins) {
|
||||
if (utx.isSending) {
|
||||
allInputsAmount += utx.value;
|
||||
inputs.add(utx);
|
||||
}
|
||||
}
|
||||
|
||||
if (inputs.isEmpty) throw BitcoinTransactionNoInputsException();
|
||||
|
||||
final allAmountFee = transactionCredentials.feeRate != null
|
||||
? feeAmountWithFeeRate(transactionCredentials.feeRate!, inputs.length, outputs.length)
|
||||
: feeAmountForPriority(transactionCredentials.priority!, inputs.length, outputs.length);
|
||||
|
||||
final allAmount = allInputsAmount - allAmountFee;
|
||||
|
||||
var credentialsAmount = 0;
|
||||
var amount = 0;
|
||||
var fee = 0;
|
||||
|
||||
if (hasMultiDestination) {
|
||||
if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!);
|
||||
|
||||
if (allAmount - credentialsAmount < minAmount) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
amount = credentialsAmount;
|
||||
|
||||
if (transactionCredentials.feeRate != null) {
|
||||
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount,
|
||||
outputsCount: outputs.length + 1);
|
||||
} else {
|
||||
fee = calculateEstimatedFee(transactionCredentials.priority, amount,
|
||||
outputsCount: outputs.length + 1);
|
||||
}
|
||||
} else {
|
||||
final output = outputs.first;
|
||||
credentialsAmount = !output.sendAll ? output.formattedCryptoAmount! : 0;
|
||||
|
||||
if (credentialsAmount > allAmount) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
amount = output.sendAll || allAmount - credentialsAmount < minAmount
|
||||
? allAmount
|
||||
: credentialsAmount;
|
||||
|
||||
if (output.sendAll || amount == allAmount) {
|
||||
fee = allAmountFee;
|
||||
} else if (transactionCredentials.feeRate != null) {
|
||||
fee = calculateEstimatedFeeWithFeeRate(transactionCredentials.feeRate!, amount);
|
||||
} else {
|
||||
fee = calculateEstimatedFee(transactionCredentials.priority, amount);
|
||||
}
|
||||
}
|
||||
|
||||
if (fee == 0) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
final totalAmount = amount + fee;
|
||||
|
||||
if (totalAmount > balance[currency]!.confirmed || totalAmount > allInputsAmount) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
final txb = bitbox.Bitbox.transactionBuilder(testnet: false);
|
||||
|
||||
final changeAddress = await walletAddresses.getChangeAddress();
|
||||
var leftAmount = totalAmount;
|
||||
var totalInputAmount = 0;
|
||||
|
||||
inputs.clear();
|
||||
|
||||
for (final utx in unspentCoins) {
|
||||
if (utx.isSending) {
|
||||
leftAmount = leftAmount - utx.value;
|
||||
totalInputAmount += utx.value;
|
||||
inputs.add(utx);
|
||||
|
||||
if (leftAmount <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inputs.isEmpty) throw BitcoinTransactionNoInputsException();
|
||||
|
||||
if (amount <= 0 || totalInputAmount < totalAmount) {
|
||||
throw BitcoinTransactionWrongBalanceException(currency);
|
||||
}
|
||||
|
||||
inputs.forEach((input) {
|
||||
txb.addInput(input.hash, input.vout);
|
||||
});
|
||||
|
||||
final String bchPrefix = "bitcoincash:";
|
||||
|
||||
outputs.forEach((item) {
|
||||
final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount;
|
||||
String outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address;
|
||||
|
||||
if (!outputAddress.startsWith(bchPrefix)) {
|
||||
outputAddress = "$bchPrefix$outputAddress";
|
||||
}
|
||||
|
||||
bool isP2sh = outputAddress.startsWith("p", bchPrefix.length);
|
||||
|
||||
if (isP2sh) {
|
||||
final p2sh = P2shAddress.fromAddress(
|
||||
address: outputAddress,
|
||||
network: BitcoinCashNetwork.mainnet,
|
||||
);
|
||||
|
||||
txb.addOutput(Uint8List.fromList(p2sh.toScriptPubKey().toBytes()), outputAmount!);
|
||||
return;
|
||||
}
|
||||
|
||||
txb.addOutput(outputAddress, outputAmount!);
|
||||
});
|
||||
|
||||
final estimatedSize = bitbox.BitcoinCash.getByteCount(inputs.length, outputs.length + 1);
|
||||
|
||||
var feeAmount = 0;
|
||||
|
||||
if (transactionCredentials.feeRate != null) {
|
||||
feeAmount = transactionCredentials.feeRate! * estimatedSize;
|
||||
} else {
|
||||
feeAmount = feeRate(transactionCredentials.priority!) * estimatedSize;
|
||||
}
|
||||
|
||||
final changeValue = totalInputAmount - amount - feeAmount;
|
||||
|
||||
if (changeValue > minAmount) {
|
||||
txb.addOutput(changeAddress, changeValue);
|
||||
}
|
||||
|
||||
if (opReturnMemo != null) txb.addOutputData(opReturnMemo);
|
||||
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
final input = inputs[i];
|
||||
final keyPair = generateKeyPair(
|
||||
hd: input.bitcoinAddressRecord.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd,
|
||||
index: input.bitcoinAddressRecord.index);
|
||||
txb.sign(i, keyPair, input.value);
|
||||
}
|
||||
|
||||
final tx = txb.build();
|
||||
|
||||
return PendingBitcoinCashTransaction(tx, type,
|
||||
electrumClient: electrumClient, amount: amount, fee: fee);
|
||||
}
|
||||
|
||||
bitbox.ECPair generateKeyPair({required bitcoin.HDWallet hd, required int index}) =>
|
||||
bitbox.ECPair.fromWIF(hd.derive(index).wif!);
|
||||
|
||||
@override
|
||||
int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount,
|
||||
{int? size}) =>
|
||||
feeRate(priority) * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount);
|
||||
|
||||
int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount, {int? size}) =>
|
||||
feeRate * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount);
|
||||
|
||||
int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount, int? size}) {
|
||||
int inputsCount = 0;
|
||||
int totalValue = 0;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import 'package:cw_bitcoin/bitcoin_commit_transaction_exception.dart';
|
||||
import 'package:cw_bitcoin/exceptions.dart';
|
||||
import 'package:bitbox/bitbox.dart' as bitbox;
|
||||
import 'package:cw_core/pending_transaction.dart';
|
||||
import 'package:cw_bitcoin/electrum.dart';
|
||||
|
@ -11,7 +11,9 @@ class PendingBitcoinCashTransaction with PendingTransaction {
|
|||
PendingBitcoinCashTransaction(this._tx, this.type,
|
||||
{required this.electrumClient,
|
||||
required this.amount,
|
||||
required this.fee})
|
||||
required this.fee,
|
||||
required this.hasChange,
|
||||
required this.isSendAll})
|
||||
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
|
||||
|
||||
final WalletType type;
|
||||
|
@ -19,6 +21,8 @@ class PendingBitcoinCashTransaction with PendingTransaction {
|
|||
final ElectrumClient electrumClient;
|
||||
final int amount;
|
||||
final int fee;
|
||||
final bool hasChange;
|
||||
final bool isSendAll;
|
||||
|
||||
@override
|
||||
String get id => _tx.getId();
|
||||
|
@ -36,18 +40,36 @@ class PendingBitcoinCashTransaction with PendingTransaction {
|
|||
|
||||
@override
|
||||
Future<void> commit() async {
|
||||
final result =
|
||||
await electrumClient.broadcastTransaction(transactionRaw: _tx.toHex());
|
||||
int? callId;
|
||||
|
||||
final result = await electrumClient.broadcastTransaction(
|
||||
transactionRaw: hex, idCallback: (id) => callId = id);
|
||||
|
||||
if (result.isEmpty) {
|
||||
throw BitcoinCommitTransactionException();
|
||||
if (callId != null) {
|
||||
final error = electrumClient.getErrorMessage(callId!);
|
||||
|
||||
if (error.contains("dust")) {
|
||||
if (hasChange) {
|
||||
throw BitcoinTransactionCommitFailedDustChange();
|
||||
} else if (!isSendAll) {
|
||||
throw BitcoinTransactionCommitFailedDustOutput();
|
||||
} else {
|
||||
throw BitcoinTransactionCommitFailedDustOutputSendAll();
|
||||
}
|
||||
}
|
||||
|
||||
if (error.contains("bad-txns-vout-negative")) {
|
||||
throw BitcoinTransactionCommitFailedVoutNegative();
|
||||
}
|
||||
}
|
||||
throw BitcoinTransactionCommitFailed();
|
||||
}
|
||||
|
||||
_listeners?.forEach((listener) => listener(transactionInfo()));
|
||||
_listeners.forEach((listener) => listener(transactionInfo()));
|
||||
}
|
||||
|
||||
void addListener(
|
||||
void Function(ElectrumTransactionInfo transaction) listener) =>
|
||||
void addListener(void Function(ElectrumTransactionInfo transaction) listener) =>
|
||||
_listeners.add(listener);
|
||||
|
||||
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue