├── logo.png ├── logo-wide.png ├── test ├── rai_ex_test.exs ├── test_helper.exs └── rai_ex │ └── block_test.exs ├── .travis.yml ├── config └── config.exs ├── lib ├── rai_ex │ ├── application.ex │ ├── helpers.ex │ ├── circuit_breaker.ex │ ├── tools │ │ ├── validator.ex │ │ └── base_32.ex │ ├── rpc.ex │ ├── block.ex │ └── tools.ex └── rai_ex.ex ├── README.md ├── mix.exs ├── .gitignore ├── mix.lock └── .credo.exs /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willHol/rai_ex/HEAD/logo.png -------------------------------------------------------------------------------- /logo-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willHol/rai_ex/HEAD/logo-wide.png -------------------------------------------------------------------------------- /test/rai_ex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RaiExTest do 2 | use ExUnit.Case 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: '1.5' 3 | otp_release: '20.0' 4 | script: 5 | - mix test 6 | - mix credo -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start(exclude: :skip) 2 | 3 | defmodule TestHelper do 4 | defmacro skip_offline() do 5 | case RaiEx.available_supply() do 6 | {:error, :econnrefused} -> 7 | quote do: @tag :skip 8 | _ -> 9 | quote do: @tag :dont_skip 10 | end 11 | end 12 | end -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | config :rai_ex, 6 | url: "http://localhost", 7 | port: 7076, 8 | min_receive: 1_000_000_000_000_000_000_000_000, 9 | breaker_max_error: 3, 10 | breaker_period: 1000 11 | -------------------------------------------------------------------------------- /lib/rai_ex/application.ex: -------------------------------------------------------------------------------- 1 | defmodule RaiEx.Application do 2 | @moduledoc false 3 | use Application 4 | 5 | def start(_type, _args) do 6 | children = [ 7 | RaiEx.CircuitBreaker, 8 | :hackney_pool.child_spec(:rai_dicee, [timeout: :infinity, max_connections: 5]) 9 | ] 10 | 11 | Supervisor.start_link(children, strategy: :one_for_one) 12 | end 13 | end -------------------------------------------------------------------------------- /lib/rai_ex/helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule RaiEx.Helpers do 2 | @moduledoc false 3 | 4 | def min_recv(), do: Application.get_env(:rai_ex, :min_receive, 1_000_000_000_000_000_000_000_000) 5 | 6 | def if_string_hex_to_binary([]), do: [] 7 | def if_string_hex_to_binary(binaries) when is_list(binaries) do 8 | [binary | rest] = binaries 9 | [if_string_hex_to_binary(binary) | if_string_hex_to_binary(rest)] 10 | end 11 | def if_string_hex_to_binary(binary) do 12 | if String.valid?(binary), do: Base.decode16!(binary), else: binary 13 | end 14 | 15 | 16 | def reverse(binary) when is_binary(binary), do: do_reverse(binary, <<>>) 17 | defp do_reverse(<<>>, acc), do: acc 18 | defp do_reverse(<< x :: binary-size(1), bin :: binary >>, acc), do: do_reverse(bin, x <> acc) 19 | 20 | def left_pad_binary(binary, bits) do 21 | <<0::size(bits), binary::bitstring>> 22 | end 23 | 24 | def right_pad_binary(binary, bits) do 25 | <> 26 | end 27 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![RaiEx](https://raw.githubusercontent.com/willHol/rai_ex/master/logo-wide.png) 2 | 3 | [![Build Status](https://travis-ci.org/willHol/rai_ex.svg?branch=master)](https://travis-ci.org/willHol/rai_ex) 4 | [![Hex.pm](https://img.shields.io/hexpm/v/rai_ex.svg)](https://hex.pm/packages/rai_ex) 5 | [![Inline docs](http://inch-ci.org/github/willHol/rai_ex.svg)](http://inch-ci.org/github/willHol/rai_ex) 6 | 7 | RaiEx is an *Elixir client* for managing a **RaiBlocks** node, here is an example: 8 | 9 | ```elixir 10 | alias RaiEx.{Block, Tools} 11 | 12 | account = "xrb_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3" 13 | 14 | # RPC mappings 15 | {:ok, %{"balance" => balance, "frontier" => frontier}} = RaiEx.account_info(account) 16 | {:ok, %{"key" => key}} = RaiEx.account_key(account) 17 | 18 | # Derive the first account from the wallet seed 19 | {priv, pub} = Tools.seed_account!("9F1D53E732E48F25F94711D5B22086778278624F715D9B2BEC8FB81134E7C904", 0) 20 | 21 | # Derives an "xrb_" address 22 | address = Tools.create_account!(pub) 23 | 24 | # Get the previous block hash 25 | {:ok, %{"frontier" => block_hash}} = RaiEx.account_info(address) 26 | 27 | block = %Block{ 28 | previous: block_hash, 29 | destination: "xrb_1aewtdjz8knar65gmu6xo5tmp7ijrur1fgtetua3mxqujh5z9m1r77fsrpqw", 30 | balance: 0 31 | } 32 | 33 | # Signs and broadcasts the block to the network 34 | block |> Block.sign(priv, pub) |> Block.send() 35 | 36 | ``` 37 | 38 | To get started read the [online documentation](https://hexdocs.pm/rai_ex/). 39 | 40 | ## Installation 41 | 42 | Add the following to your `mix.exs`: 43 | 44 | ```elixir 45 | def deps do 46 | [ 47 | {:rai_ex, "~> 0.3.0"} 48 | ] 49 | end 50 | ``` 51 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule RaiEx.Mixfile do 2 | use Mix.Project 3 | 4 | # Somewhat hacky way of setting env variables of ed25519 5 | Application.put_env(:ed25519, :hash_fn, {Blake2, :hash2b, [], []}) 6 | 7 | def project do 8 | [ 9 | app: :rai_ex, 10 | version: "0.3.1", 11 | elixir: "~> 1.5", 12 | start_permanent: Mix.env == :prod, 13 | deps: deps(), 14 | docs: docs(), 15 | package: package(), 16 | description: description(), 17 | source_url: "https://github.com/willHol/rai_ex", 18 | name: "RaiEx" 19 | ] 20 | end 21 | 22 | # Run "mix help compile.app" to learn about applications. 23 | def application do 24 | [ 25 | mod: {RaiEx.Application, []}, 26 | extra_applications: [:logger, :httpoison] 27 | ] 28 | end 29 | 30 | def docs do 31 | [ 32 | main: "RaiEx", 33 | logo: "logo.png", 34 | extras: ["README.md"] 35 | ] 36 | end 37 | 38 | defp description() do 39 | "An Elixir client for managing a RaiBlocks node." 40 | end 41 | 42 | def package do 43 | [ 44 | licenses: ["Apache 2.0"], 45 | maintainers: ["William Holmes"], 46 | links: %{"GitHub" => "https://github.com/willHol/rai_ex"}, 47 | source_url: "https://github.com/willHol/rai_ex" 48 | ] 49 | end 50 | 51 | # Run "mix help deps" to learn about dependencies. 52 | defp deps do 53 | [ 54 | {:poison, "~> 3.1"}, 55 | {:httpoison, "~> 0.13.0"}, 56 | {:decimal, "~> 1.4"}, 57 | {:blake2, "~> 1.0"}, 58 | {:ed25519, "~> 1.1"}, 59 | {:credo, "~> 0.8.8", only: :dev, runtime: false}, 60 | {:ex_doc, "~> 0.18", only: :dev, runtime: false}, 61 | {:inch_ex, "~> 0.5.6", only: [:dev, :test]} 62 | ] 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | /docs/ 5 | 6 | # If you run "mix test --cover", coverage assets end up here. 7 | /cover/ 8 | 9 | # The directory Mix downloads your dependencies sources to. 10 | /deps/ 11 | 12 | # Where 3rd-party dependencies like ExDoc output generated docs. 13 | /doc/ 14 | 15 | # Ignore .fetch files in case you like to edit your project deps locally. 16 | /.fetch 17 | 18 | # If the VM crashes, it generates a dump, let's ignore it too. 19 | erl_crash.dump 20 | 21 | /ex_doc 22 | /doc 23 | 24 | # Also ignore archive artifacts (built via "mix archive.build"). 25 | *.ez 26 | 27 | ### macOS ### 28 | *.DS_Store 29 | .AppleDouble 30 | .LSOverride 31 | 32 | # Icon must end with two \r 33 | Icon 34 | 35 | # Thumbnails 36 | ._* 37 | 38 | # Files that might appear in the root of a volume 39 | .DocumentRevisions-V100 40 | .fseventsd 41 | .Spotlight-V100 42 | .TemporaryItems 43 | .Trashes 44 | .VolumeIcon.icns 45 | .com.apple.timemachine.donotpresent 46 | 47 | # Directories potentially created on remote AFP share 48 | .AppleDB 49 | .AppleDesktop 50 | Network Trash Folder 51 | Temporary Items 52 | .apdisk 53 | 54 | ### Linux ### 55 | *~ 56 | 57 | # temporary files which can be created if a process still has a handle open of a deleted file 58 | .fuse_hidden* 59 | 60 | # KDE directory preferences 61 | .directory 62 | 63 | # Linux trash folder which might appear on any partition or disk 64 | .Trash-* 65 | 66 | # .nfs files are created when an open file is removed but is still being accessed 67 | .nfs* 68 | 69 | ### Windows ### 70 | # Windows thumbnail cache files 71 | Thumbs.db 72 | ehthumbs.db 73 | ehthumbs_vista.db 74 | 75 | # Folder config file 76 | Desktop.ini 77 | 78 | # Recycle Bin used on file shares 79 | $RECYCLE.BIN/ 80 | 81 | # Windows Installer files 82 | *.cab 83 | *.msi 84 | *.msm 85 | *.msp 86 | 87 | # Windows shortcuts 88 | *.lnk -------------------------------------------------------------------------------- /lib/rai_ex/circuit_breaker.ex: -------------------------------------------------------------------------------- 1 | defmodule RaiEx.CircuitBreaker do 2 | @moduledoc false 3 | 4 | use GenServer 5 | import HTTPoison, only: [request: 5] 6 | 7 | alias HTTPoison.Response 8 | alias HTTPoison.Error 9 | 10 | @max_error Application.get_env(:rai_dice, :breaker_max_error, 3) 11 | @period Application.get_env(:rai_dice, :breaker_period, 1000) 12 | 13 | def start_link(_) do 14 | GenServer.start_link(__MODULE__, :ok, name: __MODULE__) 15 | end 16 | 17 | def init(:ok) do 18 | {:ok, 0} 19 | end 20 | 21 | def post(url, body, headers \\ [], opts \\ []) do 22 | GenServer.call(__MODULE__, {:post, url, body, headers, opts}, :infinity) 23 | end 24 | 25 | # ==== GENSERVER CALLBACKS ==== # 26 | 27 | def handle_call({:post, url, body, headers, opts}, _from, error_count) do 28 | if blown?(error_count) do 29 | {:reply, :blown, error_count} 30 | else 31 | case make_request(:post, url, body, headers, opts) do 32 | {:ok, body} -> 33 | {:reply, {:ok, body}, error_count} 34 | {:error, reason} -> 35 | send(self(), :add_error) 36 | {:reply, {:error, reason}, error_count} 37 | end 38 | end 39 | end 40 | 41 | def handle_info(:add_error, error_count) do 42 | _ = schedule_error_remove(@period) 43 | {:noreply, error_count + 1} 44 | end 45 | 46 | def handle_info(:remove_error, error_count) do 47 | {:noreply, error_count - 1} 48 | end 49 | 50 | # ==== PRIVATE FUNCTIONS ==== # 51 | 52 | defp make_request(method, url, body, headers, opts) do 53 | case request(:post, url, body, headers, opts) do 54 | {:ok, %Response{status_code: 200, body: body}} -> {:ok, body} 55 | {:ok, %Response{status_code: code}} -> {:error, code} 56 | {:error, %Error{reason: reason}} -> {:error, reason} 57 | end 58 | end 59 | 60 | defp blown?(error_count), do: error_count >= @max_error 61 | 62 | defp schedule_error_remove(period) do 63 | Process.send_after(self(), :remove_error, period) 64 | end 65 | end -------------------------------------------------------------------------------- /lib/rai_ex/tools/validator.ex: -------------------------------------------------------------------------------- 1 | defmodule RaiEx.Tools.Validator do 2 | @moduledoc """ 3 | Provides functionality for run-time validation of rpc types. 4 | """ 5 | 6 | @type_checkers %{ 7 | :string => {Kernel, :is_binary}, 8 | :number => {Decimal, :decimal?}, 9 | :integer => {Kernel, :is_integer}, 10 | :list => {Kernel, :is_list}, 11 | :wallet => {__MODULE__, :is_hash}, 12 | :hash => {__MODULE__, :is_hash}, 13 | :hash_list => {__MODULE__, :is_hash_list}, 14 | :block => {__MODULE__, :is_hash}, 15 | :address => {__MODULE__, :is_address}, 16 | :address_list => {__MODULE__, :is_address_list}, 17 | :boolean => {Kernel, :is_boolean}, 18 | :any => {__MODULE__, :any} 19 | } 20 | 21 | @doc """ 22 | Validates the type types used by `RaiEx.RPC`. Raises `ArgumentError` 23 | if the types fail to validate. 24 | 25 | ## Examples 26 | 27 | iex> validate_types(["account" => :string, "count" => :integer], ["account" => "xrb_34bmpi65zr967cdzy4uy4twu7mqs9nrm53r1penffmuex6ruqy8nxp7ms1h1, "count" => 5]) 28 | :ok 29 | 30 | iex> validate_types(["account" => :string, "count" => :integer], ["account" => "xrb_34bmpi65zr967cdzy4uy4twu7mqs9nrm53r1penffmuex6ruqy8nxp7ms1h1, "count" => "10"]) 31 | ** (Elixir.ArgumentError) 32 | 33 | """ 34 | def validate_types!(should_be, is) do 35 | should_be = Enum.into(should_be, %{}) 36 | Enum.each(should_be, fn {param, type} -> 37 | {mod, fun} = @type_checkers[type] 38 | arg = is[String.to_atom(param)] 39 | 40 | unless apply(mod, fun, [arg]) do 41 | raise ArgumentError, message: """ 42 | #{param} is not of the correct type, should be type: #{type} 43 | """ 44 | end 45 | end) 46 | 47 | :ok 48 | end 49 | 50 | @doc false 51 | def is_hash(wallet) do 52 | is_binary(wallet) and String.length(wallet) == 64 and Regex.match?(~r/^[0-9A-F]+$/, wallet) 53 | end 54 | 55 | @doc false 56 | def is_address(addr) do 57 | is_binary(addr) and 58 | String.length(addr) == 64 and 59 | Regex.match?(~r/^[0-9a-z_]+$/, addr) and 60 | String.starts_with?(addr, "xrb_") 61 | end 62 | 63 | @doc false 64 | def is_address_list(addr_list) do 65 | if is_list(addr_list), do: addr_list |> Enum.all?(&(is_address(&1))), else: false 66 | end 67 | 68 | @doc false 69 | def is_hash_list(hash_list) do 70 | if is_list(hash_list), do: hash_list |> Enum.all?(&(is_hash(&1))), else: false 71 | end 72 | 73 | @doc false 74 | def any(_), do: true 75 | end 76 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"blake2": {:hex, :blake2, "1.0.1", "4435da3e2f5782c7230d78174da29591e6006b0dc8bf11c7f77488b19bc52b1b", [], [], "hexpm"}, 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [], [], "hexpm"}, 3 | "certifi": {:hex, :certifi, "2.0.0", "a0c0e475107135f76b8c1d5bc7efb33cd3815cb3cf3dea7aefdd174dabead064", [], [], "hexpm"}, 4 | "credo": {:hex, :credo, "0.8.10", "261862bb7363247762e1063713bb85df2bbd84af8d8610d1272cd9c1943bba63", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "decimal": {:hex, :decimal, "1.4.1", "ad9e501edf7322f122f7fc151cce7c2a0c9ada96f2b0155b8a09a795c2029770", [:mix], [], "hexpm"}, 6 | "earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [], [], "hexpm"}, 7 | "ed25519": {:hex, :ed25519, "1.1.0", "1bb574733b2a2b429bff80ff001002063af3c645f1a54508a53ceeccfd3095c4", [:mix], [], "hexpm"}, 8 | "enacl": {:git, "https://github.com/jlouis/enacl.git", "5a48c66b07192665f81b7baf8513bc7e73fadd45", []}, 9 | "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, 10 | "hackney": {:hex, :hackney, "1.9.0", "51c506afc0a365868469dcfc79a9d0b94d896ec741cfd5bd338f49a5ec515bfe", [], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, 11 | "httpoison": {:hex, :httpoison, "0.13.0", "bfaf44d9f133a6599886720f3937a7699466d23bb0cd7a88b6ba011f53c6f562", [], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "idna": {:hex, :idna, "5.1.0", "d72b4effeb324ad5da3cab1767cb16b17939004e789d8c0ad5b70f3cea20c89a", [], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, 13 | "inch_ex": {:hex, :inch_ex, "0.5.6", "418357418a553baa6d04eccd1b44171936817db61f4c0840112b420b8e378e67", [], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, 14 | "libdecaf": {:git, "https://github.com/potatosalad/erlang-libdecaf.git", "7d5715f7d4067235226612032329d353aebf7dc7", []}, 15 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [], [], "hexpm"}, 16 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [], [], "hexpm"}, 17 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [], [], "hexpm"}, 18 | "porcelain": {:hex, :porcelain, "2.0.3", "2d77b17d1f21fed875b8c5ecba72a01533db2013bd2e5e62c6d286c029150fdc", [], [], "hexpm"}, 19 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [], [], "hexpm"}, 20 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [], [], "hexpm"}} 21 | -------------------------------------------------------------------------------- /lib/rai_ex/rpc.ex: -------------------------------------------------------------------------------- 1 | defmodule RaiEx.RPC do 2 | @moduledoc """ 3 | This module provides macros for generating rpc-invoking functions. 4 | """ 5 | 6 | alias RaiEx.RPC 7 | 8 | @doc false 9 | defmacro __using__(_opts) do 10 | quote do 11 | import RaiEx.RPC 12 | 13 | @behaviour RPC 14 | end 15 | end 16 | 17 | @doc """ 18 | A macro for defining parameters and their types inside an rpc block. 19 | """ 20 | defmacro param(name, type, opts \\ []) do 21 | quote do 22 | Module.put_attribute(__MODULE__, @current_action, 23 | {unquote(name), unquote(type)}) 24 | 25 | Module.put_attribute(__MODULE__, :"#{@current_action}_opts", unquote(opts)) 26 | end 27 | end 28 | 29 | @callback post_json_rpc(map, pos_integer, tuple) :: {:ok, map} | {:error, any} 30 | 31 | @doc """ 32 | A macro for generating rpc calling functions with validations. 33 | 34 | rpc :account_remove do 35 | param "wallet", :string 36 | param "account", :string 37 | end 38 | 39 | Transforms to a single function which takes arguments `wallet` and `account` in the *declared order*. 40 | Additionally this function performs **type checking** on the arguments, e.g. If the first argument 41 | `wallet` does not pass the `:string` type check, an `ArgumentError` will be raised. 42 | 43 | """ 44 | defmacro rpc(action, do: definition) when is_atom(action) do 45 | quote do 46 | Module.put_attribute(__MODULE__, :current_action, unquote(action)) 47 | Module.register_attribute(__MODULE__, unquote(action), accumulate: true) 48 | 49 | unquote(definition) 50 | 51 | param_to_type_keywords = Enum.reverse(Module.get_attribute(__MODULE__, unquote(action))) 52 | 53 | opts = Module.get_attribute(__MODULE__, :"#{@current_action}_opts") || [] 54 | 55 | Module.eval_quoted __ENV__, [ 56 | RPC.__build_map_func__(@current_action, param_to_type_keywords, opts), 57 | RPC.__build_seq_func__(@current_action, param_to_type_keywords, opts) 58 | ] 59 | end 60 | end 61 | 62 | @doc false 63 | def __named_args_from_keyword__(context, keyword_list) do 64 | Enum.map(keyword_list, fn {arg_name, _type} -> 65 | {:"#{arg_name}", Macro.var(:"#{arg_name}", context)} 66 | end) 67 | end 68 | 69 | @doc false 70 | def __seq_args_from_keyword__(context, keyword_list) do 71 | Enum.map(keyword_list, fn {arg_name, _type} -> 72 | Macro.var(:"#{arg_name}", context) 73 | end) 74 | end 75 | 76 | @doc false 77 | def __build_map_func__(action, list, opts) do 78 | map = Macro.var(:map, __MODULE__) 79 | 80 | quote do 81 | def unquote(action) (unquote(map)) when is_map(unquote(map)) do 82 | RaiEx.Tools.Validator.validate_types!(unquote(list), unquote(map)) 83 | 84 | unquote(map) 85 | |> Map.put(:action, unquote(action)) 86 | |> Poison.encode! 87 | |> post_json_rpc(unquote(opts)) 88 | end 89 | end 90 | end 91 | 92 | @doc false 93 | def __build_seq_func__(action, list, opts) do 94 | quote do 95 | def unquote(action) (unquote_splicing(RPC.__seq_args_from_keyword__(__MODULE__, list))) do 96 | RaiEx.Tools.Validator.validate_types!(unquote(list), unquote(RPC.__named_args_from_keyword__(__MODULE__, list))) 97 | 98 | unquote(RPC.__named_args_from_keyword__(__MODULE__, list)) 99 | |> Enum.into(%{}) 100 | |> Map.put(:action, unquote(action)) 101 | |> Poison.encode! 102 | |> post_json_rpc(unquote(opts)) 103 | end 104 | end 105 | end 106 | end -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | # 6 | # These are the files included in the analysis: 7 | files: %{ 8 | included: ["lib/", "src/", "web/", "apps/"], 9 | excluded: [~r"/_build/", ~r"/deps/"] 10 | }, 11 | requires: [], 12 | strict: false, 13 | color: true, 14 | checks: [ 15 | {Credo.Check.Consistency.ExceptionNames}, 16 | {Credo.Check.Consistency.LineEndings}, 17 | {Credo.Check.Consistency.ParameterPatternMatching}, 18 | {Credo.Check.Consistency.SpaceAroundOperators}, 19 | {Credo.Check.Consistency.SpaceInParentheses}, 20 | {Credo.Check.Consistency.TabsOrSpaces}, 21 | 22 | # For some checks, like AliasUsage, you can only customize the priority 23 | # Priority values are: `low, normal, high, higher` 24 | # 25 | {Credo.Check.Design.AliasUsage, priority: :low}, 26 | 27 | # For others you can set parameters 28 | 29 | # If you don't want the `setup` and `test` macro calls in ExUnit tests 30 | # or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just 31 | # set the `excluded_macros` parameter to `[:schema, :setup, :test]`. 32 | # 33 | {Credo.Check.Design.DuplicatedCode, excluded_macros: []}, 34 | 35 | # You can also customize the exit_status of each check. 36 | # If you don't want TODO comments to cause `mix credo` to fail, just 37 | # set this value to 0 (zero). 38 | # 39 | {Credo.Check.Design.TagTODO, exit_status: 2}, 40 | {Credo.Check.Design.TagFIXME}, 41 | 42 | {Credo.Check.Readability.FunctionNames}, 43 | {Credo.Check.Readability.LargeNumbers}, 44 | {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 80}, 45 | {Credo.Check.Readability.ModuleAttributeNames}, 46 | {Credo.Check.Readability.ModuleDoc}, 47 | {Credo.Check.Readability.ModuleNames}, 48 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs}, 49 | {Credo.Check.Readability.ParenthesesInCondition}, 50 | {Credo.Check.Readability.PredicateFunctionNames}, 51 | {Credo.Check.Readability.PreferImplicitTry}, 52 | {Credo.Check.Readability.RedundantBlankLines}, 53 | {Credo.Check.Readability.StringSigils}, 54 | {Credo.Check.Readability.TrailingBlankLine}, 55 | {Credo.Check.Readability.TrailingWhiteSpace}, 56 | {Credo.Check.Readability.VariableNames}, 57 | {Credo.Check.Readability.Semicolons}, 58 | {Credo.Check.Readability.SpaceAfterCommas}, 59 | 60 | {Credo.Check.Refactor.DoubleBooleanNegation}, 61 | {Credo.Check.Refactor.CondStatements}, 62 | {Credo.Check.Refactor.CyclomaticComplexity}, 63 | {Credo.Check.Refactor.FunctionArity}, 64 | {Credo.Check.Refactor.LongQuoteBlocks}, 65 | {Credo.Check.Refactor.MatchInCondition}, 66 | {Credo.Check.Refactor.NegatedConditionsInUnless}, 67 | {Credo.Check.Refactor.NegatedConditionsWithElse}, 68 | {Credo.Check.Refactor.Nesting}, 69 | {Credo.Check.Refactor.PipeChainStart}, 70 | {Credo.Check.Refactor.UnlessWithElse}, 71 | 72 | {Credo.Check.Warning.BoolOperationOnSameValues}, 73 | {Credo.Check.Warning.IExPry}, 74 | {Credo.Check.Warning.IoInspect}, 75 | {Credo.Check.Warning.LazyLogging}, 76 | {Credo.Check.Warning.OperationOnSameValues}, 77 | {Credo.Check.Warning.OperationWithConstantResult}, 78 | {Credo.Check.Warning.UnusedEnumOperation}, 79 | {Credo.Check.Warning.UnusedFileOperation}, 80 | {Credo.Check.Warning.UnusedKeywordOperation}, 81 | {Credo.Check.Warning.UnusedListOperation}, 82 | {Credo.Check.Warning.UnusedPathOperation}, 83 | {Credo.Check.Warning.UnusedRegexOperation}, 84 | {Credo.Check.Warning.UnusedStringOperation}, 85 | {Credo.Check.Warning.UnusedTupleOperation}, 86 | {Credo.Check.Warning.RaiseInsideRescue}, 87 | 88 | # Controversial and experimental checks (opt-in, just remove `, false`) 89 | # 90 | {Credo.Check.Refactor.ABCSize, false}, 91 | {Credo.Check.Refactor.AppendSingleItem, false}, 92 | {Credo.Check.Refactor.VariableRebinding, false}, 93 | {Credo.Check.Warning.MapGetUnsafePass, false}, 94 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 95 | 96 | # Deprecated checks (these will be deleted after a grace period) 97 | # 98 | {Credo.Check.Readability.Specs, false}, 99 | {Credo.Check.Warning.NameRedeclarationByAssignment, false}, 100 | {Credo.Check.Warning.NameRedeclarationByCase, false}, 101 | {Credo.Check.Warning.NameRedeclarationByDef, false}, 102 | {Credo.Check.Warning.NameRedeclarationByFn, false}, 103 | 104 | # Custom checks can be created using `mix credo.gen.check`. 105 | # 106 | ] 107 | } 108 | ] 109 | } 110 | -------------------------------------------------------------------------------- /lib/rai_ex/tools/base_32.ex: -------------------------------------------------------------------------------- 1 | defmodule RaiEx.Tools.Base32 do 2 | @moduledoc """ 3 | This module provides functions for dealing with encoding and decoding, RaiDice base32. 4 | """ 5 | 6 | defp char_to_bin("1"), do: <<0::5>> 7 | defp char_to_bin("3"), do: <<1::5>> 8 | defp char_to_bin("4"), do: <<2::5>> 9 | defp char_to_bin("5"), do: <<3::5>> 10 | defp char_to_bin("6"), do: <<4::5>> 11 | defp char_to_bin("7"), do: <<5::5>> 12 | defp char_to_bin("8"), do: <<6::5>> 13 | defp char_to_bin("9"), do: <<7::5>> 14 | defp char_to_bin("a"), do: <<8::5>> 15 | defp char_to_bin("b"), do: <<9::5>> 16 | defp char_to_bin("c"), do: <<10::5>> 17 | defp char_to_bin("d"), do: <<11::5>> 18 | defp char_to_bin("e"), do: <<12::5>> 19 | defp char_to_bin("f"), do: <<13::5>> 20 | defp char_to_bin("g"), do: <<14::5>> 21 | defp char_to_bin("h"), do: <<15::5>> 22 | defp char_to_bin("i"), do: <<16::5>> 23 | defp char_to_bin("j"), do: <<17::5>> 24 | defp char_to_bin("k"), do: <<18::5>> 25 | defp char_to_bin("m"), do: <<19::5>> 26 | defp char_to_bin("n"), do: <<20::5>> 27 | defp char_to_bin("o"), do: <<21::5>> 28 | defp char_to_bin("p"), do: <<22::5>> 29 | defp char_to_bin("q"), do: <<23::5>> 30 | defp char_to_bin("r"), do: <<24::5>> 31 | defp char_to_bin("s"), do: <<25::5>> 32 | defp char_to_bin("t"), do: <<26::5>> 33 | defp char_to_bin("u"), do: <<27::5>> 34 | defp char_to_bin("w"), do: <<28::5>> 35 | defp char_to_bin("x"), do: <<29::5>> 36 | defp char_to_bin("y"), do: <<30::5>> 37 | defp char_to_bin("z"), do: <<31::5>> 38 | 39 | defp bin_to_char(<<0::5>>), do: "1" 40 | defp bin_to_char(<<1::5>>), do: "3" 41 | defp bin_to_char(<<2::5>>), do: "4" 42 | defp bin_to_char(<<3::5>>), do: "5" 43 | defp bin_to_char(<<4::5>>), do: "6" 44 | defp bin_to_char(<<5::5>>), do: "7" 45 | defp bin_to_char(<<6::5>>), do: "8" 46 | defp bin_to_char(<<7::5>>), do: "9" 47 | defp bin_to_char(<<8::5>>), do: "a" 48 | defp bin_to_char(<<9::5>>), do: "b" 49 | defp bin_to_char(<<10::5>>), do: "c" 50 | defp bin_to_char(<<11::5>>), do: "d" 51 | defp bin_to_char(<<12::5>>), do: "e" 52 | defp bin_to_char(<<13::5>>), do: "f" 53 | defp bin_to_char(<<14::5>>), do: "g" 54 | defp bin_to_char(<<15::5>>), do: "h" 55 | defp bin_to_char(<<16::5>>), do: "i" 56 | defp bin_to_char(<<17::5>>), do: "j" 57 | defp bin_to_char(<<18::5>>), do: "k" 58 | defp bin_to_char(<<19::5>>), do: "m" 59 | defp bin_to_char(<<20::5>>), do: "n" 60 | defp bin_to_char(<<21::5>>), do: "o" 61 | defp bin_to_char(<<22::5>>), do: "p" 62 | defp bin_to_char(<<23::5>>), do: "q" 63 | defp bin_to_char(<<24::5>>), do: "r" 64 | defp bin_to_char(<<25::5>>), do: "s" 65 | defp bin_to_char(<<26::5>>), do: "t" 66 | defp bin_to_char(<<27::5>>), do: "u" 67 | defp bin_to_char(<<28::5>>), do: "w" 68 | defp bin_to_char(<<29::5>>), do: "x" 69 | defp bin_to_char(<<30::5>>), do: "y" 70 | defp bin_to_char(<<31::5>>), do: "z" 71 | 72 | @doc """ 73 | Returns true if the binary can be encoded into base32. 74 | """ 75 | def binary_valid?(binary), do: rem(bit_size(binary), 5) === 0 76 | 77 | @doc """ 78 | Decodes a base32 string into its bitstring form. 79 | 80 | Raises `ArgumentError` if the string is invalid. 81 | 82 | ## Examples 83 | 84 | iex> decode!("34bmipzf") 85 | <<8, 147, 56, 91, 237>> 86 | 87 | iex> decode!("bmg2") 88 | ** (Elixir.ArgumentError) 89 | 90 | """ 91 | def decode!(string) do 92 | string 93 | |> String.split("", trim: true) 94 | |> Enum.reduce(<<>>, &(<<&2::bitstring, char_to_bin(&1)::bitstring>>)) 95 | end 96 | 97 | @doc """ 98 | Same as `decode!`, except returns a results tuple. 99 | 100 | ## Examples 101 | 102 | iex> decode("34bmipzf") 103 | {:ok, <<8, 147, 56, 91, 237>>} 104 | 105 | iex> decode("bmg2") 106 | {:error, :badarg} 107 | 108 | """ 109 | def decode(string) do 110 | try do 111 | {:ok, decode!(string)} 112 | rescue 113 | _e in ArgumentError -> {:error, :badarg} 114 | end 115 | end 116 | 117 | @doc """ 118 | Encodes a bitstring/binary into its base32 form. 119 | 120 | Raises `ArgumentError` if the bitstring/binary is invalid. 121 | 122 | ## Examples 123 | 124 | iex> encode(<<8, 147, 56, 91, 237>>) 125 | "34bmipzf" 126 | 127 | iex> encode(<<8, 5>>) 128 | ** (Elixir.ArgumentError) 129 | 130 | """ 131 | def encode!(bitstring, acc \\ "") 132 | def encode!(<<>>, acc), do: acc 133 | def encode!(invalid, _acc) when bit_size(invalid) < 5 do 134 | raise ArgumentError, message: "bit_size must be a multiple of 5" 135 | end 136 | def encode!(bitstring, acc) do 137 | <> = bitstring 138 | encode!(rest, acc <> bin_to_char(<>)) 139 | end 140 | 141 | @doc """ 142 | Same as `encode!`, except returns a results tuple. 143 | 144 | ## Examples 145 | 146 | iex> encode(<<8, 147, 56, 91, 237>>) 147 | {:ok, "34bmipzf"} 148 | 149 | iex> encode(<<8, 5>>) 150 | {:error, :badarg} 151 | 152 | """ 153 | def encode(bitstring) do 154 | try do 155 | {:ok, encode!(bitstring)} 156 | rescue 157 | _e in ArgumentError -> {:error, :badarg} 158 | end 159 | end 160 | end -------------------------------------------------------------------------------- /test/rai_ex/block_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RaiEx.BlockTest do 2 | use ExUnit.Case 3 | alias RaiEx.{Block, Tools} 4 | 5 | @seed "9F1D53E732E48F25F94711D5B22086778278624F715D9B2BEC8FB81134E7C904" 6 | 7 | @valid_send %{ 8 | "balance" => "00000000000000000000000000000000", 9 | "destination" => "xrb_1aewtdjz8knar65gmu6xo5tmp7ijrur1fgtetua3mxqujh5z9m1r77fsrpqw", 10 | "previous" => "0FE7DF28D6CE577C6B38ACCAE6965B64DB406FF6DB0E3BF642B93E08EFBC8159", 11 | "signature" => "C8271FB970997DA285A746D1F62D7CE475DF7B6B35B1B30917B0844FF0B84B0ACBD9B2389D09827B5A51A8D700309544CDD2832D8CAB83A0590BA562C1197D0B", 12 | "type" => "send", 13 | "work" => "804ec4df247a987d", 14 | # non-standard, just for tests 15 | "hash" => "8F23E5790F995C38D4CC68E85932FA92025813713DEFE8D8E82368272F11C072" 16 | } 17 | 18 | @valid_recv %{ 19 | "previous" => "6483B198E6CEF20727E0601D217E5E12598355F9C194B4CF9F75BE347FFCE4F9", 20 | "signature" => "6F700603E21105949A68E30FC3818B9E03B81D788983526ACFF457F67945C177ACE02B4DECCB1ED73FC1B10E02CACC2FA78A5535EA5232708C3E360C22F17305", 21 | "source" => "193ADF01F896E9955614F275738FCA63E684D3DE5FEB01398C55CD240D9210AB", 22 | "type" => "receive", 23 | "work" => "39bb3a33be963d66", 24 | # non-standard, just for tests 25 | "hash" => "5FA957F257DD62A4582A50E20206214D06F9E26167A11D76E60C392EC5695360" 26 | } 27 | 28 | @valid_open %{ 29 | "account" => "xrb_34bmpi65zr967cdzy4uy4twu7mqs9nrm53r1penffmuex6ruqy8nxp7ms1h1", 30 | "representative" => "xrb_3arg3asgtigae3xckabaaewkx3bzsh7nwz7jkmjos79ihyaxwphhm6qgjps4", 31 | "signature" => "3311DF8325D87D4C528541F6D572CDA1D93605A61FD5D50DA2102EC8B456DCC72D72D3371FE166BD7A3074C284C4AFCA8F0A934B74462126D1BBDB0C5BCF3408", 32 | "source" => "FBC27450F270743B26630E7C8730B301105D4D26997372D94680360E99702825", 33 | "type" => "open", 34 | "work" => "e8907bfce904035a", 35 | # non-standard, just for tests 36 | "hash" => "1653FC490D5AE8786F659D646D49639F0DE13DCC98470D6FA3234D175B85526F" 37 | } 38 | 39 | setup _context do 40 | {priv, pub} = Tools.seed_account!(@seed, 0) 41 | account = Tools.create_account!(pub) 42 | 43 | # Passed to all tests 44 | {:ok, %{account: account, priv: priv, pub: pub}} 45 | end 46 | 47 | describe "Block.sign/3 " do 48 | test "signs a send block", %{priv: priv, pub: pub} do 49 | block = 50 | %Block{ 51 | type: "send", 52 | previous: @valid_send["previous"], 53 | destination: @valid_send["destination"], 54 | balance: 0 55 | } 56 | |> Block.sign(priv, pub) 57 | 58 | assert block.hash === @valid_send["hash"] 59 | assert block.signature === @valid_send["signature"] 60 | end 61 | 62 | test "signs a receive block", %{priv: priv, pub: pub} do 63 | block = 64 | %Block{ 65 | type: "receive", 66 | previous: @valid_recv["previous"], 67 | source: @valid_recv["source"], 68 | } 69 | |> Block.sign(priv, pub) 70 | 71 | assert block.hash === @valid_recv["hash"] 72 | assert block.signature === @valid_recv["signature"] 73 | end 74 | 75 | test "signs an open block", %{priv: priv, pub: pub} do 76 | block = 77 | %Block{ 78 | type: "open", 79 | account: @valid_open["account"], 80 | representative: @valid_open["representative"], 81 | source: @valid_open["source"], 82 | } 83 | |> Block.sign(priv, pub) 84 | 85 | assert block.hash === @valid_open["hash"] 86 | assert block.signature === @valid_open["signature"] 87 | end 88 | end 89 | 90 | describe "Block.process/1 " do 91 | import TestHelper 92 | 93 | skip_offline() 94 | 95 | test "processes a send block and then processes the receive block", %{account: account, priv: priv, pub: pub} do 96 | {:ok, %{"frontier" => frontier}} = RaiEx.account_info(account) 97 | 98 | block = 99 | %Block{ 100 | type: "send", 101 | previous: frontier, 102 | destination: account, 103 | balance: 0 104 | } 105 | |> Block.sign(priv, pub) 106 | |> Block.process() 107 | 108 | assert block.state === :sent 109 | 110 | # Give some time for the node to validate 111 | :timer.sleep(200) 112 | 113 | # Check if the block has been included 114 | {:ok, %{"frontier" => frontier}} = RaiEx.account_info(account) 115 | 116 | assert frontier === block.hash 117 | 118 | block = 119 | %Block{ 120 | type: "receive", 121 | previous: frontier, 122 | source: frontier, 123 | } 124 | |> Block.sign(priv, pub) 125 | |> Block.process() 126 | 127 | assert block.state === :sent 128 | 129 | # Give some time for the node to validate 130 | :timer.sleep(200) 131 | 132 | # Check if the block has been included 133 | {:ok, %{"frontier" => frontier}} = RaiEx.account_info(account) 134 | 135 | assert frontier === block.hash 136 | end 137 | 138 | skip_offline() 139 | 140 | test "processes an open block", %{priv: priv, pub: pub} do 141 | block = 142 | %Block{ 143 | type: "open", 144 | account: @valid_open["account"], 145 | representative: @valid_open["representative"], 146 | source: @valid_open["source"], 147 | } 148 | |> Block.sign(priv, pub) 149 | |> Block.process() 150 | 151 | assert block.state === {:error, "Old block"} 152 | end 153 | end 154 | end -------------------------------------------------------------------------------- /lib/rai_ex/block.ex: -------------------------------------------------------------------------------- 1 | defmodule RaiEx.Block do 2 | @moduledoc """ 3 | The block struct and associated functions. 4 | 5 | ## Fields 6 | 7 | * `type` - the block type, default: "send" 8 | * `previous` - the previous block hash, e.g. 9F1D53E732E48F25F94711D5B22086778278624F715D9B2BEC8FB81134E7C904 9 | * `destination` - the destination address, e.g. xrb_34bmpi65zr967cdzy4uy4twu7mqs9nrm53r1penffmuex6ruqy8nxp7ms1h1 10 | * `balance` - the amount to send, measured in RAW 11 | * `work` - the proof of work, e.g. "266063092558d903" 12 | * `signature` - the signed block digest/hash 13 | * `hash` - the block digest/hash 14 | * `source` - the source hash for a receive block 15 | * `representative` - the representative for an open block 16 | * `account` - the account for an open block 17 | * `state` - the state of the block, can be: `:unsent` or `:sent` 18 | 19 | ## Send a block 20 | 21 | alias RaiEx.{Block, Tools} 22 | 23 | seed = "9F1D53E732E48F25F94711D5B22086778278624F715D9B2BEC8FB81134E7C904" 24 | 25 | # Generate a private and public keypair from a wallet seed 26 | {priv, pub} = Tools.seed_account!(seed, 0) 27 | 28 | # Derives an "xrb_" address 29 | address = Tools.create_account!(pub) 30 | 31 | # Get the previous block hash 32 | {:ok, %{"frontier" => block_hash}} = RaiEx.account_info(address) 33 | 34 | # Somewhat counterintuitively 'balance' refers to the new balance not the 35 | # amount to be sent 36 | block = %Block{ 37 | previous: block_hash, 38 | destination: "xrb_1aewtdjz8knar65gmu6xo5tmp7ijrur1fgtetua3mxqujh5z9m1r77fsrpqw", 39 | balance: 0 40 | } 41 | 42 | # Signs and broadcasts the block to the network 43 | block |> Block.sign(priv, pub) |> Block.send() 44 | 45 | Now *all the funds* from the first account have been transferred to: 46 | 47 | `"xrb_1aewtdjz8knar65gmu6xo5tmp7ijrur1fgtetua3mxqujh5z9m1r77fsrpqw"` 48 | 49 | ## Receive the most recent pending block 50 | 51 | alias RaiEx.{Block, Tools} 52 | 53 | seed = "9F1D53E732E48F25F94711D5B22086778278624F715D9B2BEC8FB81134E7C904" 54 | 55 | # Generate a private and public keypair from a wallet seed 56 | {priv, pub} = Tools.seed_account!(seed, 1) 57 | 58 | # Derives an "xrb_" account 59 | account = Tools.create_account!(pub) 60 | 61 | {:ok, %{"blocks" => [block_hash]}} = RaiEx.pending(account, 1) 62 | {:ok, %{"frontier" => frontier}} = RaiEx.account_info(account) 63 | 64 | block = %Block{ 65 | type: "receive", 66 | previous: frontier, 67 | source: block_hash 68 | } 69 | 70 | block |> Block.sign(priv, pub) |> Block.process() 71 | 72 | ## Open an account 73 | 74 | alias RaiEx.{Block, Tools} 75 | 76 | seed = "9F1D53E732E48F25F94711D5B22086778278624F715D9B2BEC8FB81134E7C904" 77 | representative = "xrb_3arg3asgtigae3xckabaaewkx3bzsh7nwz7jkmjos79ihyaxwphhm6qgjps4" 78 | 79 | # Generate a private and public keypair from a wallet seed 80 | {priv_existing, pub_existing} = Tools.seed_account!(seed, 1) 81 | {priv_new, pub_new} = Tools.seed_account!(seed, 2) 82 | 83 | existing_account = Tools.create_account!(pub_existing) 84 | new_account = Tools.create_account!(pub_new) 85 | 86 | {:ok, %{"frontier" => block_hash, "balance" => balance}} = RaiEx.account_info(existing_account) 87 | 88 | # Convert to number 89 | {balance, ""} = Integer.parse(balance) 90 | 91 | # We need to generate a send block to the new address 92 | block = %Block{ 93 | previous: block_hash, 94 | destination: new_account, 95 | balance: balance 96 | } 97 | 98 | # Signs and broadcasts the block to the network 99 | send_block = block |> Block.sign(priv_existing, pub_existing) |> Block.send() 100 | 101 | # The open block 102 | block = %Block{ 103 | type: "open", 104 | account: new_account, 105 | source: send_block.hash, 106 | representative: representative 107 | } 108 | 109 | # Broadcast to the network 110 | open_block = block |> Block.sign(priv_new, pub_new) |> Block.process() 111 | 112 | """ 113 | 114 | import RaiEx.Helpers 115 | 116 | alias RaiEx.{Block, Tools} 117 | 118 | @derive {Poison.Encoder, except: [:state, :hash]} 119 | defstruct [ 120 | type: "send", 121 | previous: nil, 122 | destination: nil, 123 | balance: nil, 124 | work: nil, 125 | source: nil, 126 | signature: nil, 127 | hash: nil, 128 | representative: nil, 129 | account: nil, 130 | state: :unsent 131 | ] 132 | 133 | defimpl Collectable, for: Block do 134 | def into(original) do 135 | {original, fn 136 | block, {:cont, {k, v}} when is_atom(k) -> %{block | k => v} 137 | block, {:cont, {k, v}} when is_binary(k) -> %{block | String.to_atom(k) => v} 138 | block, :done -> block 139 | _, :halt -> :ok 140 | end} 141 | end 142 | end 143 | 144 | @doc """ 145 | Processes the block. Automatically invokes the correct processing function. 146 | """ 147 | def process(%Block{state: {:error, _reason}} = block), do: block 148 | def process(%Block{type: "send"} = block), do: send(block) 149 | def process(%Block{type: "receive"} = block), do: recv(block) 150 | def process(%Block{type: "open"} = block), do: open(block) 151 | 152 | @doc """ 153 | Signs the block. Automatically invokes the correct signing function. Raises 154 | `ArgumentError` if the type is not recognised. 155 | """ 156 | def sign(block, priv_key, pub_key \\ nil) 157 | 158 | def sign(%Block{type: "send", state: :unsent} = block, priv_key, pub_key) do 159 | sign_send(block, priv_key, pub_key) 160 | end 161 | 162 | def sign(%Block{type: "receive", state: :unsent} = block, priv_key, pub_key) do 163 | sign_recv(block, priv_key, pub_key) 164 | end 165 | 166 | def sign(%Block{type: "open", state: :unsent} = block, priv_key, pub_key) do 167 | sign_open(block, priv_key, pub_key) 168 | end 169 | 170 | def sign(%Block{}, _priv, _pub) do 171 | raise ArgumentError, message: "unrecognised block type" 172 | end 173 | 174 | @doc """ 175 | Signs a send block. 176 | """ 177 | def sign_send(%Block{ 178 | previous: previous, 179 | destination: destination, 180 | balance: balance, 181 | } = block, priv_key, pub_key \\ nil) do 182 | [priv_key, pub_key, previous] = 183 | if_string_hex_to_binary([priv_key, pub_key, previous]) 184 | 185 | hash = Blake2.hash2b( 186 | previous <> 187 | Tools.address_to_public!(destination) <> <>, 32 188 | ) 189 | signature = Ed25519.signature(hash, priv_key, pub_key) 190 | 191 | %{block | 192 | balance: Base.encode16(<>), 193 | hash: Base.encode16(hash), 194 | signature: Base.encode16(signature) 195 | } 196 | end 197 | 198 | @doc """ 199 | Signs a receive block. 200 | """ 201 | def sign_recv(%Block{ 202 | previous: previous, 203 | source: source 204 | } = block, priv_key, pub_key \\ nil) do 205 | [priv_key, pub_key, previous, source] = 206 | if_string_hex_to_binary([priv_key, pub_key, previous, source]) 207 | 208 | hash = Blake2.hash2b(previous <> source, 32) 209 | signature = Ed25519.signature(hash, priv_key, pub_key) 210 | 211 | %{block | hash: Base.encode16(hash), signature: Base.encode16(signature), 212 | source: Base.encode16(source)} 213 | end 214 | 215 | @doc """ 216 | Signs an open block. 217 | """ 218 | def sign_open(%Block{ 219 | source: source, 220 | representative: representative, 221 | account: account 222 | } = block, priv_key, pub_key \\ nil) do 223 | [priv_key, pub_key, source] = 224 | if_string_hex_to_binary([priv_key, pub_key, source]) 225 | 226 | hash = Blake2.hash2b( 227 | source <> 228 | Tools.address_to_public!(representative) <> 229 | Tools.address_to_public!(account), 32 230 | ) 231 | signature = Ed25519.signature(hash, priv_key, pub_key) 232 | 233 | %{block | hash: Base.encode16(hash), signature: Base.encode16(signature)} 234 | end 235 | 236 | @doc """ 237 | Sends a block. 238 | """ 239 | def send(%Block{hash: nil}), do: raise ArgumentError 240 | def send(%Block{signature: nil}), do: raise ArgumentError 241 | def send(%Block{ 242 | previous: previous, 243 | state: :unsent 244 | } = block) do 245 | with {:ok, %{"work" => work}} <- RaiEx.work_generate(previous), 246 | {:ok, %{}} <- RaiEx.process(Poison.encode!(%{block | work: work})) 247 | do 248 | %{block | work: work, state: :sent} 249 | else 250 | {:error, reason} -> 251 | %{block | state: {:error, reason}} 252 | end 253 | end 254 | 255 | @doc """ 256 | Receives a block. 257 | """ 258 | def recv(%Block{previous: previous} = block) do 259 | with {:ok, %{"work" => work}} <- RaiEx.work_generate(previous), 260 | {:ok, %{}} <- RaiEx.process(Poison.encode!(%{block | work: work})) 261 | do 262 | %{block | work: work, state: :sent} 263 | else 264 | {:error, reason} -> 265 | %{block | state: {:error, reason}} 266 | end 267 | end 268 | 269 | @doc """ 270 | Opens a block. 271 | """ 272 | def open(%Block{account: account} = block) do 273 | work_target = account |> Tools.address_to_public!() |> Base.encode16() 274 | 275 | with {:ok, %{"work" => work}} <- RaiEx.work_generate(work_target), 276 | {:ok, %{}} <- RaiEx.process(Poison.encode!(%{block | work: work})) 277 | do 278 | %{block | work: work, state: :sent} 279 | else 280 | {:error, reason} -> 281 | %{block | state: {:error, reason}} 282 | end 283 | end 284 | 285 | @doc """ 286 | Generates a `RaiEx.Block` struct from a map. 287 | """ 288 | def from_map(%{} = map) do 289 | Enum.into(map, %Block{}) 290 | end 291 | end -------------------------------------------------------------------------------- /lib/rai_ex/tools.ex: -------------------------------------------------------------------------------- 1 | defmodule RaiEx.Tools do 2 | @moduledoc """ 3 | This module provides convenience functions for working with a RaiBlocks node. 4 | """ 5 | 6 | import RaiEx.Helpers 7 | 8 | alias RaiEx.Block 9 | alias RaiEx.Tools.Base32 10 | 11 | @delay 200 12 | @zero Decimal.new(0) 13 | 14 | @units [ 15 | GXRB: 1_000_000_000_000_000_000_000_000_000_000_000, 16 | MXRB: 1_000_000_000_000_000_000_000_000_000_000, 17 | kXRB: 1_000_000_000_000_000_000_000_000_000, 18 | XRB: 1_000_000_000_000_000_000_000_000, 19 | mXRB: 1_000_000_000_000_000_000_000, 20 | uXRB: 1_000_000_000_000_000_000 21 | ] 22 | 23 | @doc """ 24 | Generates a wallet seed. 25 | """ 26 | def seed do 27 | :crypto.strong_rand_bytes(32) 28 | end 29 | 30 | @doc """ 31 | Converts RaiBlocks raw amounts to metric prefixed amounts. The second argument 32 | to `raw_to_units/2` can optionally specify the minimum number of integer 33 | digits to occur in the converted amount. Alternatively if the second argument 34 | is one of `:GXRB`, `:MXRB`, `:kXRB`, `:XRB`, `:mXRB` or `:uXRB` then the raw 35 | amount will be converted to the relevant unit. 36 | 37 | ## Examples 38 | 39 | iex> raw_to_units(10000000000000000000) 40 | {#Decimal<10>, :uxrb} 41 | 42 | iex> raw_to_units(Decimal.new(10000000000000000000000)) 43 | {#Decimal<10>, :mxrb} 44 | 45 | iex> raw_to_units(10000000000000000000000, 3) 46 | {#Decimal<10000>, :uxrb} 47 | 48 | iex> raw_to_units(10000000000000000000000, :xrb) 49 | #Decimal<0.01> 50 | 51 | """ 52 | def raw_to_units(raw, min_digits \\ 1) 53 | def raw_to_units(raw, arg) when is_integer(raw) do 54 | raw_to_units(Decimal.new(raw), arg) 55 | end 56 | def raw_to_units(raw, unit) when is_atom(unit) do 57 | {Decimal.div(raw, Decimal.new(@units[unit] || 1)), unit} 58 | end 59 | def raw_to_units(raw, min_digits) do 60 | Enum.each(@units, fn {unit, _} -> 61 | {div, _} = raw_to_units(raw, unit) 62 | 63 | if integer_part_digits(div) >= min_digits do 64 | throw {div, unit} 65 | end 66 | end) 67 | 68 | {raw, :raw} 69 | catch 70 | result -> result 71 | end 72 | 73 | @doc """ 74 | Converts various RaiBlocks units to raw units. 75 | """ 76 | def units_to_raw(amount, unit) when is_integer(amount) do 77 | units_to_raw(Decimal.new(amount), unit) 78 | end 79 | def units_to_raw(amount, unit) do 80 | multiplier = @units[unit] || 1 81 | Decimal.mult(amount, Decimal.new(multiplier)) 82 | end 83 | 84 | # Returns the number of digits in the integer part 85 | def integer_part_digits(@zero), do: 0 86 | def integer_part_digits(%Decimal{} = num) do 87 | rounded = Decimal.round(num, 0, :floor) 88 | 89 | if Decimal.cmp(rounded, @zero) !== :eq do 90 | rounded 91 | |> Decimal.to_string() 92 | |> String.length() 93 | else 94 | 0 95 | end 96 | end 97 | 98 | @doc """ 99 | Sends a certain amount of RAW to `to`. 100 | """ 101 | def send({priv, pub}, to, amount) when is_integer(amount) do 102 | {:ok, %{"frontier" => block_hash, "balance" => balance}} = 103 | pub 104 | |> create_account!() 105 | |> RaiEx.account_info() 106 | 107 | new_balance = String.to_integer(balance) - amount 108 | 109 | %Block{ 110 | previous: block_hash, 111 | destination: to, 112 | balance: new_balance 113 | } 114 | |> Block.sign(priv, pub) 115 | |> Block.send() 116 | end 117 | 118 | def process_all_pending({priv, pub}, amount \\ 1000) do 119 | account = create_account!(pub) 120 | 121 | case RaiEx.pending(account, amount, min_recv()) do 122 | {:ok, %{"blocks" => ""}} -> :ok 123 | {:ok, %{"blocks" => blocks}} -> 124 | frontier = 125 | case RaiEx.account_info(account) do 126 | {:ok, %{"frontier" => frontier}} -> 127 | frontier 128 | {:error, "Account not found"} -> 129 | [{sent_hash, _amount}] = Enum.take(blocks, 1) 130 | open_account({priv, pub}, sent_hash) 131 | end 132 | 133 | # _ is for credo unused values 134 | _ = Enum.reduce(blocks, frontier, fn {receive_hash, _amount}, frontier -> 135 | block = 136 | %Block{ 137 | type: "receive", 138 | previous: frontier, 139 | source: receive_hash 140 | } 141 | |> Block.sign(priv, pub) 142 | |> Block.process() 143 | 144 | block.hash 145 | end) 146 | end 147 | 148 | :ok 149 | end 150 | 151 | def open_account({priv, pub}, sent_hash) do 152 | # The open block 153 | block = 154 | %Block{ 155 | type: "open", 156 | account: create_account!(pub), 157 | source: sent_hash, 158 | representative: Application.get_env(:rai_ex, :representative, 159 | "xrb_3t6k35gi95xu6tergt6p69ck76ogmitsa8mnijtpxm9fkcm736xtoncuohr3") 160 | } 161 | |> Block.sign(priv, pub) 162 | |> Block.process() 163 | 164 | block.hash 165 | end 166 | 167 | @doc """ 168 | Changes the password for the `wallet`. 169 | 170 | ## Examples 171 | 172 | iex> change_password(wallet, current_pwd, new_pwd) 173 | {:ok, wallet} 174 | 175 | iex> change_password(wallet, invalid_pwd, new_pwd) 176 | {:error, reason} 177 | 178 | """ 179 | def change_password(wallet, current_pwd, password) do 180 | with {:ok, %{"valid" => "1"}} <- RaiEx.password_enter(wallet, current_pwd), 181 | {:ok, %{"changed" => "1"}} <- RaiEx.password_change(wallet, password) 182 | do {:ok, wallet} else {_, reason} -> {:error, reason} end 183 | end 184 | 185 | @doc """ 186 | Creates a new encrypted wallet. Locks it with `password`. 187 | """ 188 | def wallet_create_encrypted(password) do 189 | with {:ok, %{"wallet" => wallet}} <- RaiEx.wallet_create, 190 | _ <- :timer.sleep(@delay), 191 | {:ok, ^wallet} <- change_password(wallet, "", password) 192 | do {:ok, wallet} else {_, reason} -> {:error, reason} end 193 | end 194 | 195 | @doc """ 196 | Inserts a new adhoc key into `wallet`. 197 | """ 198 | def wallet_add_adhoc(wallet) do 199 | with {:ok, %{"private" => priv, "public" => pub, "account" => acc}} <- RaiEx.key_create, 200 | {:ok, %{"account" => ^acc}} <- RaiEx.wallet_add(wallet, priv) 201 | do {:ok, %{"private" => priv, "public" => pub, "account" => acc}} else {_, reason} -> {:error, reason} end 202 | end 203 | 204 | @doc """ 205 | Unlocks the given wallet with its `password`. 206 | """ 207 | def unlock_wallet(wallet, password) do 208 | case RaiEx.password_enter(wallet, password) do 209 | {:ok, %{"valid" => "1"}} -> 210 | {:ok, wallet} 211 | {:ok, %{"valid" => "0"}} -> 212 | {:error, :invalid} 213 | {:error, reason} -> 214 | {:error, reason} 215 | end 216 | end 217 | 218 | @doc """ 219 | Locks the given wallet. 220 | """ 221 | def lock_wallet(wallet) do 222 | case RaiEx.password_enter(wallet, "") do 223 | {:ok, _} -> :ok 224 | _ -> :error 225 | end 226 | end 227 | 228 | @doc """ 229 | Calculates and compares the checksum on an address, returns a boolean. 230 | 231 | ## Examples 232 | 233 | iex> address_valid("xrb_34bmpi65zr967cdzy4uy4twu7mqs9nrm53r1penffmuex6ruqy8nxp7ms1h1") 234 | true 235 | 236 | iex> address_valid("clearly not valid") 237 | false 238 | 239 | """ 240 | def account_valid?(address) do 241 | {_pre, checksum} = 242 | address 243 | |> String.trim("xrb_") 244 | |> String.split_at(-8) 245 | 246 | try do 247 | computed_checksum = 248 | address 249 | |> address_to_public!() 250 | |> hash_checksum!() 251 | 252 | attached_checksum = checksum |> Base32.decode!() |> reverse() 253 | 254 | computed_checksum == attached_checksum 255 | rescue 256 | _ -> false 257 | end 258 | end 259 | 260 | @doc """ 261 | Converts a raiblocks address to a public key. 262 | """ 263 | def address_to_public!(address) do 264 | binary = address_to_public_without_trim!(address) 265 | binary_part(binary, 0, byte_size(binary) - 5) 266 | end 267 | 268 | @doc """ 269 | Same as `RaiEx.Tools.address_to_public!` except leaves untrimmied 5 bytes at end of binary. 270 | """ 271 | def address_to_public_without_trim!(address) do 272 | binary = 273 | address 274 | |> String.trim("xrb_") 275 | |> Base32.decode!() 276 | 277 | <<_drop::size(4), pub_key::binary>> = binary 278 | 279 | pub_key 280 | end 281 | 282 | @doc """ 283 | Creates an address from the given *public key*. The address is encoded in 284 | base32 as defined in `RaiEx.Tools.Base32` and appended with a checksum. 285 | 286 | ## Examples 287 | 288 | iex> create_account!(<<125, 169, 163, 231, 136, 75, 168, 59, 83, 105, 128, 71, 82, 149, 53, 87, 90, 35, 149, 51, 106, 243, 76, 13, 250, 28, 59, 128, 5, 181, 81, 116>>) 289 | "xrb_1zfbnhmrikxa9fbpm149cccmcott6gcm8tqmbi8zn93ui14ucndn93mtijeg" 290 | 291 | iex> create_address!("7DA9A3E7884BA83B53698047529535575A2395336AF34C0DFA1C3B8005B55174") 292 | "xrb_1zfbnhmrikxa9fbpm149cccmcott6gcm8tqmbi8zn93ui14ucndn93mtijeg" 293 | 294 | """ 295 | def create_account!(pub_key) do 296 | # This allows both a binary input or hex string 297 | pub_key = 298 | pub_key 299 | |> if_string_hex_to_binary() 300 | |> right_pad_binary(256 - bit_size(pub_key)) 301 | 302 | encoded_check = 303 | pub_key 304 | |> hash_checksum!() 305 | |> reverse() 306 | |> Base32.encode!() 307 | 308 | encoded_address = 309 | pub_key 310 | |> left_pad_binary(4) 311 | |> Base32.encode!() 312 | 313 | "xrb_#{encoded_address <> encoded_check}" 314 | end 315 | 316 | @doc """ 317 | Derives the public key from the private key. 318 | 319 | ## Examples 320 | 321 | iex> derive_public!(<<84, 151, 51, 84, 136, 206, 7, 211, 66, 222, 10, 240, 159, 113, 36, 98, 93, 238, 29, 96, 95, 8, 33, 62, 53, 162, 139, 52, 75, 123, 38, 144>>) 322 | <<125, 169, 163, 231, 136, 75, 168, 59, 83, 105, 128, 71, 82, 149, 53, 87, 90, 35, 149, 51, 106, 243, 76, 13, 250, 28, 59, 128, 5, 181, 81, 116>> 323 | 324 | iex> derive_public!("5497335488CE07D342DE0AF09F7124625DEE1D605F08213E35A28B344B7B2690") 325 | <<125, 169, 163, 231, 136, 75, 168, 59, 83, 105, 128, 71, 82, 149, 53, 87, 90, 35, 149, 51, 106, 243, 76, 13, 250, 28, 59, 128, 5, 181, 81, 116>> 326 | 327 | """ 328 | def derive_public!(priv_key) do 329 | # This allows both a binary input or hex string 330 | priv_key = if_string_hex_to_binary(priv_key) 331 | 332 | Ed25519.derive_public_key(priv_key) 333 | end 334 | 335 | @doc """ 336 | Generates the public and private keys for a given *wallet*. 337 | 338 | ## Examples 339 | 340 | iex> seed_account!("8208BD79655E7141DCFE792084AB6A8FDFFFB56F37CE30ADC4C2CC940E276A8B", 0) 341 | {pub, priv} 342 | 343 | """ 344 | def seed_account!(seed, nonce) do 345 | # This allows both a binary input or hex string 346 | seed = if_string_hex_to_binary(seed) 347 | 348 | priv = Blake2.hash2b(seed <> <>, 32) 349 | pub = derive_public!(priv) 350 | 351 | {priv, pub} 352 | end 353 | 354 | defp hash_checksum!(check) do 355 | Blake2.hash2b(check, 5) 356 | end 357 | end 358 | -------------------------------------------------------------------------------- /lib/rai_ex.ex: -------------------------------------------------------------------------------- 1 | defmodule RaiEx do 2 | @moduledoc """ 3 | This module contains the definitions of all the RaiBlocks node RPC calls. 4 | 5 | ## Usage 6 | 7 | All functions in this module return tuples. The best way to extract the 8 | return values is with pattern matching. Keep in mind that the values are all 9 | *encoded as strings*. If an rpc call *times out*, it will be re-sent after a short 10 | delay. 11 | 12 | ### Examples 13 | 14 | {:ok, %{"wallet" => wallet}} = RaiEx.wallet_create() 15 | 16 | {:ok, %{"frontier" => frontier}} = RaiEx.account_info(account) 17 | 18 | # All functions come with two matching clauses 19 | {:ok, %{"frontiers" => frontiers}} = RaiEx.accounts_frontiers([account], 1) 20 | {:ok, %{"frontiers" => frontiers}} = RaiEx.accounts_frontiers(accounts: [account], count: 1) 21 | 22 | # Node is unreachable 23 | iex> RaiEx.wallet_create() 24 | {:error, :econnrefused} 25 | 26 | # RPC returns an error 27 | iex> RaiEx.wallet_create() 28 | {:error, reason} 29 | 30 | # Connection is blown 31 | iex> RaiEx.wallet_create() 32 | {:error, :blown} 33 | 34 | """ 35 | 36 | import HTTPoison 37 | use RaiEx.RPC 38 | 39 | alias HTTPoison.Response 40 | alias HTTPoison.Error 41 | 42 | @headers [{"Content-Type", "application/json"}] 43 | @options [recv_timeout: 5000, timeout: 10_000, hackney: [pool: :rai_dice]] 44 | @default_url "http://localhost:7076" 45 | 46 | @doc """ 47 | Used to connect to a different endpoint. 48 | """ 49 | def connect(url \\ @default_url, opts \\ []) do 50 | Application.put_env(:rai_ex, :url, url) 51 | Application.put_env(:rai_ex, :opts, opts) 52 | end 53 | 54 | @doc """ 55 | Returns how many RAW is owned and how many have not yet been received by `account`. 56 | """ 57 | rpc :account_balance do 58 | param "account", :address 59 | end 60 | 61 | @doc """ 62 | Gets the number of blocks for a specific `account`. 63 | """ 64 | rpc :account_block_count do 65 | param "account", :address 66 | end 67 | 68 | @doc """ 69 | Returns frontier, open block, change representative block, balance, 70 | last modified timestamp from local database & block count for `account`. 71 | """ 72 | rpc :account_info do 73 | param "account", :address 74 | end 75 | 76 | @doc """ 77 | Creates a new account, insert next deterministic key in `wallet`. 78 | """ 79 | rpc :account_create do 80 | param "wallet", :wallet 81 | end 82 | 83 | @doc """ 84 | Get account number for the `public key`. 85 | """ 86 | rpc :account_get do 87 | param "key", :string 88 | end 89 | 90 | @doc """ 91 | Reports send/receive information for an `account`. 92 | """ 93 | rpc :account_history do 94 | param "account", :address 95 | param "count", :integer 96 | end 97 | 98 | @doc """ 99 | Lists all the accounts inside `wallet`. 100 | """ 101 | rpc :account_list do 102 | param "wallet", :wallet 103 | end 104 | 105 | @doc """ 106 | Moves accounts from `source` to `wallet`. 107 | 108 | Node must have *enable_control* set to 'true' 109 | """ 110 | rpc :account_move do 111 | param "wallet", :wallet 112 | param "source", :address 113 | param "accounts", :address_list 114 | end 115 | 116 | @doc """ 117 | Get the `public key` for `account`. 118 | """ 119 | rpc :account_key do 120 | param "account", :address 121 | end 122 | 123 | @doc """ 124 | Remove `account` from `wallet`. 125 | """ 126 | rpc :account_remove do 127 | param "wallet", :wallet 128 | param "account", :address 129 | end 130 | 131 | @doc """ 132 | Returns the representative for `account`. 133 | """ 134 | rpc :account_representative do 135 | param "account", :address 136 | end 137 | 138 | @doc """ 139 | Sets the representative for `account` in `wallet`. 140 | 141 | Node must have *enable_control* set to 'true' 142 | """ 143 | rpc :account_representative_set do 144 | param "wallet", :wallet 145 | param "account", :address 146 | param "representative", :string 147 | end 148 | 149 | @doc """ 150 | Returns the voting weight for `account`. 151 | """ 152 | rpc :account_weight do 153 | param "account", :address 154 | end 155 | 156 | @doc """ 157 | Returns how many RAW is owned and how many have not yet been received by accounts list. 158 | """ 159 | rpc :accounts_balances do 160 | param "accounts", :address_list 161 | end 162 | 163 | @doc """ 164 | Returns a list of pairs of account and block hash representing the head block for `accounts`. 165 | """ 166 | rpc :accounts_frontiers do 167 | param "accounts", :address_list 168 | end 169 | 170 | @doc """ 171 | Returns a list of block hashes which have not yet been received by these `accounts`. 172 | 173 | Optional `threshold`, only returns hashes with amounts >= threshold. 174 | """ 175 | rpc :accounts_pending do 176 | param "accounts", :address_list 177 | param "count", :integer 178 | end 179 | 180 | rpc :accounts_pending do 181 | param "accounts", :address_list 182 | param "count", :integer 183 | param "threshold", :number 184 | end 185 | 186 | @doc """ 187 | Returns how many rai are in the public supply. 188 | """ 189 | rpc :available_supply do 190 | end 191 | 192 | @doc """ 193 | Retrieves a json representation of `block`. 194 | """ 195 | rpc :block do 196 | param "hash", :hash 197 | end 198 | 199 | @doc """ 200 | Retrieves a json representations of multiple `blocks`. 201 | """ 202 | rpc :blocks do 203 | param "hashes", :hash_list 204 | end 205 | 206 | @doc """ 207 | Retrieves a json representations of `blocks` with transaction `amount` & block `account`. 208 | """ 209 | rpc :blocks_info do 210 | param "hashes", :hash_list 211 | end 212 | 213 | @doc """ 214 | Returns the `account` containing the `block`. 215 | """ 216 | rpc :block_account do 217 | param "hash", :hash 218 | end 219 | 220 | @doc """ 221 | Reports the number of blocks in the ledger and unchecked synchronizing blocks. 222 | """ 223 | rpc :block_count do 224 | end 225 | 226 | @doc """ 227 | Reports the number of blocks in the ledger by type (send, receive, open, change). 228 | """ 229 | rpc :block_count_type do 230 | end 231 | 232 | @doc """ 233 | Initialize bootstrap to specific IP address and `port`. 234 | """ 235 | rpc :bootstrap do 236 | param "address", :string 237 | param "port", :integer 238 | end 239 | 240 | @doc """ 241 | Initialize multi-connection bootstrap to random peers. 242 | """ 243 | rpc :bootstrap_any do 244 | end 245 | 246 | @doc """ 247 | Returns a list of block hashes in the account chain starting at `block` up to `count`. 248 | """ 249 | rpc :chain do 250 | param "block", :block 251 | param "count", :integer 252 | end 253 | 254 | @doc """ 255 | Returns a list of pairs of delegator names given `account` a representative and its balance. 256 | """ 257 | rpc :delegators do 258 | param "account", :address 259 | end 260 | 261 | @doc """ 262 | Get number of delegators for a specific representative `account`. 263 | """ 264 | rpc :delegators_count do 265 | param "account", :address 266 | end 267 | 268 | @doc """ 269 | Derive deterministic keypair from `seed` based on `index`. 270 | """ 271 | rpc :deterministic_key do 272 | param "seed", :string 273 | param "index", :integer 274 | end 275 | 276 | @doc """ 277 | Returns a list of pairs of account and block hash representing the head block starting at account up to count. 278 | """ 279 | rpc :frontiers do 280 | param "account", :address 281 | param "count", :integer 282 | end 283 | 284 | @doc """ 285 | Reports the number of accounts in the ledger. 286 | """ 287 | rpc :frontier_count do 288 | end 289 | 290 | @doc """ 291 | Reports send/receive information for a chain of blocks. 292 | """ 293 | rpc :history do 294 | param "hash", :hash 295 | param "count", :integer 296 | end 297 | 298 | @doc """ 299 | Divide a raw amount down by the Mrai ratio. 300 | """ 301 | rpc :mrai_from_raw do 302 | param "amount", :number 303 | end 304 | 305 | @doc """ 306 | Multiply an Mrai amount by the Mrai ratio. 307 | """ 308 | rpc :mrai_to_raw do 309 | param "amount", :number 310 | end 311 | 312 | @doc """ 313 | Divide a raw amount down by the krai ratio. 314 | """ 315 | rpc :krai_from_raw do 316 | param "amount", :number 317 | end 318 | 319 | @doc """ 320 | Multiply an krai amount by the krai ratio. 321 | """ 322 | rpc :krai_to_raw do 323 | param "amount", :number 324 | end 325 | 326 | @doc """ 327 | Divide a raw amount down by the rai ratio. 328 | """ 329 | rpc :rai_from_raw do 330 | param "amount", :number 331 | end 332 | 333 | @doc """ 334 | Multiply an rai amount by the rai ratio. 335 | """ 336 | rpc :rai_to_raw do 337 | param "amount", :number 338 | end 339 | 340 | @doc """ 341 | Tells the node to send a keepalive packet to address:port. 342 | """ 343 | rpc :keepalive do 344 | param "address", :string 345 | param "port", :integer 346 | end 347 | 348 | @doc """ 349 | Generates an `adhoc random keypair` 350 | """ 351 | rpc :key_create do 352 | end 353 | 354 | @doc """ 355 | Derive public key and account number from `private key`. 356 | """ 357 | rpc :key_expand do 358 | param "key", :string 359 | end 360 | 361 | @doc """ 362 | Begin a new payment session. Searches wallet for an account that's 363 | marked as available and has a 0 balance. If one is found, the account 364 | number is returned and is marked as unavailable. If no account is found, 365 | a new account is created, placed in the wallet, and returned. 366 | """ 367 | rpc :payment_begin do 368 | param "wallet", :wallet 369 | end 370 | 371 | @doc """ 372 | Marks all accounts in wallet as available for being used as a payment session. 373 | """ 374 | rpc :payment_init do 375 | param "wallet", :wallet 376 | end 377 | 378 | @doc """ 379 | End a payment session. Marks the account as available for use in a payment session. 380 | """ 381 | rpc :payment_end do 382 | param "account", :address 383 | param "wallet", :wallet 384 | end 385 | 386 | @doc """ 387 | Wait for payment of 'amount' to arrive in 'account' or until 'timeout' milliseconds have elapsed. 388 | """ 389 | rpc :payment_wait do 390 | param "account", :address 391 | param "amount", :number 392 | param "timeout", :number 393 | end 394 | 395 | @doc """ 396 | Publish `block` to the network. 397 | """ 398 | rpc :process do 399 | param "block", :any 400 | end 401 | 402 | @doc """ 403 | Receive pending block for account in wallet 404 | 405 | *enable_control* must be set to true 406 | """ 407 | rpc :receive do 408 | param "wallet", :wallet 409 | param "account", :address 410 | param "block", :block 411 | end 412 | 413 | @doc """ 414 | Returns receive minimum for node. 415 | 416 | *enable_control* must be set to true 417 | """ 418 | rpc :receive_minimum do 419 | end 420 | 421 | @doc """ 422 | Set `amount` as new receive minimum for node until restart 423 | """ 424 | rpc :receive_minimum_set do 425 | param "amount", :number 426 | end 427 | 428 | @doc """ 429 | Returns a list of pairs of representative and its voting weight. 430 | """ 431 | rpc :representatives do 432 | end 433 | 434 | @doc """ 435 | Returns the default representative for `wallet`. 436 | """ 437 | rpc :wallet_representative do 438 | param "wallet", :wallet 439 | end 440 | 441 | @doc """ 442 | Sets the default representative for wallet. 443 | 444 | *enable_control* must be set to true 445 | """ 446 | rpc :wallet_representative_set do 447 | param "wallet", :wallet 448 | param "representative", :string 449 | end 450 | 451 | @doc """ 452 | Rebroadcast blocks starting at `hash` to the network. 453 | """ 454 | rpc :republish do 455 | param "hash", :hash 456 | end 457 | 458 | @doc """ 459 | Additionally rebroadcast source chain blocks for receive/open up to `sources` depth. 460 | """ 461 | rpc :republish do 462 | param "hash", :hash 463 | param "sources", :integer 464 | end 465 | 466 | @doc """ 467 | Tells the node to look for pending blocks for any account in `wallet`. 468 | """ 469 | rpc :search_pending do 470 | param "wallet", :wallet 471 | end 472 | 473 | @doc """ 474 | Tells the node to look for pending blocks for any account in all available wallets. 475 | """ 476 | rpc :search_pending_all do 477 | end 478 | 479 | @doc """ 480 | Send `amount` from `source` in `wallet` to destination 481 | """ 482 | rpc :send do 483 | param "wallet", :wallet 484 | param "source", :address 485 | param "destination", :string 486 | param "amount", :number 487 | end 488 | 489 | @doc """ 490 | *enable_control* must be set to true 491 | """ 492 | rpc :stop do 493 | end 494 | 495 | @doc """ 496 | Check whether account is a valid account number. 497 | """ 498 | rpc :validate_account_number do 499 | param "account", :address 500 | end 501 | 502 | @doc """ 503 | Returns a list of block hashes in the account chain ending at block up to count. 504 | """ 505 | rpc :successors do 506 | param "block", :block 507 | param "count", :number 508 | end 509 | 510 | @doc """ 511 | Retrieves node versions. 512 | """ 513 | rpc :version do 514 | end 515 | 516 | @doc """ 517 | Returns a list of pairs of peer IPv6:port and its node network version. 518 | """ 519 | rpc :peers do 520 | end 521 | 522 | @doc """ 523 | Returns a list of block hashes which have not yet been received by this account. 524 | """ 525 | rpc :pending do 526 | param "account", :address 527 | param "count", :integer 528 | param "threshold", :integer 529 | end 530 | 531 | @doc """ 532 | Check whether block is pending by hash. 533 | """ 534 | rpc :pending_exists do 535 | param "hash", :hash 536 | end 537 | 538 | @doc """ 539 | Returns a list of pairs of unchecked synchronizing block hash and its json representation up to count. 540 | """ 541 | rpc :unchecked do 542 | param "count", :integer 543 | end 544 | 545 | @doc """ 546 | Clear unchecked synchronizing blocks. 547 | 548 | *enable_control* must be set to true 549 | """ 550 | rpc :unchecked_clear do 551 | end 552 | 553 | @doc """ 554 | Retrieves a json representation of unchecked synchronizing block by hash. 555 | """ 556 | rpc :unchecked_get do 557 | param "hash", :hash 558 | end 559 | 560 | @doc """ 561 | Retrieves unchecked database keys, blocks hashes & a json representations of unchecked pending blocks starting from key up to count. 562 | """ 563 | rpc :unchecked_keys do 564 | param "key", :string 565 | param "count", :integer 566 | end 567 | 568 | @doc """ 569 | Add an adhoc private key key to wallet. 570 | 571 | *enable_control* must be set to true 572 | """ 573 | rpc :wallet_add do 574 | param "wallet", :wallet 575 | param "key", :string 576 | end 577 | 578 | @doc """ 579 | Returns the sum of all accounts balances in wallet. 580 | """ 581 | rpc :wallet_balance_total do 582 | param "wallet", :wallet 583 | end 584 | 585 | @doc """ 586 | Returns how many rai is owned and how many have not yet been received by all accounts in . 587 | """ 588 | rpc :wallet_balances do 589 | param "wallet", :wallet 590 | end 591 | 592 | @doc """ 593 | Changes seed for wallet to seed. 594 | 595 | *enable_control* must be set to true 596 | """ 597 | rpc :wallet_change_seed do 598 | param "wallet", :wallet 599 | param "seed", :string 600 | end 601 | 602 | @doc """ 603 | Check whether wallet contains account. 604 | """ 605 | rpc :wallet_contains do 606 | param "wallet", :wallet 607 | param "account", :address 608 | end 609 | 610 | @doc """ 611 | Creates a new random wallet id. 612 | 613 | *enable_control* must be set to true 614 | """ 615 | rpc :wallet_create do 616 | end 617 | 618 | @doc """ 619 | Destroys wallet and all contained accounts. 620 | 621 | *enable_control* must be set to true 622 | """ 623 | rpc :wallet_destroy do 624 | param "wallet", :wallet 625 | end 626 | 627 | @doc """ 628 | Return a json representation of wallet. 629 | """ 630 | rpc :wallet_export do 631 | param "wallet", :wallet 632 | end 633 | 634 | @doc """ 635 | Returns a list of pairs of account and block hash representing the head block starting 636 | for accounts from wallet. 637 | """ 638 | rpc :wallet_frontiers do 639 | param "wallet", :wallet 640 | end 641 | 642 | @doc """ 643 | Returns a list of block hashes which have not yet been received by accounts in this wallet. 644 | 645 | *enable_control* must be set to true 646 | """ 647 | rpc :wallet_pending do 648 | param "wallet", :wallet 649 | param "count", :integer 650 | end 651 | 652 | @doc """ 653 | Returns a list of pending block hashes with amount more or equal to threshold. 654 | 655 | *enable_control* must be set to true 656 | """ 657 | rpc :wallet_pending do 658 | param "wallet", :wallet 659 | param "count", :integer 660 | param "threshold", :number 661 | end 662 | 663 | @doc """ 664 | Rebroadcast blocks for accounts from wallet starting at frontier down to count to the network. 665 | 666 | *enable_control* must be set to true 667 | """ 668 | rpc :wallet_republish do 669 | param "wallet", :wallet 670 | param "count", :integer 671 | end 672 | 673 | @doc """ 674 | Returns a list of pairs of account and work from wallet. 675 | 676 | *enable_control* must be set to true 677 | """ 678 | rpc :wallet_work_get do 679 | param "wallet", :wallet 680 | end 681 | 682 | @doc """ 683 | Changes the password for wallet to password. 684 | 685 | *enable_control* must be set to true 686 | """ 687 | rpc :password_change do 688 | param "wallet", :wallet 689 | param "password", :string 690 | end 691 | 692 | @doc """ 693 | Enters the password in to wallet. 694 | """ 695 | rpc :password_enter do 696 | param "wallet", :wallet 697 | param "password", :string 698 | end 699 | 700 | @doc """ 701 | Checks whether the password entered for wallet is valid. 702 | """ 703 | rpc :password_valid do 704 | param "wallet", :wallet 705 | end 706 | 707 | @doc """ 708 | Stop generating work for block. 709 | 710 | *enable_control* must be set to true 711 | """ 712 | rpc :work_cancel do 713 | param "hash", :hash 714 | end 715 | 716 | @doc """ 717 | Generates work for block 718 | 719 | *enable_control* must be set to true 720 | """ 721 | rpc :work_generate do 722 | param "hash", :hash, [recv_timeout: 1_200_000, timeout: 1_200_000] 723 | end 724 | 725 | @doc """ 726 | Retrieves work for account in wallet. 727 | 728 | *enable_control* must be set to true 729 | """ 730 | rpc :work_get do 731 | param "wallet", :wallet 732 | param "account", :address 733 | end 734 | 735 | @doc """ 736 | Set work for account in wallet. 737 | 738 | *enable_control* must be set to true 739 | """ 740 | rpc :work_set do 741 | param "wallet", :wallet 742 | param "account", :address 743 | param "work", :string 744 | end 745 | 746 | @doc """ 747 | Add specific IP address and port as work peer for node until restart. 748 | 749 | *enable_control* must be set to true 750 | """ 751 | rpc :work_peer_add do 752 | param "address", :string 753 | param "port", :integer 754 | end 755 | 756 | @doc """ 757 | Retrieves work peers. 758 | 759 | *enable_control* must be set to true 760 | """ 761 | rpc :work_peers do 762 | end 763 | 764 | @doc """ 765 | Clear work peers node list until restart. 766 | 767 | *enable_control* must be set to true 768 | """ 769 | rpc :work_peers_clear do 770 | end 771 | 772 | @doc """ 773 | Check whether work is valid for block. 774 | """ 775 | rpc :work_validate do 776 | param "work", :string 777 | param "hash", :hash 778 | end 779 | 780 | defp get_url() do 781 | url = Application.get_env(:rai_ex, :url, @default_url) 782 | port = Application.get_env(:rai_ex, :port, @default_url) 783 | 784 | "#{url}:#{port}" 785 | end 786 | 787 | @doc """ 788 | Posts some json to the RaiBlocks rpc. Callback implementation for `RaiEx.RPC`. 789 | 790 | ## Examples 791 | 792 | iex> post_json_rpc(%{"action" => "wallet_create"}) 793 | {:ok, %{"wallet" => "0000000000000000"}} 794 | 795 | iex> post_json_rpc(%{"action" => "timeout"}) 796 | {:error, reason} 797 | 798 | """ 799 | def post_json_rpc(json, opts) do 800 | env_opts = Application.get_env(:rai_ex, :opts, opts) 801 | comb_opts = Keyword.merge(Keyword.merge(@options, env_opts), opts) 802 | 803 | with {:ok, body} <- RaiEx.CircuitBreaker.post(get_url(), json, @headers, comb_opts), 804 | {:ok, map} <- Poison.decode(body) 805 | do 806 | case map do 807 | # Returns rpc errors as error tuples 808 | %{"error" => e} -> 809 | {:error, e} 810 | _ -> 811 | {:ok, map} 812 | end 813 | else 814 | {:error, reason} -> 815 | {:error, reason} 816 | :blown -> 817 | {:error, :blown} 818 | end 819 | end 820 | end 821 | --------------------------------------------------------------------------------