
com.bccapi.bitlib.StandardTransactionBuilder Maven / Gradle / Ivy
package com.bccapi.bitlib;
import com.bccapi.bitlib.crypto.BitcoinSigner;
import com.bccapi.bitlib.crypto.PrivateKeyRing;
import com.bccapi.bitlib.crypto.PublicKey;
import com.bccapi.bitlib.crypto.PublicKeyRing;
import com.bccapi.bitlib.model.*;
import com.bccapi.bitlib.util.ByteWriter;
import com.bccapi.bitlib.util.CoinUtil;
import com.bccapi.bitlib.util.HashUtils;
import java.util.LinkedList;
import java.util.List;
public class StandardTransactionBuilder {
//TODO: changed that from 50000 to 20000
private static final long MIN_MINER_FEE = 20000;
public static class InsufficientFundsException extends Exception {
private static final long serialVersionUID = 1L;
public long sending;
public long fee;
public InsufficientFundsException(long sending, long fee) {
this.sending = sending;
this.fee = fee;
}
}
public static class SigningRequest {
// The public part of the key we need to sign with
public PublicKey publicKey;
// The data to make a signature on. For transactions this is the
// transaction hash
public byte[] toSign;
public SigningRequest(PublicKey publicKey, byte[] toSign) {
this.publicKey = publicKey;
this.toSign = toSign;
}
}
public static class UnsignedTransaction {
private TransactionOutput[] _outputs;
private UnspentTransactionOutput[] _funding;
private SigningRequest[] _signingRequests;
private NetworkParameters _network;
private UnsignedTransaction(List outputs, List funding,
PublicKeyRing keyRing, NetworkParameters network) {
_network = network;
_outputs = outputs.toArray(new TransactionOutput[] {});
_funding = funding.toArray(new UnspentTransactionOutput[] {});
_signingRequests = new SigningRequest[_funding.length];
// Create empty input scripts pointing at the right out points
TransactionInput[] inputs = new TransactionInput[_funding.length];
for (int i = 0; i < _funding.length; i++) {
inputs[i] = new TransactionInput(_funding[i].outPoint, ScriptInput.EMPTY);
}
// Create transaction with valid outputs and empty inputs
Transaction transaction = new Transaction(1, inputs, _outputs, 0);
for (int i = 0; i < _funding.length; i++) {
UnspentTransactionOutput f = _funding[i];
// Make sure that we only work on standard output scripts
if (!(f.script instanceof ScriptOutputStandard)) {
throw new RuntimeException("Unsupported script");
}
// Find the address of the funding
byte[] addressBytes = ((ScriptOutputStandard) f.script).getAddressBytes();
Address address = Address.fromStandardBytes(addressBytes, _network);
// Find the key to sign with
PublicKey publicKey = keyRing.findPublicKeyByAddress(address);
if (publicKey == null) {
// This should not happen as we only work on outputs that we have
// keys for
throw new RuntimeException("Public key not found");
}
// Set the input script to the funding output script
inputs[i].script = ScriptInput.fromOutputScript(_funding[i].script);
// Calculate the transaction hash that has to be signed
byte[] hash = hashTransaction(transaction);
// Set the input to the empty script again
inputs[i] = new TransactionInput(_funding[i].outPoint, ScriptInput.EMPTY);
_signingRequests[i] = new SigningRequest(publicKey, hash);
}
}
public SigningRequest[] getSignatureInfo() {
return _signingRequests;
}
public long calculateFee() {
long in = 0, out = 0;
for (UnspentTransactionOutput funding : _funding) {
in += funding.value;
}
for (TransactionOutput output : _outputs) {
out += output.value;
}
return in - out;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
String fee = CoinUtil.valueString(calculateFee());
sb.append(String.format("Fee: %s", fee)).append('\n');
int max = Math.max(_funding.length, _outputs.length);
for (int i = 0; i < max; i++) {
UnspentTransactionOutput in = i < _funding.length ? _funding[i] : null;
TransactionOutput out = i < _outputs.length ? _outputs[i] : null;
String line;
if (in != null && out != null) {
line = String.format("%36s %13s -> %36s %13s", getAddress(in.script, _network), getValue(in.value),
getAddress(out.script, _network), getValue(out.value));
} else if (in != null && out == null) {
line = String.format("%36s %13s %36s %13s", getAddress(in.script, _network), getValue(in.value), "",
"");
} else if (in == null && out != null) {
line = String.format("%36s %13s %36s %13s", "", "", getAddress(out.script, _network),
getValue(out.value));
} else {
line = "";
}
sb.append(line).append('\n');
}
return sb.toString();
}
private String getAddress(ScriptOutput script, NetworkParameters network) {
Address address = script.getAddress(network);
if (address == null) {
return "Unknown";
}
return address.toString();
}
private String getValue(Long value) {
return String.format("(%s)", CoinUtil.valueString(value));
}
}
private NetworkParameters _network;
private List _outputs;
public StandardTransactionBuilder(NetworkParameters network) {
_network = network;
_outputs = new LinkedList();
}
public void addOutput(Address sendTo, long value) {
_outputs.add(createOutput(sendTo, value));
}
// XXX Should we support pubkey outputs?
private TransactionOutput createOutput(Address sendTo, long value) {
ScriptOutput script;
if (sendTo.isMultisig(_network)) {
script = new ScriptOutputMultisig(sendTo.getTypeSpecificBytes());
} else {
script = new ScriptOutputStandard(sendTo.getTypeSpecificBytes());
}
TransactionOutput output = new TransactionOutput(value, script);
return output;
}
public static List generateSignatures(SigningRequest[] requests, PrivateKeyRing keyRing) {
List signatures = new LinkedList();
for (SigningRequest request : requests) {
BitcoinSigner signer = keyRing.findSignerByPublicKey(request.publicKey);
if (signer == null) {
// This should not happen as we only work on outputs that we have
// keys for
throw new RuntimeException("Private key not found");
}
byte[] signature = signer.makeStandardBitcoinSignature(request.toSign);
signatures.add(signature);
}
return signatures;
}
/**
* Create an unsigned transaction without specifying a fee. The fee is
* automatically calculated to pass minimum relay and mining requirements.
*
* @param unspent
* The list of unspent transaction outputs that can be used as
* funding
* @param changeAddress
* The address to send any change to
* @param keyRing
* The public key ring matching the unspent outputs
* @param network
* The network we are working on
* @return An unsigned transaction or null if not enough funds were available
* @throws InsufficientFundsException if there is not enough funds available
*/
public UnsignedTransaction createUnsignedTransaction(List unspent, Address changeAddress,
PublicKeyRing keyRing, NetworkParameters network) throws InsufficientFundsException {
long fee = MIN_MINER_FEE;
while (true) {
UnsignedTransaction unsigned;
try {
unsigned = createUnsignedTransaction(unspent, changeAddress, fee, keyRing, network);
} catch (InsufficientFundsException e) {
// We did not even have enough funds to pay the minimum fee
throw e;
}
int txSize = estimateTransacrionSize(unsigned);
// fee is based on the size of the transaction, we have to pay for
// every 1000 bytes
long requiredFee = (1 + (txSize / 1000)) * MIN_MINER_FEE;
if (fee >= requiredFee) {
return unsigned;
}
// collect coins anew with an increased fee
fee += MIN_MINER_FEE;
}
}
/**
* Create an unsigned transaction with a specific miner fee. Note that
* specifying a miner fee that is too low may result in hanging transactions
* that never confirm.
*
* @param unspent
* The list of unspent transaction outputs that can be used as
* funding
* @param changeAddress
* The address to send any change to
* @param fee
* The miner fee to pay. Specifying zero may result in hanging
* transactions.
* @param keyRing
* The public key ring matching the unspent outputs
* @param network
* The network we are working on
* @return An unsigned transaction or null if not enough funds were available
* @throws InsufficientFundsException
*/
public UnsignedTransaction createUnsignedTransaction(List unspent, Address changeAddress,
long fee, PublicKeyRing keyRing, NetworkParameters network) throws InsufficientFundsException {
// Make a copy so we can mutate the list
unspent = new LinkedList(unspent);
List funding = new LinkedList();
long outputSum = outputSum();
long toSend = fee + outputSum;
long found = 0;
while (found < toSend) {
UnspentTransactionOutput output = extractOldest(unspent);
if (output == null) {
// We do not have enough funds
throw new InsufficientFundsException(outputSum, fee);
}
found += output.value;
funding.add(output);
}
// We have our funding, calculate change
long change = found - toSend;
// Get a copy of all outputs
List outputs = new LinkedList(_outputs);
if (change > 0) {
// We have more funds than needed, add an output to our change address
outputs.add(createOutput(changeAddress, change));
}
return new UnsignedTransaction(outputs, funding, keyRing, network);
}
public static Transaction finalizeTransaction(UnsignedTransaction unsigned, List signatures) {
// Create finalized transaction inputs
TransactionInput[] inputs = new TransactionInput[unsigned._funding.length];
for (int i = 0; i < unsigned._funding.length; i++) {
// Create script from signature and public key
ScriptInputStandard script = new ScriptInputStandard(signatures.get(i),
unsigned._signingRequests[i].publicKey.getPublicKeyBytes());
inputs[i] = new TransactionInput(unsigned._funding[i].outPoint, script);
}
// Create transaction with valid outputs and empty inputs
Transaction transaction = new Transaction(1, inputs, unsigned._outputs, 0);
return transaction;
}
private UnspentTransactionOutput extractOldest(List unspent) {
// find the "oldest" output
int minHeight = Integer.MAX_VALUE;
UnspentTransactionOutput oldest = null;
for (UnspentTransactionOutput output : unspent) {
if (!(output.script instanceof ScriptOutputStandard)) {
// only look for standard scripts
continue;
}
if (output.height < minHeight) {
minHeight = output.height;
oldest = output;
}
}
if (oldest == null) {
// There were no outputs
return null;
}
unspent.remove(oldest);
return oldest;
}
private long outputSum() {
long sum = 0;
for (TransactionOutput output : _outputs) {
sum += output.value;
}
return sum;
}
private static byte[] hashTransaction(Transaction t) {
ByteWriter writer = new ByteWriter(1024);
t.toByteWriter(writer);
// We also have to write a hash type.
int hashType = 1;
writer.putIntLE(hashType);
// Note that this is NOT reversed to ensure it will be signed
// correctly. If it were to be printed out
// however then we would expect that it is IS reversed.
return HashUtils.doubleSha256(writer.toBytes());
}
/**
* Estimate transaction size by clearing all input scripts and adding 140
* bytes for each input. (The type of scripts we generate are 138-140 bytes
* long). This allows us to give a good estimate of the final transaction
* size, and determine whether out fee size is large enough.
*
* @param unsigned
* The unsigned transaction to estimate the size of
* @return The estimated transaction size
*/
private static int estimateTransacrionSize(UnsignedTransaction unsigned) {
// Create fake empty inputs
TransactionInput[] inputs = new TransactionInput[unsigned._funding.length];
for (int i = 0; i < inputs.length; i++) {
inputs[i] = new TransactionInput(unsigned._funding[i].outPoint, ScriptInput.EMPTY);
}
// Create fake transaction
Transaction t = new Transaction(1, inputs, unsigned._outputs, 0);
int txSize = t.toBytes().length;
// Add maximum size for each input
txSize += 140 * t.inputs.length;
return txSize;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy