buildUnsignedTransaction function
- required TransferIntent<
UtxoFeeInformation> intent, - required UTXONetworkType networkType,
- required HDWalletPath walletPath,
- required Iterable<
UTXOTransaction> txList, - required Amount feePerByte,
- required Iterable<
String> changeAddresses, - List<
ElectrumOutput> ? preChosenUTXOs,
Useful: https://btcinformation.org/en/developer-reference#raw-transaction-format https://github.com/bitcoin/bips/blob/master/bip-0144.mediawiki
Implementation
RawTransaction buildUnsignedTransaction({
required TransferIntent<UtxoFeeInformation> intent,
required UTXONetworkType networkType,
required HDWalletPath walletPath,
required Iterable<UTXOTransaction> txList,
required Amount feePerByte,
required Iterable<String> changeAddresses,
/// Pre chosen UTXOs to deterministly choose the UTXOs
/// if null, the UTXOs will be chosen randomly
List<ElectrumOutput>? preChosenUTXOs,
}) {
if (txList.isEmpty) {
throw SendFailure("No transactions");
}
var targetValue = intent.amount.value;
if (targetValue < BigInt.zero) {
throw SendFailure("targetValue < 0");
}
if (targetValue < networkType.dustTreshhold.legacy.toBI &&
walletPath.purpose != HDWalletPurpose.BIP84) {
throw SendFailure(
"targetValue < DUST_THRESHOLD: ${networkType.dustTreshhold.legacy}",
);
}
if (walletPath.purpose == HDWalletPurpose.BIP84 &&
targetValue < networkType.dustTreshhold.segwit.toBI) {
throw SendFailure(
"targetValue < DUST_THRESHOLD_BIP84: ${networkType.dustTreshhold.segwit}",
);
}
final allUTXOs = extractUTXOs(txList: txList);
if (allUTXOs.isEmpty) {
throw const SendFailure("no UTXOs"); // should be never reached
}
const lockTime = 0;
const validFrom = 0; // EC8 specific
const validUntil = 0; // EC8 specific
final version = networkType.txVersion;
final chosenUTXOs = preChosenUTXOs ??
singleRandomDrawUTXOSelection(
allUTXOs.keys.toList(),
targetValue,
);
Logger.log("Chosen UTXOs: ${chosenUTXOs}");
var chosenUTXOsMap = {
for (final utxo in chosenUTXOs) utxo: allUTXOs[utxo]!,
};
var (totalInputValue, inputMap) = buildInputs(chosenUTXOsMap, networkType);
if (totalInputValue < targetValue) {
throw SendFailure("Not enough funds to pay targetValue $targetValue");
}
if (inputMap.keys.isEmpty) {
throw SendFailure("No inputs");
}
final targetAddress = intent.recipient;
final changeAddress = findUnusedAddress(
addresses: changeAddresses,
txs: txList,
);
///
/// Build Dummy TX
///
final dummyOutputs = buildOutputs(
recipient: intent.recipient,
value: targetValue,
changeAddress: changeAddress,
changeValue: BigInt.one,
networkType: networkType,
);
var dummyTx = buildDummyTx(
networkType: networkType,
walletPath: walletPath,
inputMap: inputMap,
dummyOutputs: dummyOutputs,
);
///
/// Build Outputs again with the estimated size
///
var estimatedFee = calculateFee(tx: dummyTx, feePerByte: feePerByte);
var changeValue = totalInputValue - targetValue - estimatedFee;
if (changeValue < BigInt.zero) {
targetValue -= changeValue.abs();
if (targetValue < networkType.dustTreshhold.legacy.toBI) {
/// Ad addidional UTXO to cover the fee
targetValue = intent.amount.value;
final additionalUTXO = fillUpToTargetAmount(
chosenUTXOs,
allUTXOs.keys.toList(),
targetValue + estimatedFee * BigInt.two,
);
chosenUTXOsMap = {
for (final utxo in additionalUTXO) utxo: allUTXOs[utxo]!,
};
(totalInputValue, inputMap) = buildInputs(chosenUTXOsMap, networkType);
dummyTx = buildDummyTx(
networkType: networkType,
walletPath: walletPath,
inputMap: inputMap,
dummyOutputs: dummyOutputs,
);
estimatedFee = calculateFee(tx: dummyTx, feePerByte: feePerByte);
}
changeValue = totalInputValue - targetValue - estimatedFee;
if (changeValue < BigInt.zero)
throw SendFailure("Not enough funds to pay targetValue $targetValue");
}
assert(
totalInputValue == targetValue + changeValue + estimatedFee,
"Total Input Value does not match Total Output Value",
);
Logger.log("Estimated Fee: $estimatedFee");
final outputs = buildOutputs(
recipient: targetAddress,
value: targetValue,
changeAddress: changeAddress,
changeValue: changeValue,
networkType: networkType,
);
///
/// Build final transaction
///
var tx = RawTransaction.build(
version: version,
lockTime: lockTime,
validFrom: validFrom,
validUntil: validUntil,
inputMap: inputMap,
outputs: outputs,
);
if (tx.totalOutputValue + estimatedFee != totalInputValue) {
throw SendFailure(
"Total Output Value does not match Total Input Value",
);
}
return tx;
}