mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2024-12-22 19:49:22 +00:00
basic transaction creation flow
This commit is contained in:
parent
628fb212fb
commit
b66f5d89f4
2 changed files with 65 additions and 379 deletions
|
@ -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<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type);
|
||||
|
||||
Future<void> 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 = <Future<Map<String, dynamic>>>[];
|
||||
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);
|
||||
|
|
|
@ -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<PendingTransaction> createTransaction(Object credentials) async {
|
||||
// final transactionCredentials = credentials as BitcoinTransactionCredentials;
|
||||
//
|
||||
// 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 ??
|
||||
// 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);
|
||||
// }
|
||||
@override
|
||||
Future<PendingBitcoinCashTransaction> 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<List<SigningData>> fetchBuildTxData(
|
||||
List<BitcoinUnspent> utxosToUse, HDWallet wallet) async {
|
||||
// Initialize the list to store signing data
|
||||
List<SigningData> 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<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");
|
||||
}
|
||||
}
|
||||
bitbox.ECPair generateKeyPair(
|
||||
{required bitcoin.HDWallet hd,
|
||||
required int index,
|
||||
required bitcoin.NetworkType network}) =>
|
||||
bitbox.ECPair.fromWIF(hd.derive(index).wif!);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue