Skip to content

The Transaction builder

The TransactionBuilder assembles a Transaction from three kinds of parts:

  • Inputs, each pointing at an Output we are spending, with an unlocking script builder that will produce the Unlocking Script (ScriptSig), and optionally a signer.
  • Outputs, each an amount together with a locking script builder.
  • Optionally a Change Output, which receives whatever the Inputs hold beyond the Outputs and the fee (see fees and change).

Calling build(true) checks the result, creates the Transaction, and signs every Input that has a signer. The examples on this page spend P2PKH Outputs, which is the standard pay-to-address case. The other pages under Transactions cover the other script types, and the Developer Guide explains Inputs, Outputs and scripts.

The examples share some setup. We will need Alice’s key and Address, Bob’s Address, and a constant for the usual Sighash type. Fixtures.fundingTransaction() is the helper from your first transaction.

// The Sighash type we sign with throughout: ALL, with the FORKID flag that BSV requires.
static final int SIGHASH = SigHashType.ALL.value | SigHashType.FORKID.value;
PrivateKey alice = PrivateKey.fromWIF("cVVvUsNHhbrgd7aW3gnuGo2qJM45LhHhTCVXrDSJDDcNGE6qmyCs");
Address aliceAddress = Address.fromKey(NetworkAddressType.TEST_PKH, alice.getPublicKey());
Address bobAddress = Address.fromString(NetworkType.TEST, "mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc");

A LockingScriptBuilder produces an Output’s Locking Script (ScriptPubkey), and an UnlockingScriptBuilder produces an Input’s Unlocking Script, usually from a Signature it receives during signing. The library has a pair for each standard script type:

Script type Locking builder Unlocking builder Page
P2PKH P2PKHLockBuilder(address) P2PKHUnlockBuilder(publicKey) this page
P2PK P2PKLockBuilder(publicKey) P2PKUnlockBuilder() P2PK
Multisig P2MSLockBuilder(keys, m) P2MSUnlockBuilder() Multisig
P2SH P2SHLockBuilder(redeemScript) P2SHUnlockBuilder(script) P2SH
Hodlocker HodlLockBuilder(pubkeyHash, height) HodlUnlockBuilder(...) Hodlocker
Data UnspendableDataLockBuilder, P2PKHDataLockBuilder (P2PKH for the latter) Data outputs
Anything DefaultLockBuilder(script) DefaultUnlockBuilder(script) Custom builders

Each locking builder also has a constructor that takes an existing Script and parses it. That is how we read the fields back out of an Output.

When we have the whole funding Transaction, spendFromTransaction() takes the Output index and reads the amount and the Locking Script from it. This is the form that our first transaction uses. The arguments are the signer, the funding Transaction, the Output index, the Input’s sequence number and the unlocker.

Every Input has its own signer, so Inputs owned by different keys can go into one Transaction.

// Each Input gets the signer for the key that owns the Output it spends.
Transaction tx = new TransactionBuilder()
.spendFromTransaction(new TransactionSigner(SIGHASH, alice), aliceFunding, 0,
TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(alice.getPublicKey()))
.spendFromTransaction(new TransactionSigner(SIGHASH, carol), carolFunding, 0,
TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(carol.getPublicKey()))
.spendTo(new P2PKHLockBuilder(bobAddress), BigInteger.valueOf(120_000))
.sendChangeTo(aliceAddress)
.build(true);

Wallet APIs and indexers usually return UTXOs as records, with a txid, an index, an amount and a Locking Script. spendFromUtxoMap() takes such a record as a Map, with these keys and value types:

// The shape an indexer or wallet API usually gives us, with scriptPubKey in hex.
Map<String, Object> utxoMap = new HashMap<>();
utxoMap.put("transactionId", fundingTxId);
utxoMap.put("outputIndex", 0);
utxoMap.put("satoshis", BigInteger.valueOf(100_000));
utxoMap.put("sequenceNumber", TransactionInput.MAX_SEQ_NUMBER);
utxoMap.put("scriptPubKey", "76a91494837d2d5d6106aa97db38957dcc294181ee91e988ac");
Transaction tx = new TransactionBuilder()
.spendFromUtxoMap(new TransactionSigner(SIGHASH, alice), utxoMap, new P2PKHUnlockBuilder(alice.getPublicKey()))
.spendTo(new P2PKHLockBuilder(bobAddress), BigInteger.valueOf(50_000))
.sendChangeTo(aliceAddress)
.build(true);

It should be noted that the values are cast without any conversion, so outputIndex must be an Integer, sequenceNumber a Long and satoshis a BigInteger.

Please note that scriptPubKey may be hex, as most APIs give it, or ASM (the form Script.toAsmString() gives us). The builder reads any even-length string of hex digits as hex.

spendFromOutput() takes the txid, index and amount directly. To sign, the signer also needs the Locking Script of the Output we spend, since the Signature commits to it, so the signer overload takes that script as well.

// We know the txid, index and amount. The signer also needs the Output's Locking Script.
Script lockingScript = new P2PKHLockBuilder(aliceAddress).getLockingScript();
Transaction tx = new TransactionBuilder()
.spendFromOutput(new TransactionSigner(SIGHASH, alice), fundingTxId, 0, BigInteger.valueOf(100_000),
lockingScript, TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(alice.getPublicKey()))
.spendTo(new P2PKHLockBuilder(bobAddress), BigInteger.valueOf(50_000))
.sendChangeTo(aliceAddress)
.build(true);

Do note that the older signer overload, without the Locking Script, is deprecated, and throws an IllegalArgumentException that names the overload to use.

spendFromOutpoint() takes the same information as a TransactionOutpoint.

TransactionOutpoint outpoint = new TransactionOutpoint(fundingTxId, 0, BigInteger.valueOf(100_000),
new P2PKHLockBuilder(aliceAddress).getLockingScript());
Transaction tx = new TransactionBuilder()
.spendFromOutpoint(new TransactionSigner(SIGHASH, alice), outpoint,
TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(alice.getPublicKey()))
.spendTo(new P2PKHLockBuilder(bobAddress), BigInteger.valueOf(50_000))
.sendChangeTo(aliceAddress)
.build(true);

If we leave out the signer, build() produces an unsigned Transaction in which each Unlocking Script is empty. We then sign it once the Transaction is final, e.g. after setting the lock time, or on another machine that holds the key.

// No signer this time, so build() leaves the Input's Unlocking Script empty.
Transaction tx = new TransactionBuilder()
.spendFromTransaction(fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(alice.getPublicKey()))
.spendTo(new P2PKHLockBuilder(bobAddress), BigInteger.valueOf(50_000))
.sendChangeTo(aliceAddress)
.build(true);
// Later, after any changes to the Transaction, we sign Input 0 against the Output it spends.
TransactionOutput utxo = fundingTx.getOutputs().get(0);
new TransactionSigner(SIGHASH, alice).sign(tx, utxo, 0);

TransactionSigner.sign() puts the Signature into the Input’s unlocker, and the unlocker builds the Unlocking Script from it. The page on signing has the details.

Choosing between build(true) and build(false)

Section titled “Choosing between build(true) and build(false)”

build(true) checks the Transaction before creating it, and throws a TransactionException if:

  • the Outputs (including the change) add up to more than the Inputs,
  • an Output, or the change, is below the dust limit of 256 satoshis (OP_RETURN data Outputs excepted),
  • the fee is more than 150 times the estimate, which guards against forgetting the Change Output,
  • a fee set with setFee() does not match what is left over, which can only happen without a Change Output.

build(false) skips all of these. We need it for Transactions that others will add Inputs to (see Sighash types). The dust check leaves data Outputs alone, since they carry no satoshis.

Do note that the checks never look at scripts or Signatures. For that, we run the interpreter.

To switch off one check at a time, we pass a TransactionOption to withOption(): DISABLE_MORE_OUTPUT_THAN_INPUT, DISABLE_LARGE_FEES or DISABLE_DUST_OUTPUTS. DISABLE_ALL switches them all off.

// Without a Change Output, a 50,000-satoshi fee fails the check for large fees...
assertThrows(TransactionException.class, () -> new TransactionBuilder()
.spendFromTransaction(fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(alice.getPublicKey()))
.spendTo(new P2PKHLockBuilder(bobAddress), BigInteger.valueOf(50_000))
.build(true));
// ...unless we switch that one check off. The other checks still run.
Transaction tx = new TransactionBuilder()
.spendFromTransaction(fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(alice.getPublicKey()))
.spendTo(new P2PKHLockBuilder(bobAddress), BigInteger.valueOf(50_000))
.withOption(TransactionOption.DISABLE_LARGE_FEES)
.build(true);