Terminus v0.1.1 Terminus.Planaria behaviour View Source

A behaviour module for implementing Planaria-like state machines in Elixir.

A module using Terminus.Planaria is a GenStage consumer process that will automatically mangage its own producer processes to crawl and listen to Bitcoin transaction events. Developers only need to implement callback functions to handle transaction events.

Example

The following code demonstrates how a Twetch scraper can be built in a few lines of code.

defmodule TwetchScraper do
  @query %{
    "find" => %{
      "out.s2": "19HxigV4QyBv3tHpQVcUEQyq1pzZVdoAut",
      "out.s25": "twetch"
    }
  }

  use Terminus.Planaria, token: {:my_app, :planaria_token},
                         from: 600000,
                         query: @query

  def handle_data(:block, txns) do
    # Handle confirmed transactions
  end

  def handle_data(:mempool, txns) do
    # Handle unconfirmed transactions
  end
end

The handle_data/2 callback can be implemented for each tx_event, and is typically be used to persist required data from each transaction. The handle_tape/2 callback can also be implemented for loading and persisting the tape head so a re-crawl isn't necessary if the process is interrupted.

Options

When invoking use Terminus.Planaria, the following config options are accepted:

  • :token - Planaria Token. Required.
  • :host - The Bitbus/Bitsocket endpoint to use. Defaults to :txo.
  • :from - The block height from which to crawl for transactions. Required.
  • :query - Full or shorthand Bitquery map.
  • :poll - Interval (in seconds) to poll Bitbus for new blocks. Defaults to 300 (5 minutes).
  • :recycle - Interval (in seconds) to recycle quiet Bitsocket requests. Defaults to 900 (15 minutes).

Supervision

Each Terminus.Planaria will most commonly be started under your application's supervision tree. When you invoke use Terminus.Planaria, it automatically defines a child_spec/1 function so your Planaria modules can be started directly under a supervisor.

And this is where we can have some fun and take full advantage of Elixir's concurrency model. Why not run many Planarias concurrently in your app?

children = [
  TwetchScraper,
  PreevScraper,
  WeathersvScraper
]

Supervisor.start_link(children, strategy: :one_for_all)

Link to this section Summary

Types

Planaria config.

t()

Planaria state.

Planaria tape.

Planaria tape event.

Planaria transaction event.

Functions

Starts a Terminus.Planaria process without links (outside of a supervision tree).

Starts a Terminus.Planaria process linked to the current process.

Callbacks

Invoked for each new transaction seen by the Planaria.

Invoked when a Planaria starts and also after each crawl of new blocks.

Link to this section Types

Specs

config() :: %{token: String.t(), poll: integer(), from: integer(), query: map()}

Planaria config.

Specs

t() :: %Terminus.Planaria{
  config: config(),
  crawl_sub: {pid(), GenStage.subscription_tag()},
  listen_sub: {pid(), GenStage.subscription_tag()},
  mod: atom(),
  tape: tape()
}

Planaria state.

Specs

tape() :: %{head: integer(), height: integer()}

Planaria tape.

Specs

tape_event() :: :start | :block

Planaria tape event.

Specs

tx_event() :: :block | :mempool

Planaria transaction event.

Link to this section Functions

Link to this function

start(module, config, options)

View Source

Specs

start(atom(), config(), keyword()) :: GenServer.on_start()

Starts a Terminus.Planaria process without links (outside of a supervision tree).

See start_link/3 for more information.

Link to this function

start_link(module, config, options)

View Source

Specs

start_link(atom(), config(), keyword()) :: GenServer.on_start()

Starts a Terminus.Planaria process linked to the current process.

This is often used to start the Planaria as part of a supervision tree.

Link to this section Callbacks

Link to this callback

handle_data(tx_event, list)

View Source

Specs

handle_data(tx_event(), list()) :: any()

Invoked for each new transaction seen by the Planaria.

This is the main callback you will need to implement for your Planaria module. Typically it will be used to pull the necessary data from each transaction event and store it to a local database.

When an unconfirmed transaction is seen the callback is invoked with the tx_event of :mempool. For each confirmed transaction, the callback is invoked with the tx_event of :block. The callback can return any value.

Examples

def handle_data(:block, txns) do
  txns
  |> Enum.map(&MyApp.Transaction.build/1)
  |> Repo.insert(on_conflict: :replace_all, conflict_target: :txid)
end

def handle_data(:mempool, txns) do
  txns
  |> Enum.map(&MyApp.Transaction.build/1)
  |> Repo.insert
end
Link to this callback

handle_tape(tape_event, tape)

View Source

Specs

handle_tape(tape_event(), tape()) :: {:ok, tape()} | any()

Invoked when a Planaria starts and also after each crawl of new blocks.

This callback can be used to load and persist the tape head so a re-crawl isn't necessary if the process is interrupted.

When a Planaria starts the callback is invoked with the tape_event of :start. This provides an oppurtunity to load the current :head of the tape from a database and update the given tape. The callback must return {:ok, tape}.

After each crawl of block data the callback in invoked with the tape_event if :block. This allows us to store the tape :head. In this case any return value is acceptable.

Examples

Load the :head from a database when the Planaria starts.

def handle_tape(:start, tape) do
  tape = case MyApp.Config.get("tape_head") do
    {:ok, head} -> put_in(tape.head, head)
    _ -> tape
  end
  {:ok, tape}
end

Persist the :head after each crawl of new blocks.

def handle_tape(:update, tape) do
  MyApp.Config.put("tape_head", tape.head)
end