From b66f5d89f4fe3618f23ac06a6a1d0a6a6fe45b84 Mon Sep 17 00:00:00 2001 From: Serhii Date: Fri, 25 Aug 2023 10:18:53 +0300 Subject: [PATCH] basic transaction creation flow --- cw_bitcoin/lib/electrum_wallet.dart | 31 +- .../lib/src/bitcoin_cash_wallet.dart | 413 ++---------------- 2 files changed, 65 insertions(+), 379 deletions(-) diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 139ddd324..3a5859f0d 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -12,7 +12,7 @@ import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:bitbox/bitbox.dart'; +import 'package:bitbox/bitbox.dart' as bitbox; import 'package:cw_bitcoin/electrum_transaction_info.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_bitcoin/address_to_output_script.dart'; @@ -480,15 +480,27 @@ abstract class ElectrumWalletBase Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); Future updateUnspent() async { - final unspent = await Future.wait(walletAddresses.addresses.map((address) => electrumClient - .getListUnspentWithAddress(address.address, networkType) - .then((unspent) => unspent.map((unspent) { - try { - return BitcoinUnspent.fromJSON(address, unspent); - } catch (_) { - return null; + final unspent = await Future.wait(walletAddresses.addresses.map((address) { + if(walletInfo.type == WalletType.bitcoinCash) { //TODO: BCH: Remove this when address format is fixed + final tempAddress = address.address; + if (bitbox.Address.detectFormat(tempAddress) == bitbox.Address.formatCashAddr) { + try { + address.address = bitbox.Address.toLegacyAddress(tempAddress); + } catch (_) { + rethrow; + } + } } - }).whereNotNull()))); + return electrumClient + .getListUnspentWithAddress(address.address, networkType) + .then((unspent) => unspent.map((unspent) { + try { + return BitcoinUnspent.fromJSON(address, unspent); + } catch (_) { + return null; + } + }).whereNotNull()); + })); unspentCoins = unspent.expand((e) => e).toList(); if (unspentCoinsInfo.isEmpty) { @@ -668,7 +680,6 @@ abstract class ElectrumWalletBase final addresses = walletAddresses.addresses.toList(); final balanceFutures = >>[]; for (var i = 0; i < addresses.length; i++) { - // walletInfo.type != WalletType.bitcoinCash ? Address : final addressRecord = addresses[i] ; final sh = scriptHash(addressRecord.address, networkType: networkType); final balanceFuture = electrumClient.getBalance(sh); diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index 6de231637..8f73fb328 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -1,23 +1,16 @@ import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:bitcoin_flutter/bitcoin_flutter.dart'; 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/pending_bitcoin_transaction.dart'; import 'package:cw_bitcoin_cash/src/pending_bitcoin_cash_transaction.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/pending_transaction.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:flutter/foundation.dart'; -import 'package:hex/hex.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -110,374 +103,56 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { initialChangeAddressIndex: snp.changeAddressIndex); } - // @override - // Future createTransaction(Object credentials) async { - // final transactionCredentials = credentials as BitcoinTransactionCredentials; - // - // const minAmount = 546; - // final inputs = []; - // var allInputsAmount = 0; - // final outputs = transactionCredentials.outputs; - // final hasMultiDestination = outputs.length > 1; - // - // 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 int feeRate = transactionCredentials.feeRate ?? - // BitcoinCashFeeRates.feeRate(transactionCredentials.priority!); - // - // final int allAmountFee = - // bitbox.BitcoinCash.getByteCount(inputs.length, transactionCredentials.outputs.length) * - // feeRate; - // - // 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) { - // return 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 changeAddress = await walletAddresses.getChangeAddress(); TODO: BCH: implement change address - // 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); - // } - // - // final builder = bitbox.Bitbox.transactionBuilder(testnet: false); - // final _wallet = hd; - // - // final utxoSigningData = await fetchBuildTxData(inputs, _wallet); - // - // List _utxos = []; - // for (var element in inputs) { - // _utxos.add(bitbox.Utxo(element.hash, element.vout, - // bitbox.BitcoinCash.fromSatoshi(element.value), element.value, 0, 1)); - // } - // - // final signatures = []; - // int totalBalance = 0; - // - // _utxos.forEach((bitbox.Utxo utxo) { - // builder.addInput(utxo.txid, utxo.vout); - // - // final ec = utxoSigningData.firstWhere((e) => e.utxo.hash == utxo.txid).keyPair!; - // - // final bitboxEC = bitbox.ECPair.fromWIF(ec.toWIF()); - // - // signatures - // .add({"vin": signatures.length, "key_pair": bitboxEC, "original_amount": utxo.satoshis}); - // - // totalBalance += utxo.satoshis; - // }); - // - // outputs.forEach((item) { - // final outputAmount = hasMultiDestination ? item.formattedCryptoAmount : amount; - // final outputAddress = item.isParsedAddress ? item.extractedAddress! : item.address; - // builder.addOutput(outputAddress, outputAmount!); - // }); - // - // signatures.forEach((signature) { - // builder.sign(signature["vin"], signature["key_pair"], signature["original_amount"]); - // }); - // - // // build the transaction - // final tx = builder.build(); - // return PendingBitcoinCashTransaction(tx, type, - // electrumClient: electrumClient, amount: amount, fee: fee) - // ..addListener((transaction) async { - // transactionHistory.addOne(transaction); - // await updateBalance(); - // }); - // } - // @override - // Future createTransaction(Object credentials) async { - // final utxosToUse = unspentCoins.where((utxo) => utxo.isSending).toList(); - // final utxoSigningData = await fetchBuildTxData(utxosToUse, hd); - // final builder = bitbox.Bitbox.transactionBuilder(testnet: false); - // - // List _utxos = []; - // for (var element in utxosToUse) { - // _utxos.add(bitbox.Utxo(element.hash, element.vout, - // bitbox.BitcoinCash.fromSatoshi(element.value), element.value, 0, 1)); - // } - // - // final signatures = []; - // int totalBalance = 0; - // - // _utxos.forEach((bitbox.Utxo utxo) { - // // add the utxo as an input for the transaction - // builder.addInput(utxo.txid, utxo.vout); - // - // final ec = utxoSigningData.firstWhere((e) => e.utxo.hash == utxo.txid).keyPair!; - // - // final bitboxEC = bitbox.ECPair.fromWIF(ec.toWIF()); - // - // // add a signature to the list to be used later - // signatures - // .add({"vin": signatures.length, "key_pair": bitboxEC, "original_amount": utxo.satoshis}); - // - // totalBalance += utxo.satoshis; - // }); - // - // // set an address to send the remaining balance to - // final outputAddress = "13Hvge9HRduGiXMfcJHFn6sggequmaKqsZ"; - // - // // if there is an unspent balance, create a spending transaction - // if (totalBalance > 0 && outputAddress != "") { - // // calculate the fee based on number of inputs and one expected output - // final fee = bitbox.BitcoinCash.getByteCount(signatures.length, 1); - // - // // calculate how much balance will be left over to spend after the fee - // final sendAmount = totalBalance - fee; - // - // // add the output based on the address provided in the testing data - // builder.addOutput(outputAddress, sendAmount); - // - // // sign all inputs - // signatures.forEach((signature) { - // builder.sign(signature["vin"], signature["key_pair"], signature["original_amount"]); - // }); - // - // // build the transaction - // final tx = builder.build(); - // - // // broadcast the transaction - // final result = await electrumClient.broadcastTransaction(transactionRaw: tx.toHex()); - // - // // Yatta! - // print("Transaction broadcasted: $result"); - // } - // return PendingBitcoinTransaction(Transaction(), type, - // electrumClient: electrumClient, amount: 1, fee: 1); - // } + @override + Future createTransaction(Object credentials) async { + final _wallet = hd; + final transactionCredentials = credentials as BitcoinTransactionCredentials; + final utxosToUse = unspentCoins.where((element) => element.isSending).toList(); + final builder = bitbox.Bitbox.transactionBuilder(testnet: false); - Future> fetchBuildTxData( - List utxosToUse, HDWallet wallet) async { - // Initialize the list to store signing data - List signingData = []; + int totalBalance = 0; - try { - // Iterate over UTXOs to populate the addresses and fetch transaction details - for (var i = 0; i < utxosToUse.length; i++) { - final txid = utxosToUse[i].hash; - final tx = await electrumClient.getTransactionRaw( - hash: txid); //TODO: BCH: replace with getting from local storage if possible +// Add all inputs + utxosToUse.forEach((BitcoinUnspent utxo) { + builder.addInput(utxo.hash, utxo.vout); + totalBalance += utxo.value; + }); - // Iterate through transaction outputs to find corresponding addresses - for (final output in tx["vout"] as List) { - // Handle each transaction output - await handleTransactionOutput(output, utxosToUse[i]); - } +// Calculate the amount to send and change + final sendAmount = transactionCredentials.outputs[0].formattedCryptoAmount!; + final outputAddress = transactionCredentials.outputs[0].address; + final fee = bitbox.BitcoinCash.getByteCount(utxosToUse.length, 2); + final changeAmount = totalBalance - sendAmount - fee; - // Determine address type and create signing data object - signingData.add(SigningData( - derivePathType: DerivePathType.bch44, - utxo: utxosToUse[i])); //TODO: BCH: hardcoded DerivePathType.bch44 +// Add output for the recipient + builder.addOutput(outputAddress, sendAmount); - // Determine public key (pubKey) and Wallet Import Format (wif) here - // TODO: You need to implement logic to determine pubKey and wif - String? pubKey = wallet.pubKey; - String? wif = wallet.wif; - - // Then call the preparePaymentData function - preparePaymentData( - signingData[i], pubKey, wif, bitcoincash); //TODO: BCH: hardcoded bitcoincash - } - - // Return the signing data for later use - return signingData; - } catch (e) { - print(e); - rethrow; +// Add change output if there is change + if (changeAmount > 0) { + final changeAddress = await walletAddresses.getChangeAddress(); + builder.addOutput(changeAddress, changeAmount); } + +// Sign all inputs after adding all outputs + for (var i = 0; i < utxosToUse.length; i++) { + final input = utxosToUse[i]; + final keyPair = generateKeyPair( + hd: input.address.isHidden ? walletAddresses.sideHd : walletAddresses.mainHd, + index: input.address.index, + network: bitcoinCashNetworkType); + builder.sign(i, keyPair, input.value); + } + + // Build the transaction + final tx = builder.build(); + + return PendingBitcoinCashTransaction(tx, type, + electrumClient: electrumClient, amount: sendAmount, fee: fee); } -// Function to handle each transaction output - Future handleTransactionOutput(Map output, BitcoinUnspent utxo) async { - final n = output["n"]; - if (n != null && n == utxo.vout) { - String address = output["scriptPubKey"]?["addresses"]?[0] as String? ?? - output["scriptPubKey"]["address"] as String; - - // Convert to Cash Address format if needed - if (bitbox.Address.detectFormat(address) != bitbox.Address.formatCashAddr) { - try { - address = bitbox.Address.toCashAddress(address); - } catch (_) { - rethrow; - } - } - - // Update UTXO with the new address - utxo.updateAddress(address); // Make sure 'address' is mutable or create a method to update it - } - } - -// Function to prepare payment data - void preparePaymentData( - SigningData sd, String? pubKey, String? wif, bitcoin.NetworkType _network) { - if (wif != null && pubKey != null) { - PaymentData data; // Removed 'final' modifier - final Uint8List? redeemScript; - - switch (sd.derivePathType) { - case DerivePathType.bip44: - case DerivePathType.bch44: - data = P2PKH( - data: PaymentData( - pubkey: Uint8List.fromList(HEX.decode(pubKey)), - ), - network: _network, - ).data; - redeemScript = null; - break; - - default: - throw Exception("DerivePathType unsupported"); - } - - final keyPair = ECPair.fromWIF( - wif, - network: _network, - ); - - sd.redeemScript = redeemScript; - sd.output = data.output; - sd.keyPair = keyPair; - } - } -} - -class SigningData { - SigningData({ - required this.derivePathType, - required this.utxo, - this.output, - this.keyPair, - this.redeemScript, - }); - - final DerivePathType derivePathType; - final BitcoinUnspent utxo; - Uint8List? output; - ECPair? keyPair; - Uint8List? redeemScript; -} - -enum DerivePathType { - bip44, - bch44, - bip49, - bip84, - eth, - eCash44, -} - -// Bitcoincash Network -final bitcoincash = bitcoin.NetworkType( - messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'bc', - bip32: bitcoin.Bip32Type(public: 0x0488b21e, private: 0x0488ade4), - pubKeyHash: 0x00, - scriptHash: 0x05, - wif: 0x80); - -final bitcoincashtestnet = bitcoin.NetworkType( - messagePrefix: '\x18Bitcoin Signed Message:\n', - bech32: 'tb', - bip32: bitcoin.Bip32Type(public: 0x043587cf, private: 0x04358394), - pubKeyHash: 0x6f, - scriptHash: 0xc4, - wif: 0xef); - -class BitcoinCashFeeRates { - static const int highPriority = 10; - static const int mediumPriority = 5; - static const int lowPriority = 1; - - static int feeRate(BitcoinTransactionPriority priority) { - switch (priority) { - case BitcoinTransactionPriority.fast: - return highPriority; - case BitcoinTransactionPriority.medium: - return mediumPriority; - case BitcoinTransactionPriority.slow: - return lowPriority; - default: - throw Exception("Unknown priority level"); - } - } + bitbox.ECPair generateKeyPair( + {required bitcoin.HDWallet hd, + required int index, + required bitcoin.NetworkType network}) => + bitbox.ECPair.fromWIF(hd.derive(index).wif!); }