Current section

Files

Jump to
gold lib gold.ex
Raw

lib/gold.ex

defmodule Gold do
use GenServer
require Logger
alias Gold.Config
alias Gold.Transaction
##
# Client-side
##
@doc """
Starts GenServer link with Gold server.
"""
def start_link(config), do: GenServer.start_link(__MODULE__, config)
@doc """
Returns basic info about wallet and connection
"""
def getinfo(pid) do
case GenServer.call(pid, {:getinfo, []}) do
{:ok, info} ->
{:ok, info}
otherwise ->
otherwise
end
end
@doc """
Returns basic info about wallet and connection, raising an exeption on failure.
"""
def getinfo!(pid) do
{:ok, info} = getinfo(pid)
info
end
@doc """
Returns wallet's total available balance.
"""
def getbalance(pid), do: getbalance(pid, "")
@doc """
Returns wallet's total available balance, raising an exception on failure.
"""
def getbalance!(pid), do: getbalance!(pid, "")
@doc """
Returns wallet's total available balance.
"""
def getbalance(pid, account), do: getbalance(pid, account, 1)
@doc """
Returns wallet's total available balance, raising an exception on failure.
"""
def getbalance!(pid, account), do: getbalance!(pid, account, 1)
@doc """
Returns wallet's total available balance.
"""
def getbalance(pid, account, confirmations), do: getbalance(pid, account, confirmations, false)
@doc """
Returns wallet's total available balance, raising an exception on failure.
"""
def getbalance!(pid, account, confirmations), do: getbalance!(pid, account, confirmations, false)
@doc """
Returns wallet's total available balance.
"""
def getbalance(pid, account, confirmations, watchonly) do
case GenServer.call(pid, {:getbalance, [account, confirmations, watchonly]}) do
{:ok, balance} ->
{:ok, btc_to_decimal(balance)}
otherwise ->
otherwise
end
end
@doc """
Returns server's total available balance, raising an exception on failure.
"""
def getbalance!(pid, account, confirmations, watchonly) do
{:ok, balance} = getbalance(pid, account, confirmations, watchonly)
balance
end
@doc """
Returns a new bitcoin address for receiving payments.
"""
def getnewaddress(pid), do: getnewaddress(pid, "")
@doc """
Returns a new bitcoin address for receiving payments, raising an exception on failure.
"""
def getnewaddress!(pid), do: getnewaddress!(pid, "")
@doc """
Returns a new bitcoin address for receiving payments.
"""
def getnewaddress(pid, account), do: GenServer.call(pid, {:getnewaddress, [account]})
@doc """
Returns a new bitcoin address for receiving payments, raising an exception on failure.
"""
def getnewaddress!(pid, account) do
{:ok, address} = getnewaddress(pid, account)
address
end
@doc """
Returns the account associated with the given address.
"""
def getaccount(pid, address), do: GenServer.call(pid, {:getaccount, [address]})
@doc """
Returns the account associated with the given address, raising an exception on failure.
"""
def getaccount!(pid, address) do
{:ok, account} = getaccount(pid, address)
account
end
@doc """
Returns most recent transactions in wallet.
"""
def listtransactions(pid), do: listtransactions(pid, "*")
@doc """
Returns most recent transactions in wallet, raising an exception on failure.
"""
def listtransactions!(pid), do: listtransactions!(pid, "*")
@doc """
Returns most recent transactions in wallet.
"""
def listtransactions(pid, account), do: listtransactions(pid, account, 10)
@doc """
Returns most recent transactions in wallet, raising an exception on failure.
"""
def listtransactions!(pid, account), do: listtransactions!(pid, account, 10)
@doc """
Returns most recent transactions in wallet.
"""
def listtransactions(pid, account, limit), do: listtransactions(pid, account, limit, 0)
@doc """
Returns most recent transactions in wallet, raising an exception on failure.
"""
def listtransactions!(pid, account, limit), do: listtransactions!(pid, account, limit, 0)
@doc """
Returns most recent transactions in wallet.
"""
def listtransactions(pid, account, limit, offset), do: listtransactions(pid, account, limit, offset, false)
@doc """
Returns most recent transactions in wallet, raising an exception on failure.
"""
def listtransactions!(pid, account, limit, offset), do: listtransactions!(pid, account, limit, offset, false)
@doc """
Returns most recent transactions in wallet.
"""
def listtransactions(pid, account, limit, offset, watchonly) do
case GenServer.call(pid, {:listtransactions, [account, limit, offset, watchonly]}) 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!(pid, account, limit, offset, watchonly) do
{:ok, transactions} = listtransactions(pid, account, limit, offset, watchonly)
transactions
end
@doc """
Get detailed information about in-wallet transaction.
"""
def gettransaction(pid, txid) do
case GenServer.call(pid, {: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!(pid, txid) do
{:ok, tx} = gettransaction(pid, txid)
tx
end
@doc """
Send an amount to a given address.
"""
def sendtoaddress(pid, address, %Decimal{} = amount) do
GenServer.call(pid, {:sendtoaddress, [address, amount]})
end
@doc """
Send an amount to a given address, raising an exception on failure.
"""
def sendtoaddress!(pid, address, %Decimal{} = amount) do
{:ok, txid} = sendtoaddress(pid, address, amount)
txid
end
@doc """
Add an address or pubkey script to the wallet without the associated private key.
"""
def importaddress(pid, address), do: importaddress(pid, address, "")
@doc """
Add an address or pubkey script to the wallet without the associated private key,
raising an exception on failure.
"""
def importaddress!(pid, address), do: importaddress!(pid, address, "")
@doc """
Add an address or pubkey script to the wallet without the associated private key.
"""
def importaddress(pid, address, account), do: importaddress(pid, address, account, true)
@doc """
Add an address or pubkey script to the wallet without the associated private key,
raising an exception on failure.
"""
def importaddress!(pid, address, account), do: importaddress!(pid, address, account, true)
@doc """
Add an address or pubkey script to the wallet without the associated private key.
"""
def importaddress(pid, address, account, rescan) do
GenServer.call(pid, {: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!(pid, address, account, rescan) do
{:ok, _} = importaddress(pid, address, account, rescan)
:ok
end
@doc """
Mine block immediately. Blocks are mined before RPC call returns.
"""
def generate(pid, amount) do
GenServer.call(pid, {:generate, [amount]})
end
@doc """
Mine block immediately. Blocks are mined before RPC call returns. Raises an
exception on failure.
"""
def generate!(pid, amount) do
{:ok, result} = generate(pid, amount)
result
end
##
# Server-side
##
def handle_call(request, _from, config)
when is_atom(request), do: handle_rpc_request(request, [], config)
def handle_call({request, params}, _from, config)
when is_atom(request) and is_list(params), do: handle_rpc_request(request, params, config)
##
# Internal functions
##
defp handle_rpc_request(method, params, config) when is_atom(method) do
%Config{hostname: hostname, port: port, user: user, password: password} = config
command = %{"jsonrpc": "2.0",
"method": to_string(method),
"params": params,
"id": 1}
headers = ["Authorization": "Basic " <> Base.encode64(user <> ":" <> password)]
Logger.debug "Bitcoin RPC request for method: #{method}, params: #{inspect params}"
case HTTPoison.post("http://" <> hostname <> ":" <> to_string(port) <> "/", JSON.encode!(command), headers) do
{:ok, %{status_code: 200, body: body}} ->
case JSON.decode!(body) do
%{"error" => nil, "result" => result} -> {:reply, {:ok, result}, config}
%{"error" => error} -> {:reply, {:error, error}, config}
end
{:ok, %{status_code: code, body: error}} ->
{:reply, handle_error(code, error), config}
{:error, %HTTPoison.Error{reason: reason}} ->
Logger.debug "Bitcoin RPC HTTP error: #{reason}"
{:reply, {:error, status: :connection_closed, error: reason}, config}
otherwise ->
Logger.error "Bitcoin RPC unexpected response: #{inspect otherwise}"
{:reply, otherwise, config}
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}"
%{"error" => %{"message" => message}} = JSON.decode!(error)
{:error, %{status: status, error: message}}
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}
|> Decimal.round(8)
end
def btc_to_decimal(nil), do: nil
end