Sending Bitcoin with Ruby
Super simplified version! WAOW
“Hmm, okay so we want to send BTC programmatically? It shouldn’t be that hard, let me read some docs and look at libraries”
(AFTER 3 HOURS OF BEING MENTALLY DRAINED)
One of the basic things you’d want to achieve in building on top of the Bitcoin ecosystem is to send Bitcoin. With, that I’ve written down some things that I’ve learned along the way in the most simplified way I can think of.
Here’s a step by step guide in sending Bitcoin with ruby by leveraging bitcoind’s JSON RPC endpoint.
You’ll need to have:
- bitcoind running with
-rpcallowip
, configured withrpcusername and rpcpassword
and more importantly-testnet
. This will act as your ‘hot wallet’. bitcoiner
gem, a wrapper for bitcoind’s JSON RPC interface- a testnet wallet with testnet accounts.
- an open tab with this: https://bitcoincore.org/en/doc/0.17.0/ or whatever version you’re using.
What if someone gets access to my private keys? What if someone brute forces the username/password of my bitcoind’s JSON RPC endpoint? etc.
Security is another topic worth another post (limit the surface of attack, pod/vm-level restrictions, network-level restraints, multiple bitcoind’s, not keeping large amounts of bitcoin in your hot wallet, etc.) but for now, let’s send some bitcoins 😄
Concept (simplified)
A transaction in the Bitcoin network is made of in’s and out’s.
- Inputs are where you get your BTC from. These are past transactions with addresses you can control.
- Outputs are where they go. Usually there’s 2. 1 for the recipient, and 1 for you. “Why me?” because you’ll need to send the difference back to yourself. More on that later.
Example
You have 5BTC in your wallet, it’s split up in 4 different addresses. yeah, I mean, I’m actually saying addresses, but it’s actually “unspent transaction outputs” ❓🙄❓🙄❓ Stick with me for now and I’ll point this out later.
- 1addr has 2 BTC
- 2addr has 1.5BTC
- 3addr has 0.5BTC
- 4addr has 1BTC
You want to send 0.01BTC to someone named Person A, you’d need to use some of your BTC in those 4 address. Let’s say we want to use 3addr, and Person A gave you their address: 1some1. Your transaction would look like this:
- Inputs
3addr = 0.5BTC
- Outputs
1some1 = 0.01BTC
This isn’t complete though, so what happens to the 0.49BTC? Well you’d need to send it back to yourself of course! Otherwise it’ll all go to the miners. Best to generate a new address and put it there. Lets call that new address as 5addr. Now our transaction looks like this:
- Inputs
3addr = 0.5BTC
- Outputs
1some1 = 0.01BTC | 5addr = 0.49BTC
What about those transaction fees? How do I set them?
The transaction fee is the difference between the sum of your inputs and sum of your output amounts. Let’s say the transaction fees would be 0.000178BTC
So the final transaction would look like:
- Inputs
3addr = 0.5BTC
- Outputs
1some1 = 0.01BTC | 5addr = (0.49BTC — 0.000178BTC)
If you still didn’t get it, it’s okay! If you understand concepts better when you’re writing the code and tests, then it’ll probably click as you go along 😃
Getting the fees
First we’ll want to set what type of fees are “fast”, so that we can use the right number when creating the transaction. We can stuff this code inside some simple service class.
module Bitcoinzzz
class GetFees
AVE_TX_BYTES = 250.0 def self.call
client = Bitcoiner.new(<your creds here>) # whats the fee for 3 blocks?
response = client.request("estimatesmartfee", 3) if response["errors"].any?
# ... do something
end
# estimatesmartfee is a bitcoind JSON RPC call
# it returns the optimal fee per kB (1000 bytes), given the
# number of blocks you want your transaction to confirm
#
# we only want the fee for 250bytes (average tx size).
# you can also compute for tx size if you want to.
fee = response["feerate"].to_d * (AVE_TX_SIZE / 1000.0)
fee
end end
end
Sending bitcoin with the fees
Can’t we just use sendtoaddress? (sendtoaddress is a bitcoind JSON RPC call)
Yes you can! That’s definitely the easiest way to do this, but if you want more control on fees, outputs being spent; splitting up what sendtoaddress
does would be one step to that direction (a.k.a. small refactors using the Ship of Theseus method.), and we get to learn along the way too!
So here’s the steps to mimic sendtoaddress
similarly
Getting your unspent money (What can I use for my inputs?)
To build the transaction you’ll need to get “spendable outputs”. To simplify the explanation, this is where the BTC will come from, a list of unspent addresses in the bitcoind wallet.
# listunspent is another bitcoind JSON RPC call, it lists all the
# previous txs/addresses that you contains your
unspent = client.request("listunspent")unspent.inspect
#=> [
{
"address": "1addr",
"amount": 2.0,
"confirmations": 1000,
"desc": "some_text",
"redeemScript": "some_redeem_script",
"safe": true,
"scriptPubKey": "some_script_pubkey",
"solvable": true,
"spendable": true,
"txid": "the_remote_txid",
"vout": 0
},
{
"address": "2addr",
"amount": 1.5,
"confirmations": 1000,
"desc": "some_text",
"redeemScript": "some_redeem_script",
"safe": true,
"scriptPubKey": "some_script_pubkey",
"solvable": true,
"spendable": true,
"txid": "the_remote_txid",
"vout": 0
},
...
]
Remember earlier I said this?
yeah, I mean, I’m actually saying addresses, but it’s actually “unspent transaction outputs” ❓🙄❓🙄❓ Stick with me for now and I’ll point this out later.
Sometimes you will see here 2 items with the same address but different vout values. It means that the address was used multiple times. So what this list really represents are unspent transaction outputs (UTXO)
. These are previous transactions where the outputs are addresses that your bitcoind ‘hot wallet’ controls a.k.a. someone sending your hot wallet some BTC, or “change amounts” from previous transactions you made (more on that later)
Filter what you’ll use (What can I use for my inputs?)
Presented with this list, just grab what’s “spendable”. It means that this transaction can be used by bitcoind for sending (underneath, all that means is that the bitcoind has the keys to use that address).
# what you want your recipient to actually receive
receivable_amount = 0.01# sample fee from Bitcoinzzz::GetFees.()
fee = 0.000178# filter all spendable
spendable = unspent.map do |output|
output if output["spendable"]
end.compact
If you’re sending 0.01BTC + 0.000178BTC
, you won’t need all the “spendable outputs”, so just get the right amount of outputs.
total_usable = 0# total amount you'll spend: 0.01 + 0.000178
sending_amount = receivable_amount + fee# just get the right amount of outputs
usable = spendable.map do |output|
if total_usable < sending_amount
total_usable += output["amount"]
output
end
end.compact# based on the unspent example, 'usable' array will only contain the 1addr hash by now. You're only sending 0.01BTC + 0.000178BTC anyway. 1addr contains 2BTC
Get a change address (Where will I send it? My outputs?)
Remember what I said earlier about outputs?
You’ll need to send the difference back to yourself. More on that later.
Of course you only want to send 0.01BTC + 0.000178BTC (fees)
. Right now, you have 0.5BTC
worth of (ins) that funds your transaction! You’ll want to send the rest back to your own wallet. This is called a change address, you can get one using bitcoind’s JSON RPC again.
receiving_address = "SOME_ADDRESS_HERE"# getrawchangeaddress is another bitcoind JSON RPC call that gives
# you a new address where you can send change tochange_address = client.request(
"getrawchangeaddress",
"bech32", # bech32 adoption would be very nice, lower tx size.
)# Lets compute what we'll send back to yourself and what we'll send
# to the change_address
#
# total_usable = 2BTC from 1addr
# sending_amount = 0.01BTC + 0.000178 (fees)change_amount = total_usable - sending_amount
Build the transaction hash (Putting my outputs and inputs together)
Once you’ve determined where you’ll get the money from (inputs), and where it’ll go (outputs). Let’s prepare a json object for createrawtransaction
ins = usable.map do |output|
{
"txid" => output["txid"],
"vout" => output["vout"],
}
endouts = [
{ destination_address => receivable_amount },
{ change_address => change_amount },
]
outs.inspect
#=> [
{ "1some1" => 0.01 },
{ "change_address" => 1.98922 },
]tx_to_submit = { ins: ins, outs: outs }# createrawtransaction builds your transaction ready for signing.
# the result will be a hex-encoded string
raw_tx = client.request(
"createrawtransaction",
tx_to_submit[:ins],
tx_to_submit[:outs],
0, # locktime
true, # replaceable
)
Sign the transaction
After getting the hex-encoded transaction via createrawtransaction
, sign it.
# signrawtransactionwithwallet is a bitcoind JSON RPC call that will
# sign your transaction with the keys for those addresses
resp = client.request(
"signrawtransactionwithwallet",
raw_tx,
)if resp["errors"].present?
raise StandardError, "Error with signing transaction - #{resp}"
endsigned_tx = resp["hex"]
Send the transaction
You can now send the signed transaction to the local bitcoind node and to the network!
# you can check the tx_id returned in a blockchain explorer like
# blockstream.info or blockchain.com
tx_id = client.request("sendrawtransaction", signed_tx)
Testing Sending Bitcoin
Yey! Let’s grab our handy-dandy vcr: { record: :once }
Here’s a simple test for all that work we just did.
require "rails_helper"# Put all of the things that we did in some decoupled class so that it's easy to test. Here it assumes we can just write an integration test for a Bitcoinzzz::Send class that does everything we talked aboutmodule Bitcoinzzz
RSpec.describe Send do let(:fee) { Bitcoinzzz::GetFees.() }
let(:client) do
Bitcoiner.new(<test_net_credentials>)
end
let(:address) { client.request("getnewaddress") } it(
"sends BTC",
vcr: {
record: :once,
match_requests_on: %i[body uri method],
},
) do
current_balance = client.request("getbalance") result_tx_id = described_class.(
tx_fee_in_btc: fee,
destination_address: address,
receivable_amount: 0.002,
) after_send_balance = client.request("getbalance") # expect the balance differences to be for the tx_fee since we
# sent it back to our self
expected_diff = current_balance - after_send_balance
expect(expected_diff.round(7).to_d).to eq fee expect(result_tx_id).not_to be_nil
expect(result_tx_id).to be_a String
remote_tx = client.request(
"getrawtransaction",
result_tx_id,
true, # verbose, gets you json instead of a hex string
)
# parse that remote_tx and do your expectations
end end
end
Next steps
There you go! You can now send Bitcoin with Ruby. Although, what if this service class gets called a lot? And at the same time?
- You can probably use something like
sidekiq-unique-jobs
and line all the withdrawal requests in a worker queue so that you won’t have race conditions. - You can also have balance validations at the start via
getbalance
if the wallet still has spendable money and you can fail the job accordingly.
There are more ways but you can definitely make this a safe service class to use via defensive programming.
Another next-level implementation for this is to not rely on bitcoind’s wallet for getting the unspent outputs, building and signing the transaction hashes. This allows you to feed multiple inputs coming from a cold wallet, and multi-sig keys from different signers.
You can use lower-level libraries such as bitcoin-ruby
to achieve this, but you’ll have to keep track of your addresses, keys, transactions on your own a.k.a. implementing/complementing bitcoind’s wallet capabilities.
Hopefully this guide has eased you into how to work with Bitcoin and bitcoind.