Skip to content

The transaction builder

TransactionBuilder assembles a Transaction for us. We add the Outputs we are spending (the Inputs), the payments (the Outputs), where the change goes, and the fee rate, and then call build. The guide’s Transactions overview explains the parts of a Transaction.

The examples in this section use three fixed testnet keys, and a shorthand for the usual Sighash type:

/// Testnet key used throughout the examples. Its address is mu4DpTaD75nheE4z5CQazqm1ivej1vzL4L.
final aliceKey = SVPrivateKey.fromWIF('cVVvUsNHhbrgd7aW3gnuGo2qJM45LhHhTCVXrDSJDDcNGE6qmyCs');
/// A second testnet key. Its address is mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1.
final bobKey = SVPrivateKey.fromWIF('cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY');
/// A third testnet key, for multisig examples.
final carolKey = SVPrivateKey.fromWIF('cStLVGeWx7fVYKKDXYWVeEbEcPZEC4TD73DjQpHCks2Y8EAjVDSS');
/// The standard sighash type: SIGHASH_ALL | SIGHASH_FORKID.
final sighashAll = SighashType.SIGHASH_ALL.value | SighashType.SIGHASH_FORKID.value;

The coins they spend come from fundingTransaction, which builds a Transaction paying a given Locking Script (see Your first transaction). Unless a page says otherwise, myAddress is the testnet Address of aliceKey, and fundingTx pays 100,000 satoshis to it:

final myAddress = aliceKey.toAddress(networkType: NetworkType.TEST);
final recipient = Address('n3aZKucfWmXeXhX13MREQQnqNfbrWiYKtg');
final fundingTx = fundingTransaction(P2PKHLockBuilder.fromAddress(myAddress).getScriptPubkey(), BigInt.from(100000));
final signer = DefaultTransactionSigner(
SighashType.SIGHASH_ALL.value | SighashType.SIGHASH_FORKID.value, privateKey);
final tx = TransactionBuilder()
// Inputs: what we spend, who signs, and how the Unlocking Script is built
.spendFromTxnWithSigner(signer, fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER,
P2PKHUnlockBuilder(privateKey.publicKey))
// Outputs: who gets paid
.spendToPKH(recipient, BigInt.from(30000))
// whatever is left over, less the fee, comes back to us
.sendChangeToPKH(myAddress)
.withFeePerKb(100)
// build and sign (true runs the builder's sanity checks first)
.build(true);

Three objects work together on each Input:

  • a signer (DefaultTransactionSigner), which holds the Private Key and the Sighash type.
  • an Unlocking Script builder (here P2PKHUnlockBuilder), which knows how to turn Signatures into the Input’s Unlocking Script for one kind of Locking Script.
  • the Output being spent, which the signer needs for its Locking Script and amount.

We add Outputs with spendToPKH(address, satoshis) for a payment to an Address, or with spendToLockBuilder(locker, satoshis) for any other Locking Script. Amounts are BigInt satoshis.

build(true) checks the amounts and the fee before signing, while build(false) skips the checks. Both sign every Input that has a signer. See Fees and change for what the checks reject.

The builder needs four facts about each Output it spends: the Transaction ID, the Output index, the amount and the Locking Script. We pick the method that matches what we have.

We have the whole funding Transaction (parsed with Transaction.fromHex, or built locally). We use spendFromTxnWithSigner, as in the example above.

We have the four values from a UTXO lookup. This is the usual case when working with an indexer or a wallet database. We wrap the values in a TransactionOutpoint.

// From a UTXO lookup: Transaction ID, Output index, amount and Locking Script.
final outpoint = TransactionOutpoint(
txId,
0,
BigInt.from(100000),
SVScript.fromHex(scriptHex),
);
final tx = TransactionBuilder()
.spendFromOutpointWithSigner(signer, outpoint, TransactionInput.MAX_SEQ_NUMBER, unlocker)
.spendToPKH(recipient, BigInt.from(30000))
.sendChangeToPKH(myAddress)
.build(true);

We have a map, for example from decoded JSON:

// These are the keys the builder reads. Note that scriptPubKey is in the library's
// script string format (SVScript.toString()), not hex.
final utxo = <String, dynamic>{
'transactionId': fundingTx.id,
'outputIndex': 0,
'satoshis': 100000,
'scriptPubKey': fundingTx.outputs[0].script.toString(),
};
final tx = TransactionBuilder()
.spendFromUtxoMapWithSigner(signer, utxo, unlocker)
.spendToPKH(recipient, BigInt.from(30000))
.sendChangeToPKH(myAddress)
.build(true);

Adding the same outpoint twice adds only one Input. Each Input can have its own signer, so a single Transaction can spend coins held by several keys. See Spending P2PKH.

If we leave out the signer (spendFromTxn, spendFromOutpoint, spendFromUtxoMap, spendFromOutput), we build an unsigned Transaction whose Unlocking Scripts stay empty. Each Input is then signed afterwards with the signer’s sign(transaction, spentOutput, inputIndex), which adds the Signature to that Input’s Unlocking Script builder. This is how one collects several Signatures on one Input (multisig), or signs after reordering the Inputs.

final unlocker = P2PKHUnlockBuilder(aliceKey.publicKey);
// No signer, so the Input's Unlocking Script stays empty.
final tx = TransactionBuilder()
.spendFromTxn(fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER, unlocker)
.spendToPKH(recipient, BigInt.from(30000))
.sendChangeToPKH(myAddress)
.build(false);
// Later, perhaps somewhere else, we sign Input 0 against the Output it spends.
final signer = DefaultTransactionSigner(
SighashType.SIGHASH_ALL.value | SighashType.SIGHASH_FORKID.value, aliceKey);
signer.sign(tx, fundingTx.outputs[0], 0);

Sign only after the Transaction is final. With the default SIGHASH_ALL, any later change to the Inputs or Outputs invalidates the Signatures already made.

Next: Spending P2PKH.