Manual Bitcoin Management Example
Mix.install([
{:bitcoinlib, "~> 0.4.2"}
])
alias BitcoinLib.Address
alias BitcoinLib.Key.{PrivateKey, PublicKey}
alias BitcoinLib.Key.HD.SeedPhrase
alias BitcoinLib.Transaction
alias BitcoinLib.Script
Keys creation
🎲 Create a seed phrase from dice rolls.
To do so, just throw 50 dice, and make a string of 50 chars containing the results, so it can be converted into a 12 words seed phrase
.
{:ok, seed_phrase} =
SeedPhrase.from_dice_rolls("12345612345612345612345612345612345612345612345612")
🔑 Derive the master private key
from the seed phrase.
master_private_key = PrivateKey.from_seed_phrase(seed_phrase)
Derive the first testnet receive private key
.
To keep things simple, just know that it’s the 44'
in the derivation path that makes it a P2PKH
transaction. This is a bit outside of the exercise’s scope, so let’s continue.
receive_private_key =
master_private_key
|> PrivateKey.from_derivation_path!("m/44'/1'/0'/0/0")
receive_public_key =
receive_private_key
|> PublicKey.from_private_key()
Convert the public key
to an address
, which will be needed by faucets to receive funds
receive_address = Address.from_public_key(receive_public_key, :p2pkh, :testnet)
Request some funds
👁️ Monitor the address
Go to mempool.space
by directly forging the URL with the address and wait transactions being confirmed.
Ex: https://mempool.space/testnet/address/mwuh4ikLxAqQfSBEYVDiwdrfEVBKw8h98x
Now leave that browser tab lying around. Mempool is supposed to issue a cashier bell sound 🔔 when it detects new incoming transactions to that address. When funds become spendable, you’ll also hear somewhat of a magic wand sound. 🪄
🚰 Request some testnet Bitcoin from faucets
Go to a Bitcoin Testnet faucet and use the above address to request funds… any amount would do, even tiny fractions.
- https://coinfaucet.eu/en/btc-testnet/
- https://bitcoinfaucet.uo1.net/
- https://testnet.qc.to/
- https://onchain.io/bitcoin-testnet-faucet
Here is an example of 10000 testnet sats, or 0.0001 tBTC, being ready to spend with 14 confirmations.
Examine transactions
Click on the transaction ID.
There is a Inputs & Outputs
section, with inputs on the left and outputs in the right… here, our address is the first output. In computer science, 0
represents the first value, and we associate it to vout
. This will be useful later to point to the right output inside the transaction, which happens to have two here.
Clicking on the Details
button reveals this:
Here lies the script pub key
, also known as the locking script
. This is what prevents funds from being spent, unless it is being used with the right parameters. That is exactly what’s going to happen later.
The following code is defining variables containing what we just talked about.
transaction_id = "0c2d8359e90ef40bc49973c5bab23148262ddf880caf4a802602e8ce5702fcc1"
vout = 0
script_pub_key = "76a914afc3e518577316386188af748a816cd14ce333f288ac"
Extract parameters
The above script pub key
is locking what we call a UTXO which stands for Unspent Transaction Output. The sum of all UTXOs in a wallet is what can be considered as the balance.
Spending from a UTXO invalidates it, creating new ones in the process.
The current UTXO contains 500 000
sats. We intend to send 1000
sats to a new address. This leaves 499 000
sats remaining. They must end up in two other destinations: the change
and the transaction fee
. A fee also has to be provided, we’re gonna leave 500
sats for the miners, leaving 498 500
in the change address:
original_amount = 500_000
destination_amount = 1_000
fee = 500
change_amount = original_amount - destination_amount - fee
Let’s create two public keys: a destination
and a change
.
destination_public_key =
master_private_key
|> PrivateKey.from_derivation_path!("m/44'/1'/0'/0/1")
|> PublicKey.from_private_key()
change_public_key =
master_private_key
|> PrivateKey.from_derivation_path!("m/44'/1'/0'/0/1")
|> PublicKey.from_private_key()
Create a transaction
Now we’ve got everything we need to create a new transaction and sign it. Let’s kill two birds in one shot. The following code will create a transaction specification and sign it with the help of the private key.
%Transaction.Spec{}
|> Transaction.Spec.add_input!(
txid: transaction_id,
vout: vout,
redeem_script: script_pub_key
)
|> Transaction.Spec.add_output(
receive_public_key
|> PublicKey.hash()
|> Script.Types.P2pkh.create(),
destination_amount
)
|> Transaction.Spec.add_output(
change_public_key
|> PublicKey.hash()
|> Script.Types.P2pkh.create(),
change_amount
)
|> Transaction.Spec.sign_and_encode(receive_private_key)
Move funds
Now that we’ve got an encoded transaction, it is time to include it in the blockchain. To do so, we can use mempool.space
again by inserting it in this form: https://mempool.space/testnet/tx/push.
In the present context, it returned 05517750a78fb8c38346b1bf5908d71abe728811b643105be6595e11a9392373
. This is the transaction that spends the fund and can be seen on the transaction explorer
Conclusion
From now on, the previous 500 000 sats transaction is considered as spent and can no longer be called a UTXO. Two new ones have been created:
- The destination containing 1000 sats
- The change containing 498 500 sats
The remaining 500 sats have been donated to the miner. It can be seen on mempool.space
in the header of the transaction screen.