basic transaction creation flow

This commit is contained in:
Serhii 2023-08-25 10:18:53 +03:00
parent 628fb212fb
commit b66f5d89f4
2 changed files with 65 additions and 379 deletions

View file

@ -12,7 +12,7 @@ import 'package:mobx/mobx.dart';
import 'package:rxdart/subjects.dart'; import 'package:rxdart/subjects.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; 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_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/pathForWallet.dart';
import 'package:cw_bitcoin/address_to_output_script.dart'; import 'package:cw_bitcoin/address_to_output_script.dart';
@ -480,15 +480,27 @@ abstract class ElectrumWalletBase
Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
Future<void> updateUnspent() async { Future<void> updateUnspent() async {
final unspent = await Future.wait(walletAddresses.addresses.map((address) => electrumClient final unspent = await Future.wait(walletAddresses.addresses.map((address) {
.getListUnspentWithAddress(address.address, networkType) if(walletInfo.type == WalletType.bitcoinCash) { //TODO: BCH: Remove this when address format is fixed
.then((unspent) => unspent.map((unspent) { final tempAddress = address.address;
try { if (bitbox.Address.detectFormat(tempAddress) == bitbox.Address.formatCashAddr) {
return BitcoinUnspent.fromJSON(address, unspent); try {
} catch (_) { address.address = bitbox.Address.toLegacyAddress(tempAddress);
return null; } 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(); unspentCoins = unspent.expand((e) => e).toList();
if (unspentCoinsInfo.isEmpty) { if (unspentCoinsInfo.isEmpty) {
@ -668,7 +680,6 @@ abstract class ElectrumWalletBase
final addresses = walletAddresses.addresses.toList(); final addresses = walletAddresses.addresses.toList();
final balanceFutures = <Future<Map<String, dynamic>>>[]; final balanceFutures = <Future<Map<String, dynamic>>>[];
for (var i = 0; i < addresses.length; i++) { for (var i = 0; i < addresses.length; i++) {
// walletInfo.type != WalletType.bitcoinCash ? Address :
final addressRecord = addresses[i] ; final addressRecord = addresses[i] ;
final sh = scriptHash(addressRecord.address, networkType: networkType); final sh = scriptHash(addressRecord.address, networkType: networkType);
final balanceFuture = electrumClient.getBalance(sh); final balanceFuture = electrumClient.getBalance(sh);

View file

@ -1,23 +1,16 @@
import 'package:bitbox/bitbox.dart' as bitbox; import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; 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_address_record.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.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/bitcoin_unspent.dart';
import 'package:cw_bitcoin/electrum_balance.dart'; import 'package:cw_bitcoin/electrum_balance.dart';
import 'package:cw_bitcoin/electrum_wallet.dart'; import 'package:cw_bitcoin/electrum_wallet.dart';
import 'package:cw_bitcoin/electrum_wallet_snapshot.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_bitcoin_cash/src/pending_bitcoin_cash_transaction.dart';
import 'package:cw_core/crypto_currency.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/unspent_coins_info.dart';
import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_info.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hex/hex.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart'; import 'package:mobx/mobx.dart';
@ -110,374 +103,56 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
initialChangeAddressIndex: snp.changeAddressIndex); initialChangeAddressIndex: snp.changeAddressIndex);
} }
// @override @override
// Future<PendingTransaction> createTransaction(Object credentials) async { Future<PendingBitcoinCashTransaction> createTransaction(Object credentials) async {
// final transactionCredentials = credentials as BitcoinTransactionCredentials; final _wallet = hd;
// final transactionCredentials = credentials as BitcoinTransactionCredentials;
// const minAmount = 546; final utxosToUse = unspentCoins.where((element) => element.isSending).toList();
// final inputs = <BitcoinUnspent>[]; final builder = bitbox.Bitbox.transactionBuilder(testnet: false);
// 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<bitbox.Utxo> _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 = <Map>[];
// 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<PendingBitcoinTransaction> 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<bitbox.Utxo> _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 = <Map>[];
// 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);
// }
Future<List<SigningData>> fetchBuildTxData( int totalBalance = 0;
List<BitcoinUnspent> utxosToUse, HDWallet wallet) async {
// Initialize the list to store signing data
List<SigningData> signingData = [];
try { // Add all inputs
// Iterate over UTXOs to populate the addresses and fetch transaction details utxosToUse.forEach((BitcoinUnspent utxo) {
for (var i = 0; i < utxosToUse.length; i++) { builder.addInput(utxo.hash, utxo.vout);
final txid = utxosToUse[i].hash; totalBalance += utxo.value;
final tx = await electrumClient.getTransactionRaw( });
hash: txid); //TODO: BCH: replace with getting from local storage if possible
// Iterate through transaction outputs to find corresponding addresses // Calculate the amount to send and change
for (final output in tx["vout"] as List) { final sendAmount = transactionCredentials.outputs[0].formattedCryptoAmount!;
// Handle each transaction output final outputAddress = transactionCredentials.outputs[0].address;
await handleTransactionOutput(output, utxosToUse[i]); final fee = bitbox.BitcoinCash.getByteCount(utxosToUse.length, 2);
} final changeAmount = totalBalance - sendAmount - fee;
// Determine address type and create signing data object // Add output for the recipient
signingData.add(SigningData( builder.addOutput(outputAddress, sendAmount);
derivePathType: DerivePathType.bch44,
utxo: utxosToUse[i])); //TODO: BCH: hardcoded DerivePathType.bch44
// Determine public key (pubKey) and Wallet Import Format (wif) here // Add change output if there is change
// TODO: You need to implement logic to determine pubKey and wif if (changeAmount > 0) {
String? pubKey = wallet.pubKey; final changeAddress = await walletAddresses.getChangeAddress();
String? wif = wallet.wif; builder.addOutput(changeAddress, changeAmount);
// 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;
} }
// 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 bitbox.ECPair generateKeyPair(
Future<void> handleTransactionOutput(Map output, BitcoinUnspent utxo) async { {required bitcoin.HDWallet hd,
final n = output["n"]; required int index,
if (n != null && n == utxo.vout) { required bitcoin.NetworkType network}) =>
String address = output["scriptPubKey"]?["addresses"]?[0] as String? ?? bitbox.ECPair.fromWIF(hd.derive(index).wif!);
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");
}
}
} }