BloomX
Published in

BloomX

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: https://bitcoincore.org/en/doc/0.17.0/ 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
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

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")
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
},
...
]

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 = unspent.map do |output|
output if output["spendable"]
end.compact
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?)

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(
"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)

ins = usable.map do |output|
{
"txid" => output["txid"],
"vout" => output["vout"],
}
end
outs = [
{ 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

# 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}"
end
signed_tx = resp["hex"]

Send the transaction

# 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

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

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