diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index c6bf79f0d..081e43fc7 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -4,6 +4,7 @@ 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'; @@ -110,64 +111,149 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { Future createTransaction(Object credentials) async { const minAmount = 546; final transactionCredentials = credentials as BitcoinTransactionCredentials; + final inputs = []; final outputs = transactionCredentials.outputs; final hasMultiDestination = outputs.length > 1; - final builder = bitbox.Bitbox.transactionBuilder(testnet: false); var allInputsAmount = 0; - var inputs = []; if (unspentCoins.isEmpty) await updateUnspent(); - inputs = unspentCoins.where((element) => element.isSending).toList(); - allInputsAmount = inputs.fold(0, (prev, element) => prev + element.value); + for (final utx in unspentCoins) { + if (utx.isSending) { + allInputsAmount += utx.value; + inputs.add(utx); + } + } if (inputs.isEmpty) throw BitcoinTransactionNoInputsException(); - inputs.forEach((BitcoinUnspent utx) => builder.addInput(utx.hash, utx.vout)); - 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; - //allInputsAmount - transactionCredentials.outputs.fold(0, (prev, element) => prev + element.value); + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || item.formattedCryptoAmount! <= 0)) { + throw BitcoinTransactionWrongBalanceException(currency); + } + credentialsAmount = outputs.fold(0, (acc, value) => acc + value.formattedCryptoAmount!); -// Calculate the amount to send and change - final sendAmount = transactionCredentials.outputs[0].formattedCryptoAmount!; - final outputAddress = transactionCredentials.outputs[0].isParsedAddress - ? transactionCredentials.outputs[0].extractedAddress - : transactionCredentials.outputs[0].address; - final fee = bitbox.BitcoinCash.getByteCount(inputs.length, 2); - final changeAmount = allInputsAmount - sendAmount - fee; + if (allAmount - credentialsAmount < minAmount) { + throw BitcoinTransactionWrongBalanceException(currency); + } -// Add output for the recipient - builder.addOutput(outputAddress, sendAmount); + amount = credentialsAmount; -// Add change output if there is change - if (changeAmount > 0) { - final changeAddress = await walletAddresses.getChangeAddress(); - builder.addOutput(changeAddress, changeAmount); + 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); + }); + + outputs.forEach((item) { + final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount; + final outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address; + 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); } -// Sign all inputs after adding all outputs 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, network: bitcoinCashNetworkType); - builder.sign(i, keyPair, input.value); + txb.sign(i, keyPair, input.value); } // Build the transaction - final tx = builder.build(); + final tx = txb.build(); return PendingBitcoinCashTransaction(tx, type, - electrumClient: electrumClient, amount: sendAmount, fee: fee); + electrumClient: electrumClient, amount: amount, fee: fee); } bitbox.ECPair generateKeyPair( @@ -177,12 +263,34 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { bitbox.ECPair.fromWIF(hd.derive(index).wif!); @override - int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, int outputsCount) => + int feeAmountForPriority( + BitcoinTransactionPriority priority, int inputsCount, int outputsCount) => feeRate(priority) * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount); int feeAmountWithFeeRate(int feeRate, int inputsCount, int outputsCount) => feeRate * bitbox.BitcoinCash.getByteCount(inputsCount, outputsCount); + int calculateEstimatedFeeWithFeeRate(int feeRate, int? amount, {int? outputsCount}) { + 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 BitcoinCashTransactionPriority) { @@ -199,4 +307,3 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { return 0; } } - diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 01a68acbd..9d49da13a 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -48,10 +48,6 @@ class CWBitcoin extends Bitcoin { List getLitecoinTransactionPriorities() => LitecoinTransactionPriority.all; - @override - List getBitcoinCashTransactionPriorities() - => BitcoinTransactionPriority.all; - @override TransactionPriority deserializeBitcoinTransactionPriority(int raw) => BitcoinTransactionPriority.deserialize(raw: raw); @@ -60,10 +56,6 @@ class CWBitcoin extends Bitcoin { TransactionPriority deserializeLitecoinTransactionPriority(int raw) => LitecoinTransactionPriority.deserialize(raw: raw); - @override - TransactionPriority deserializeBitcoinCashTransactionPriority(int raw) - => BitcoinTransactionPriority.deserialize(raw: raw); - @override int getFeeRate(Object wallet, TransactionPriority priority) { final bitcoinWallet = wallet as ElectrumWallet; diff --git a/lib/bitcoin_cash/cw_bitcoin_cash.dart b/lib/bitcoin_cash/cw_bitcoin_cash.dart index b39a4b9ec..50643a492 100644 --- a/lib/bitcoin_cash/cw_bitcoin_cash.dart +++ b/lib/bitcoin_cash/cw_bitcoin_cash.dart @@ -28,11 +28,11 @@ class CWBitcoinCash extends BitcoinCash { @override TransactionPriority deserializeBitcoinCashTransactionPriority(int raw) => - BitcoinTransactionPriority.deserialize(raw: raw); + BitcoinCashTransactionPriority.deserialize(raw: raw); @override - TransactionPriority getDefaultTransactionPriority() => BitcoinTransactionPriority.medium; + TransactionPriority getDefaultTransactionPriority() => BitcoinCashTransactionPriority.medium; @override - List getTransactionPriorities() => BitcoinTransactionPriority.all; + List getTransactionPriorities() => BitcoinCashTransactionPriority.all; } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 8cac26ba6..f1ef266d9 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -731,7 +731,7 @@ abstract class SettingsStoreBase with Store { priority[WalletType.ethereum]!; } if (sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority) != null) { - priority[WalletType.bitcoinCash] = bitcoin?.deserializeBitcoinCashTransactionPriority( + priority[WalletType.bitcoinCash] = bitcoinCash?.deserializeBitcoinCashTransactionPriority( sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority)!) ?? priority[WalletType.bitcoinCash]!; } diff --git a/tool/configure.dart b/tool/configure.dart index 3d3527ebe..c8afbc0d4 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -28,7 +28,6 @@ Future main(List args) async { Future generateBitcoin(bool hasImplementation) async { final outputFile = File(bitcoinOutputPath); const bitcoinCommonHeaders = """ -import 'package:cw_bitcoin_cash/cw_bitcoin_cash.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -61,10 +60,8 @@ abstract class Bitcoin { Map getWalletKeys(Object wallet); List getTransactionPriorities(); List getLitecoinTransactionPriorities(); - List getBitcoinCashTransactionPriorities(); TransactionPriority deserializeBitcoinTransactionPriority(int raw); - TransactionPriority deserializeLitecoinTransactionPriority(int raw); - TransactionPriority deserializeBitcoinCashTransactionPriority(int raw); + TransactionPriority deserializeLitecoinTransactionPriority(int raw); int getFeeRate(Object wallet, TransactionPriority priority); Future generateNewAddress(Object wallet); Object createBitcoinTransactionCredentials(List outputs, {required TransactionPriority priority, int? feeRate});