Published in


Sending Bitcoin with Ruby

Super simplified version! WAOW

  • bitcoind running with -rpcallowip , configured with rpcusername 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: or whatever version you’re using.

Concept (simplified)

  • 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.
  • 1addr has 2 BTC
  • 2addr has 1.5BTC
  • 3addr has 0.5BTC
  • 4addr has 1BTC
  • Inputs 3addr = 0.5BTC
  • Outputs 1some1 = 0.01BTC
  • Inputs 3addr = 0.5BTC
  • Outputs 1some1 = 0.01BTC | 5addr = 0.49BTC
  • Inputs 3addr = 0.5BTC
  • Outputs 1some1 = 0.01BTC | 5addr = (0.49BTC — 0.000178BTC)

Getting the fees

module Bitcoinzzz
class GetFees

AVE_TX_BYTES = 250.0
client =<your creds here>)
# whats the fee for 3 blocks?
response = client.request("estimatesmartfee", 3)
if response["errors"].any?
# ... do something

# 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)

Sending bitcoin with the fees

Getting your unspent money (What can I use for my inputs?)

# listunspent is another bitcoind JSON RPC call, it lists all the 
# previous txs/addresses that you contains your
unspent = client.request("listunspent")
#=> [
"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

Filter what you’ll use (What can I use for my inputs?)

# what you want your recipient to actually receive
receivable_amount = 0.01
# sample fee from Bitcoinzzz::GetFees.()
fee = 0.000178
# filter all spendable
spendable = do |output|
output if output["spendable"]
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 = do |output|
if total_usable < sending_amount
total_usable += output["amount"]
# 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?)

receiving_address = "SOME_ADDRESS_HERE"# getrawchangeaddress is another bitcoind JSON RPC call that gives 
# you a new address where you can send change to
change_address = client.request(
"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)

ins = do |output|
"txid" => output["txid"],
"vout" => output["vout"],
outs = [
{ destination_address => receivable_amount },
{ change_address => change_amount },
#=> [
{ "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(
0, # locktime
true, # replaceable

Sign the transaction

# signrawtransactionwithwallet is a bitcoind JSON RPC call that will 
# sign your transaction with the keys for those addresses
resp = client.request(
if resp["errors"].present?
raise StandardError, "Error with signing transaction - #{resp}"
signed_tx = resp["hex"]

Send the transaction

# you can check the tx_id returned in a blockchain explorer like 
# or
tx_id = client.request("sendrawtransaction", signed_tx)

Testing Sending Bitcoin

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<test_net_credentials>)
let(:address) { client.request("getnewaddress") }
"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(
true, # verbose, gets you json instead of a hex string

# parse that remote_tx and do your expectations

Next steps

  • 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.



Transforming money services with cryptocurrencies

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Ace Subido

Father. Husband. Likes video games. Software Engineer @ Shopify. I like writing notes to myself.