Build a Transaction
In this section we will do a step-by-step walkthrough of how one creates a new Spending Transaction. We will be assuming that we are working with the most common type of Spending Transaction called P2PKH (Pay-to-Public-Key-Hash).
We cover P2PKH in detail in the next section on Standard Script Templates.
While creating the Spending Transaction itself is rather straightforward when using DartSV, there is a bit of ceremony we need to endure in order to get our hands on a Transaction we can spend from. We will be doing that against a RegTest Teranode running on our own machine. If you haven’t set one up yet, the RegTest Teranode tutorial takes you through it.
The Script
Section titled “The Script”To successfully build our Spending Transaction we need an existing Transaction on the blockchain which has a Locking Script (ScriptPubkey) in one of its Outputs of the form:
OP_DUP OP_HASH160 <hash of Public Key> OP_EQUALVERIFY OP_CHECKSIGWe will need to provide the Unlocking Script (ScriptSig) as a counterpart to this, which will have the general form:
<Signature> <Public Key>When combined by the Script Interpreter to validate our Spending Transaction, the executing Script will have the form:
<Signature> <Public Key> OP_DUP OP_HASH160 <hash of Public Key> OP_EQUALVERIFY OP_CHECKSIGCreating a Private Key
Section titled “Creating a Private Key”DartSV provides some built-in convenience classes which abstract a lot of the above complexity away from the developer. It is still important to note that the ScriptSig created by the Spending Transaction will need two items:
- an ECDSA Signature,
- an ECDSA Public Key.
The library will:
- generate the Signature for us, in accordance with the Sighash flags that we provide,
- derive the Public Key from the Private Key, and include it in the ScriptSig for us.
For the above to happen, we need to provide a Private Key to the library. This Private Key must of course be the key that generates a valid Signature and Public Key capable of unlocking the funds.
Let’s put this together into a few lines that create a random Private Key, and print it along with its Address:
final privateKey = SVPrivateKey(networkType: NetworkType.REGTEST);final address = privateKey.toAddress(networkType: NetworkType.REGTEST);
print('Private Key: ${privateKey.toWIF()}');print('Address: $address');Running the above produces output like the following. Because the Private Key is randomly generated, you will get a different key.
Private Key: cVVvUsNHhbrgd7aW3gnuGo2qJM45LhHhTCVXrDSJDDcNGE6qmyCsAddress: mu4DpTaD75nheE4z5CQazqm1ivej1vzL4LWhere you see classes prefixed with SV, this is to avoid namespace clashes and potential confusion with similarly named classes in other libraries. Here there is an existing PrivateKey class in the pointycastle cryptographic library.
A regtest Address uses the same prefixes as testnet, so it starts with an m or an n, and its WIF starts with a c. If you want to follow along with exactly the same key as we do, you can import it from its WIF:
final privateKey = SVPrivateKey.fromWIF('cVVvUsNHhbrgd7aW3gnuGo2qJM45LhHhTCVXrDSJDDcNGE6qmyCs');final myAddress = privateKey.toAddress(networkType: NetworkType.REGTEST);// myAddress: mu4DpTaD75nheE4z5CQazqm1ivej1vzL4LYou can learn more about the WIF format for Private Keys, and how an Address is derived from a Public Key, in the Addresses section of the Appendix.
Receiving some coins
Section titled “Receiving some coins”A Transaction where we can spend from is the same as saying a UTXO. Yet another way of putting it is to say that we need to send some coins to a bitcoin Address for which we hold the Private Key.
Before we can spend any coins, we need to receive some first. This means we need a way to get our hands on a Transaction that contains a UTXO (Unspent Transaction Output) that we are able to spend using our Private Key.
The simplest way to get our hands on some bitcoins to play with is to use fake bitcoins, or at least bitcoins which are not spendable on the public Bitcoin network. Our RegTest Teranode has a faucet which holds the coins it has mined. Let’s ask it to send 1,000,000 satoshis to our Address. The faucet prints something like this:
./scripts/faucet.sh send mu4DpTaD75nheE4z5CQazqm1ivej1vzL4L 1000000txid 07cadf410a4a44e239981346385d6657104c95f8e6c092633df98f03379d69cesent 1000000 sats to mu4DpTaD75nheE4z5CQazqm1ivej1vzL4Lhex 0100000001f66dee52d8b4f885e6cd2342820be4fa5b16bd626b74adcbeda7e0f786f8dca0…Notice that the faucet has given us a Transaction ID, as well as the Raw Transaction. Your Transaction ID will be different to this one, since your faucet holds different coins to ours.
Raw Transaction data refers to the serialised hexadecimal encoding of a Transaction.
Let’s paste that hex into our program:
// The raw Transaction the faucet printed when it paid us 1,000,000 satoshis.const faucetHex = '0100000001f66dee52d8b4f885e6cd2342820be4fa5b16bd626b74adcbeda7e0f786f8dca000000000' '6a47304402202dfc903e7f6c9c8654c195831bf5aaff030936f4256d1559c5b738d39f6cca480220615ba327' 'af7ccfac82dbc5cbe666d01095b3d8149bb563462ef9451598032c99412102b226c8e640807782a015dbc84d14' '19da1119a014ca56dbe14e7ff2fb7d79a845ffffffff0240420f00000000001976a91494837d2d5d6106aa97db' '38957dcc294181ee91e988aca9aff629010000001976a91481cab069466b06e588b4c884114c3dac740ee9cf88' 'ac00000000';That information is not very useful as it stands. In 2020 we would have asked our node to decode it for us, but Teranode doesn’t offer decoderawtransaction. That’s no great loss, as DartSV reads a Raw Transaction just as well. Let’s parse it, and look for the Output that pays to our Address:
final faucetTx = Transaction.fromHex(faucetHex);print('txid ${faucetTx.id}');
final myScript = P2PKHLockBuilder.fromAddress(myAddress).getScriptPubkey().toHex();for (var i = 0; i < faucetTx.outputs.length; i++) { final output = faucetTx.outputs[i]; final mine = output.script.toHex() == myScript ? ' <- ours' : ''; print('Output $i: ${output.satoshis} satoshis, ${output.script.toString(type: 'asm')}$mine');}This prints:
txid 07cadf410a4a44e239981346385d6657104c95f8e6c092633df98f03379d69ceOutput 0: 1000000 satoshis, OP_DUP OP_HASH160 94837d2d5d6106aa97db38957dcc294181ee91e9 OP_EQUALVERIFY OP_CHECKSIG <- oursOutput 1: 4998999977 satoshis, OP_DUP OP_HASH160 81cab069466b06e588b4c884114c3dac740ee9cf OP_EQUALVERIFY OP_CHECKSIGWe can see that Output 0 is the one locked to the hash of our Public Key, with our 1,000,000 satoshis in it. Output 1 is the faucet’s change, which it sent back to itself. Notice too that both Locking Scripts have exactly the P2PKH form we saw at the start of this section.
The DataHub of our RegTest Teranode will also show us any Transaction it holds, e.g.
http://127.0.0.1:18090/api/v1/utxos/<txid>/jsonlists every Output of a Transaction and whether it is still unspent.
The Spending Transaction
Section titled “The Spending Transaction”Now that we have received some coins at a Bitcoin Address that we control, we can finally create a Spending Transaction. Let’s send half of our coins to Bob, and the rest back to ourselves:
final bobAddress = Address('mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1');
// The signer holds our Private Key, and the Sighash type to sign with.final signer = DefaultTransactionSigner( SighashType.SIGHASH_ALL.value | SighashType.SIGHASH_FORKID.value, privateKey,);// The unlocker writes the Unlocking Script: <Signature> <Public Key>.final unlocker = P2PKHUnlockBuilder(privateKey.publicKey);
final spendingTx = TransactionBuilder() .spendFromTxnWithSigner(signer, faucetTx, 0, TransactionInput.MAX_SEQ_NUMBER, unlocker) // Output 0 is ours .spendToPKH(bobAddress, BigInt.from(500000)) // pay Bob half .sendChangeToPKH(myAddress) // and the rest, less the fee, back to us .withFeePerKb(100) // 100 satoshis per kilobyte .build(true);In the preceding example the TransactionBuilder needs to know four things:
- Where the money is coming from.
spendFromTxnWithSigner()creates an Input that spends Output 0 of the faucet’s Transaction. It takes the signer, which holds our Private Key and the Sighash type, and the unlocker, which writes the Unlocking Script once there is a Signature to put in it. - Where the money is going to.
spendToPKH()creates an Output that pays 500,000 satoshis to Bob’s Address. - Where what’s left over should go.
sendChangeToPKH()creates a Change Output back to our own Address, with whatever remains after the payment and the fee. - How much we are willing to pay the miners.
withFeePerKb()sets the fee rate, here 100 satoshis per kilobyte.
build(true) runs the builder’s sanity checks on the amounts and the fee, and then signs every Input that has a signer.
Any coins in our Inputs that we don’t explicitly send to an Output are paid to the miners as the fee. This is why the Change Output matters so much. Do note that DartSV puts the Change Output first, so in this Transaction Output 0 is our change of 499,994 satoshis, and Output 1 is Bob’s payment.
Checking it ourselves
Section titled “Checking it ourselves”Before we hand our Transaction to anyone, it is a good idea to check it the same way a node would. The Script Interpreter runs our new Unlocking Script followed by the Locking Script it has to satisfy:
Interpreter().correctlySpends( spendingTx.inputs[0].script!, // the Unlocking Script we just made faucetTx.outputs[0].script, // the Locking Script it has to satisfy spendingTx, 0, // the Input's index {VerifyFlag.SIGHASH_FORKID, VerifyFlag.UTXO_AFTER_GENESIS}, Coin.ofSat(faucetTx.outputs[0].satoshis),);If the spend is valid, correctlySpends() returns quietly. If it isn’t, it throws a ScriptException that tells us which operation failed. This is a great deal more useful than a rejection from the network, so make it a habit.
Broadcasting it
Section titled “Broadcasting it”All that is left is to serialise our Transaction:
print(spendingTx.serialize()); // the raw Transaction, ready to broadcastThis gives us the Raw Transaction once again, this time our own:
0200000001ce699d37038ff93d6392c0e6f8954c1057665d3846139839e2444a0a41dfca07000000006a4730440220176592495a390ced7c9aac6fef69e0a05034d8a5076b340760b6addfcb210a7902203d3f6c39f564668d212c9c62ad8a5f6f7338998c1bf2a221c6f84a1278f5b4494121029ddb9308c0dab96d047bbc99191ef60d3882e03dd8f49b8811abe1c9f0b3389dffffffff021aa10700000000001976a91494837d2d5d6106aa97db38957dcc294181ee91e988ac20a10700000000001976a91488d9931ea73d60eaf7e5671efc0552b912911f2a88ac00000000To broadcast it to the network (our RegTest Teranode in this case), we use the node’s sendrawtransaction RPC method, and then mine a block so that it is confirmed:
./scripts/rpc.sh sendrawtransaction 0200000001ce699d37038ff93d6392c0e6f8954c1057665d3846139839e2444a0a41dfca07…./scripts/mine.sh 1Teranode answers with the Transaction ID, which is the same one spendingTx.id gives us. Ours is 3f3e41d80f9069af73d42f6f72862a48546e2de8fb15d1185451aa167ba5f544. Once the block is mined, we can look it up in the DataHub, and see the block it was mined in:
curl -s http://127.0.0.1:18090/api/v1/txmeta/3f3e41d80f9069af73d42f6f72862a48546e2de8fb15d1185451aa167ba5f544/jsonIt should be noted that the node’s RPC is the quick way to broadcast while we are learning, but it is not how an application does it. An application broadcasts through Arcade, which follows the Transaction until it is mined and hands back its merkle proof. Arcade wants the Transaction in Extended Format, which also carries the amount and Locking Script of each Output it spends. The RegTest Teranode tutorial and Broadcasting and merkle proofs cover this.
This chapter might have been a little heavy. We delved rather deeply into how one gets coins from a local test network into one’s own code and back out again. It is a good idea to take some time here to play with the amounts, the fee rate and the Outputs, and to see what the Interpreter makes of your changes.

