Skip to content

Parsing and serialising

A Transaction is serialised to raw hex for broadcasting and storage, and parsed back from it again. The byte layout is described in the guide’s Transaction format page.

The examples use the keys and the fundingTransaction helper described in About the examples. This page works with a signed Transaction, signed, and its raw hex, rawHex:

// A signed Transaction to work with: the one from "Your first transaction".
final myAddress = aliceKey.toAddress(networkType: NetworkType.TEST);
final fundingTx = fundingTransaction(P2PKHLockBuilder.fromAddress(myAddress).getScriptPubkey(), BigInt.from(100000));
final signed = TransactionBuilder()
.spendFromTxnWithSigner(DefaultTransactionSigner(sighashAll, aliceKey), fundingTx, 0,
TransactionInput.MAX_SEQ_NUMBER, P2PKHUnlockBuilder(aliceKey.publicKey))
.spendToPKH(Address('n3aZKucfWmXeXhX13MREQQnqNfbrWiYKtg'), BigInt.from(60000))
.sendChangeToPKH(myAddress)
.withFeePerKb(100)
.build(true);
final rawHex = signed.serialize();
final tx = Transaction.fromHex(rawHex);
tx.id; // transaction id, hex, in the byte order block explorers show
tx.version; // 2
tx.nLockTime; // 0
for (final input in tx.inputs) {
input.prevTxnId; // the transaction it spends from
input.prevTxnOutputIndex; // which output of that transaction
input.sequenceNumber;
input.script; // the unlocking script
}
for (final output in tx.outputs) {
output.satoshis; // BigInt
output.script; // the locking script
}

Input scripts parse as DefaultUnlockBuilders. To read their parts, we parse them with the matching builder, such as P2PKHUnlockBuilder.fromScript. To recognise Output types, we use script templates.

final hex = signed.serialize(); // raw transaction, ready to broadcast
final copy = Transaction.fromHex(hex);
copy.serialize() == hex; // true: parsing and serialising preserve every byte
copy.id == signed.id; // true

When a Transaction arrives, we compare its Locking Scripts with our own to find the Outputs our wallet can spend:

// Which Outputs of a received Transaction can this wallet spend?
final myScript = P2PKHLockBuilder.fromAddress(myAddress).getScriptPubkey().toHex();
final mine = <int>[];
for (var i = 0; i < tx.outputs.length; i++) {
if (tx.outputs[i].script.toHex() == myScript) mine.add(i);
}

toObject() gives us a map for logging or JSON, and Transaction.fromJSONMap reads the same shape back.

final Map<String, dynamic> obj = signed.toObject();
// {hash: ..., version: 2, inputs: [{prevTxId, outputIndex, sequenceNumber, script}],
// outputs: [{satoshis, script}], nLockTime: 0}; scripts are hex

sort() orders the Inputs and Outputs by the BIP69 rules, so that their order reveals nothing about which Output is the change. We sort before signing:

// We sort the Inputs and Outputs (BIP69) while the Transaction is unsigned.
// Sorting a signed Transaction would invalidate its Signatures.
final tx = TransactionBuilder()
.spendFromTxn(fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER, P2PKHUnlockBuilder(aliceKey.publicKey))
.spendFromTxn(fundingB, 0, TransactionInput.MAX_SEQ_NUMBER, P2PKHUnlockBuilder(aliceKey.publicKey))
.spendToPKH(Address('n3aZKucfWmXeXhX13MREQQnqNfbrWiYKtg'), BigInt.from(90000))
.spendToPKH(myAddress, BigInt.from(39000))
.build(false)
..sort();
// Now we sign each Input against the Output it spends, by its new position.
final utxos = {fundingTx.id: fundingTx.outputs[0], fundingB.id: fundingB.outputs[0]};
final signer = DefaultTransactionSigner(
SighashType.SIGHASH_ALL.value | SighashType.SIGHASH_FORKID.value, aliceKey);
for (var i = 0; i < tx.inputs.length; i++) {
signer.sign(tx, utxos[tx.inputs[i].prevTxnId]!, i);
}