defmodule Gold do 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) do getbalance(name, nil) end 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), do: getnewaddress(name, "") @doc """ Returns a new bitcoin address for receiving payments, raising an exception on failure. """ def getnewaddress!(name), do: getnewaddress!(name, "") @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), do: listtransactions(name, "*") @doc """ Returns most recent transactions in wallet, raising an exception on failure. """ def listtransactions!(name), do: listtransactions!(name, "*") @doc """ Returns most recent transactions in wallet. """ def listtransactions(name, account), do: listtransactions(name, account, 10) @doc """ Returns most recent transactions in wallet, raising an exception on failure. """ def listtransactions!(name, account), do: listtransactions!(name, account, 10) @doc """ Returns most recent transactions in wallet. """ def listtransactions(name, account, limit), do: listtransactions(name, account, limit, 0) @doc """ Returns most recent transactions in wallet, raising an exception on failure. """ def listtransactions!(name, account, limit), do: listtransactions!(name, account, limit, 0) @doc """ Returns most recent transactions in wallet. """ def listtransactions(name, account, limit, offset) 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, offset) 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), do: importaddress(name, address, "") @doc """ Add an address or pubkey script to the wallet without the associated private key, raising an exception on failure. """ def importaddress!(name, address), do: importaddress!(name, address, "") @doc """ Add an address or pubkey script to the wallet without the associated private key. """ def importaddress(name, address, account), do: importaddress(name, 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!(name, address, account), do: importaddress!(name, address, account, true) @doc """ Add an address or pubkey script to the wallet without the associated private key. """ def importaddress(name, address, account, rescan) 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) 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 @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 def unquote(:"#{method}!")(name) do call(name, {unquote(method), []}) end end @doc """ https://bitcoin.org/en/developer-reference#getrawtransaction """ def getrawtrasaction(name, txid, verbose \\ 1) do call(name, {:getrawtransaction, [txid, verbose]}) end def getrawtransaction!(name, txid, verbose \\ 1) do {:ok, tx} = getrawtrasaction(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 @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 ## # 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 %{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 = code, body: body}} -> case Poison.decode!(body) do %{"error" => nil, "result" => result} -> {:ok, result} %{"error" => error} -> handle_error(code, error) end {:ok, %{status_code: code, body: body}} -> handle_error(code, body) otherwise -> Logger.error "Bitcoin RPC unexpected response: #{inspect otherwise}" {:error, otherwise} 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