Current section

Files

Jump to
gold lib gold.ex
Raw

lib/gold.ex

defmodule Gold do
@moduledoc """
Opinionated interface to Bitcoin core JSON-RPC API.
Currently in MVP mode: architecture is ready and stable, doesn't fully implement
all of the RPC commands yet.
"""
use Application
require Logger
alias Gold.Transaction
def start(_type, _args) do
import Supervisor.Spec, warn: false
opts = [strategy: :one_for_one, name: Gold.Supervisor]
Supervisor.start_link([], opts)
end
@doc """
Returns wallet's total available balance, raising an exception on failure.
"""
def getbalance(name, account \\ nil)
def getbalance(name, nil) do
call(name, :getbalance) |> handle_getbalance
end
def getbalance(name, account) do
call(name, {:getbalance, [account]}) |> handle_getbalance
end
defp handle_getbalance({:ok, balance}), do:
{:ok, btc_to_decimal(balance)}
defp handle_getbalance(otherwise), do:
otherwise
@doc """
Returns server's total available balance, raising an exception on failure.
"""
def getbalance!(name, account \\ nil) do
{:ok, balance} = getbalance(name, account)
balance
end
@doc """
Returns a new bitcoin address for receiving payments.
"""
def getnewaddress(name, account \\ ""), do: call(name, {:getnewaddress, [account]})
@doc """
Returns a new bitcoin address for receiving payments, raising an exception on failure.
"""
def getnewaddress!(name, account \\ "") do
{:ok, address} = getnewaddress(name, account)
address
end
@doc """
Returns the account associated with the given address.
"""
def getaccount(name, address), do: call(name, {:getaccount, [address]})
@doc """
Returns the account associated with the given address, raising an exception on failure.
"""
def getaccount!(name, address) do
{:ok, account} = getaccount(name, address)
account
end
@doc """
Returns most recent transactions in wallet.
"""
def listtransactions(name, account \\ "*", limit \\ 10, offset \\ 0) do
case call(name, {:listtransactions, [account, limit, offset]}) do
{:ok, transactions} ->
{:ok, Enum.map(transactions, &Transaction.from_json/1)}
otherwise ->
otherwise
end
end
@doc """
Returns most recent transactions in wallet, raising an exception on failure.
"""
def listtransactions!(name, account \\ "*", limit \\ 10, offset \\ 0) do
{:ok, transactions} = listtransactions(name, account, limit, offset)
transactions
end
@doc """
Get detailed information about in-wallet transaction.
"""
def gettransaction(name, txid) do
case call(name, {:gettransaction, [txid]}) do
{:ok, transaction} ->
{:ok, Transaction.from_json transaction}
otherwise ->
otherwise
end
end
@doc """
Get detailed information about in-wallet transaction, raising an exception on
failure.
"""
def gettransaction!(name, txid) do
{:ok, tx} = gettransaction(name, txid)
tx
end
@doc """
Send an amount to a given address.
"""
def sendtoaddress(name, address, %Decimal{} = amount) do
call(name, {:sendtoaddress, [address, amount]})
end
@doc """
Send an amount to a given address, raising an exception on failure.
"""
def sendtoaddress!(name, address, %Decimal{} = amount) do
{:ok, txid} = sendtoaddress(name, address, amount)
txid
end
@doc """
Add an address or pubkey script to the wallet without the associated private key.
"""
def importaddress(name, address, account \\ "", rescan \\ true) do
call(name, {:importaddress, [address, account, rescan]})
end
@doc """
Add an address or pubkey script to the wallet without the associated private key,
raising an exception on failure.
"""
def importaddress!(name, address, account \\ "", rescan \\ true) do
{:ok, _} = importaddress(name, address, account, rescan)
:ok
end
@doc """
Mine block immediately. Blocks are mined before RPC call returns.
"""
def generate(name, amount) do
call(name, {:generate, [amount]})
end
@doc """
Mine block immediately. Blocks are mined before RPC call returns. Raises an
exception on failure.
"""
def generate!(name, amount) do
{:ok, result} = generate(name, amount)
result
end
@doc """
https://bitcoin.org/en/developer-reference#getblock
"""
def getblock(name, hash) do
call(name, {:getblock, [hash]})
end
def getblock!(name, hash) do
{:ok, block} = getblock(name, hash)
block
end
@doc """
https://bitcoin.org/en/developer-reference#getblockhash
"""
def getblockhash(name, index) do
call(name, {:getblockhash, [index]})
end
def getblockhash!(name, index) do
{:ok, blockhash} = getblockhash(name, index)
blockhash
end
@doc """
https://bitcoin.org/en/developer-reference#getrawtransaction
"""
def getrawtransaction(name, txid, verbose \\ 1) do
call(name, {:getrawtransaction, [txid, verbose]})
end
def getrawtransaction!(name, txid, verbose \\ 1) do
{:ok, tx} = getrawtransaction(name, txid, verbose)
tx
end
@doc """
https://bitcoin.org/en/developer-reference#getblockcount
"""
def getblockcount(name) do
call(name, {:getblockcount, []})
end
def getblockcount!(name) do
{:ok, count} = getblockcount(name)
count
end
@doc """
https://bitcoin.org/en/developer-reference#gettxout
"""
def gettxout(name, txid, n \\ 1) do
call(name, {:gettxout, [txid, n]})
end
def gettxout!(name, txid, n \\ 1) do
{:ok, txout} = gettxout(name, txid, n)
txout
end
@info_methods ~w(getblockchaininfo
getmempoolinfo
gettxoutsetinfo
getinfo
getmemoryinfo
getmininginfo
getnetworkinfo
getpeerinfo
getwalletinfo)a
Enum.each @info_methods, fn(method) ->
@doc """
https://bitcoin.org/en/developer-reference##{method}"
"""
def unquote(method)(name) do
call(name, {unquote(method), []})
end
@doc """
https://bitcoin.org/en/developer-reference##{method}"
"""
def unquote(:"#{method}!")(name) do
{:ok, info} = call(name, {unquote(method), []})
info
end
end
@doc """
Call generic RPC command
"""
def call(name, method) when is_atom(method), do:
call(name, {method, []})
def call(name, {method, params}) when is_atom(method) do
case load_config(name) do
:undefined ->
{:error, {:invalid_configuration, name}}
config ->
handle_rpc_request(method, params, config)
end
end
##
# Internal functions
##
defp handle_rpc_request(method, params, config) when is_atom(method) do
%{hostname: hostname, port: port, user: user, password: password} = config
Logger.debug "Bitcoin RPC request for method: #{method}, params: #{inspect params}"
params = PoisonedDecimal.poison_params(params)
command = %{"jsonrpc": "2.0",
"method": to_string(method),
"params": params,
"id": 1}
headers = ["Authorization": "Basic " <> Base.encode64(user <> ":" <> password)]
options = [timeout: 30000, recv_timeout: 20000]
case HTTPoison.post("http://" <> hostname <> ":" <> to_string(port) <> "/", Poison.encode!(command), headers, options) do
{:ok, %{status_code: 200, body: body}} ->
%{"error" => nil, "result" => result} = Poison.decode!(body)
{:ok, result}
{:ok, %{status_code: code, body: body}} ->
handle_error(code, body)
end
end
@statuses %{401 => :forbidden, 404 => :notfound, 500 => :internal_server_error}
defp handle_error(status_code, error) do
status = @statuses[status_code]
Logger.debug "Bitcoin RPC error status #{status}: #{error}"
case Poison.decode(error) do
{:ok, %{"error" => %{"message" => message}}} ->
{:error, %{status: status, error: message}}
{:error, :invalid, _pos} ->
{:error, %{status: status, error: error}}
{:error, {:invalid, _token, _pos}} ->
{:error, %{status: status, error: error}}
end
end
@doc """
Converts a float BTC amount to an Decimal.
"""
def btc_to_decimal(btc) when is_float(btc) do
satoshi_per_btc = :math.pow(10, 8)
# Convert the bitcoins to integer to avoid any precision loss
satoshi = round(btc * satoshi_per_btc)
# Now construct a decimal
%Decimal{sign: if(satoshi < 0, do: -1, else: 1), coef: abs(satoshi), exp: -8}
end
def btc_to_decimal(nil), do: nil
defp load_config(name) do
case :application.get_env(:gold, name) do
{:ok, config} -> Enum.into(config, %{})
:undefined -> :undefined
end
end
end