Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Manual Bitcoin Management Example

livebooks/wallet_operations/index.livemd

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.

Here is an example of 10000 testnet sats, or 0.0001 tBTC, being ready to spend with 14 confirmations.

confirmed transaction

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:

transaction details

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.