add transaction creation

This commit is contained in:
Serhii 2023-08-20 21:46:09 +03:00
parent c563e300f8
commit b4316581c8
13 changed files with 425 additions and 129 deletions

View file

@ -19,7 +19,7 @@ class BitcoinAddressRecord {
bool operator ==(Object o) =>
o is BitcoinAddressRecord && address == o.address;
final String address;
String address;
final bool isHidden;
final int index;
bool get isUsed => _isUsed;

View file

@ -21,4 +21,8 @@ class BitcoinUnspent {
bool isSending;
bool isFrozen;
String note;
void updateAddress(String newAddress) {
address.address = newAddress;
}
}

View file

@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:bitcoin_flutter/bitcoin_flutter.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
@ -11,6 +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:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/pathForWallet.dart';
import 'package:cw_bitcoin/address_to_output_script.dart';
@ -191,7 +193,7 @@ abstract class ElectrumWalletBase
}
@override
Future<PendingBitcoinTransaction> createTransaction(Object credentials) async {
Future<PendingTransaction> createTransaction(Object credentials) async {
const minAmount = 546;
final transactionCredentials = credentials as BitcoinTransactionCredentials;
final inputs = <BitcoinUnspent>[];
@ -668,7 +670,8 @@ abstract class ElectrumWalletBase
final addresses = walletAddresses.addresses.toList();
final balanceFutures = <Future<Map<String, dynamic>>>[];
for (var i = 0; i < addresses.length; i++) {
final addressRecord = addresses[i];
// walletInfo.type != WalletType.bitcoinCash ? Address :
final addressRecord = addresses[i] ;
final sh = scriptHash(addressRecord.address, networkType: networkType);
final balanceFuture = electrumClient.getBalance(sh);
balanceFutures.add(balanceFuture);

View file

@ -66,6 +66,15 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.6"
bitbox:
dependency: "direct main"
description:
path: "."
ref: main
resolved-ref: ea65073efbaf395a5557e8cd7bd72f195cd7eb11
url: "https://github.com/Serhii-Borodenko/cw_bitbox.git"
source: git
version: "1.0.1"
bitcoin_flutter:
dependency: "direct main"
description:

View file

@ -23,6 +23,10 @@ dependencies:
git:
url: https://github.com/cake-tech/bitcoin_flutter.git
ref: cake-update-v2
bitbox:
git:
url: https://github.com/Serhii-Borodenko/cw_bitbox.git
ref: main
rxdart: ^0.27.5
unorm_dart: ^0.2.0
cryptography: ^2.0.5

View file

@ -2,7 +2,6 @@ export 'bitcoin_cash_balance.dart';
export 'bitcoin_cash_client.dart';
export 'bitcoin_cash_transaction_history.dart';
export 'bitcoin_cash_transaction_info.dart';
export 'bitcoin_cash_transaction_priority.dart';
export 'bitcoin_cash_wallet.dart';
export 'bitcoin_cash_wallet_addresses.dart';
export 'bitcoin_cash_wallet_creation_credentials.dart';

View file

@ -1,52 +0,0 @@
import 'package:cw_bitcoin/bitcoin_transaction_priority.dart';
class BitcoinCashTransactionPriority extends BitcoinTransactionPriority {
const BitcoinCashTransactionPriority({required String title, required int raw})
: super(title: title, raw: raw);
static const List<BitcoinCashTransactionPriority> all = [fast, medium, slow];
static const BitcoinCashTransactionPriority slow =
BitcoinCashTransactionPriority(title: 'Slow', raw: 0);
static const BitcoinCashTransactionPriority medium =
BitcoinCashTransactionPriority(title: 'Medium', raw: 1);
static const BitcoinCashTransactionPriority fast =
BitcoinCashTransactionPriority(title: 'Fast', raw: 2);
static BitcoinCashTransactionPriority deserialize({required int raw}) {
switch (raw) {
case 0:
return slow;
case 1:
return medium;
case 2:
return fast;
default:
throw Exception('Unexpected token: $raw for BitcoinTransactionPriority deserialize');
}
}
String get units => 'sat';
@override
String toString() {
var label = '';
switch (this) {
case BitcoinCashTransactionPriority.slow:
label = 'Slow ~24hrs'; // '${S.current.transaction_priority_slow} ~24hrs';
break;
case BitcoinCashTransactionPriority.medium:
label = 'Medium'; // S.current.transaction_priority_medium;
break;
case BitcoinCashTransactionPriority.fast:
label = 'Fast'; // S.current.transaction_priority_fast;
break;
default:
break;
}
return label;
}
String labelWithRate(int rate) => '${toString()} ($rate ${units}/byte)';
}

View file

@ -1,14 +1,22 @@
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';
@ -91,69 +99,325 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store {
}
@override
Future<PendingBitcoinTransaction> createTransaction(Object credentials,
[List<Object>? unspents, Object? wallet]) async {
Future<PendingTransaction> createTransaction(Object credentials) async {
final transactionCredentials = credentials as BitcoinTransactionCredentials;
// final utxoSigningData = await fetchBuildTxData(unspents as List<BitcoinUnspent>, wallet as BitcoinCashWalletBase);
// final builder = bitbox.Bitbox.transactionBuilder(testnet: false);
// final utxosToUse = unspents as List<UnspentCash>;
// final _wallet = wallet as BitcoinCashWallet;
// print('unspents: ${unspents.first.address}');
//
// 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(bitcoin.Transaction(), type,
electrumClient: electrumClient, amount: 1, fee: 1);
const minAmount = 546;
final inputs = <BitcoinUnspent>[];
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 != null
? 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!;
});
print(credentialsAmount);
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();
});
}
Future<List<SigningData>> fetchBuildTxData(
List<BitcoinUnspent> utxosToUse, HDWallet wallet) async {
// Initialize the list to store signing data
List<SigningData> signingData = [];
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
// 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]);
}
// Determine address type and create signing data object
signingData.add(SigningData(
derivePathType: DerivePathType.bch44,
utxo: utxosToUse[i])); //TODO: BCH: hardcoded DerivePathType.bch44
// 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;
}
}
// Function to handle each transaction output
Future<void> 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");
}
}
}

View file

@ -0,0 +1,62 @@
import 'package:cw_bitcoin/bitcoin_commit_transaction_exception.dart';
import 'package:bitbox/bitbox.dart' as bitbox;
import 'package:cw_core/pending_transaction.dart';
import 'package:cw_bitcoin/electrum.dart';
import 'package:cw_bitcoin/bitcoin_amount_format.dart';
import 'package:cw_bitcoin/electrum_transaction_info.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/wallet_type.dart';
class PendingBitcoinCashTransaction with PendingTransaction {
PendingBitcoinCashTransaction(this._tx, this.type,
{required this.electrumClient,
required this.amount,
required this.fee})
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
final WalletType type;
final bitbox.Transaction _tx;
final ElectrumClient electrumClient;
final int amount;
final int fee;
@override
String get id => _tx.getId();
@override
String get hex => _tx.toHex();
@override
String get amountFormatted => bitcoinAmountToString(amount: amount);
@override
String get feeFormatted => bitcoinAmountToString(amount: fee);
final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
@override
Future<void> commit() async {
final result =
await electrumClient.broadcastTransaction(transactionRaw: _tx.toHex());
if (result.isEmpty) {
throw BitcoinCommitTransactionException();
}
_listeners?.forEach((listener) => listener(transactionInfo()));
}
void addListener(
void Function(ElectrumTransactionInfo transaction) listener) =>
_listeners.add(listener);
ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type,
id: id,
height: 0,
amount: amount,
direction: TransactionDirection.outgoing,
date: DateTime.now(),
isPending: true,
confirmations: 0,
fee: fee);
}

View file

@ -50,7 +50,7 @@ class CWBitcoin extends Bitcoin {
@override
List<TransactionPriority> getBitcoinCashTransactionPriorities()
=> BitcoinCashTransactionPriority.all;
=> BitcoinTransactionPriority.all;
@override
TransactionPriority deserializeBitcoinTransactionPriority(int raw)
@ -62,7 +62,7 @@ class CWBitcoin extends Bitcoin {
@override
TransactionPriority deserializeBitcoinCashTransactionPriority(int raw)
=> BitcoinCashTransactionPriority.deserialize(raw: raw);
=> BitcoinTransactionPriority.deserialize(raw: raw);
@override
int getFeeRate(Object wallet, TransactionPriority priority) {

View file

@ -28,11 +28,11 @@ class CWBitcoinCash extends BitcoinCash {
@override
TransactionPriority deserializeBitcoinCashTransactionPriority(int raw) =>
BitcoinCashTransactionPriority.deserialize(raw: raw);
BitcoinTransactionPriority.deserialize(raw: raw);
@override
TransactionPriority getDefaultTransactionPriority() => BitcoinCashTransactionPriority.medium;
TransactionPriority getDefaultTransactionPriority() => BitcoinTransactionPriority.medium;
@override
List<TransactionPriority> getTransactionPriorities() => BitcoinCashTransactionPriority.all;
List<TransactionPriority> getTransactionPriorities() => BitcoinTransactionPriority.all;
}

View file

@ -87,6 +87,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.dash:
case CryptoCurrency.eos:
case CryptoCurrency.bch:
return '[0-9a-zA-Z,:]';
case CryptoCurrency.bnb:
return '[0-9a-zA-Z]';
case CryptoCurrency.ltc:
@ -171,7 +172,7 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.shib:
case CryptoCurrency.avaxc:
case CryptoCurrency.bch:
return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44]; //TODO: BCH: replace with correct length
return [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,54,55,56]; //TODO: BCH: replace with correct length
case CryptoCurrency.bnb:
return [42];
case CryptoCurrency.ltc:

View file

@ -81,10 +81,8 @@ abstract class OutputBase with Store {
_amount = monero!.formatterMoneroParseAmount(amount: _cryptoAmount);
break;
case WalletType.bitcoin:
_amount =
bitcoin!.formatterStringDoubleToBitcoinAmount(_cryptoAmount);
break;
case WalletType.litecoin:
case WalletType.bitcoinCash:
_amount =
bitcoin!.formatterStringDoubleToBitcoinAmount(_cryptoAmount);
break;
@ -116,7 +114,8 @@ abstract class OutputBase with Store {
_settingsStore.priority[_wallet.type]!, formattedCryptoAmount);
if (_wallet.type == WalletType.bitcoin ||
_wallet.type == WalletType.litecoin) {
_wallet.type == WalletType.litecoin ||
_wallet.type == WalletType.bitcoinCash) {
return bitcoin!.formatterBitcoinAmountToDouble(amount: fee);
}
@ -234,6 +233,9 @@ abstract class OutputBase with Store {
case WalletType.litecoin:
maximumFractionDigits = 8;
break;
case WalletType.bitcoinCash:
maximumFractionDigits = 8;
break;
case WalletType.haven:
maximumFractionDigits = 12;
break;