defmodule Shapeshifter do @moduledoc """ ![Shapeshifter lets you quickly and simply switch between Bitcoin transaction formats](https://github.com/libitx/shapeshifter/raw/master/media/poster.png) ![GitHub](https://img.shields.io/github/license/libitx/shapeshifter?color=informational) Shapeshifter is an Elixir library for switching between Bitcoin transaction formats. Quickly and simply shift between raw tx, [`BSV Transaction`](`t:BSV.Tx.t/0`), [`TXO`](`t:txo/0`) and [`BOB`](`t:bob/0`) transaction formats. ## Installation The package can be installed by adding `shapeshifter` to your list of dependencies in `mix.exs`: def deps do [ {:shapeshifter, "~> #{ Mix.Project.config[:version] }"} ] end ## Usage Using Shapeshifter couldn't be simpler. Under the hood pattern matching is used to automatically determine the source format, so all you need to do is pass a transaction object of **any** format to the appropriate function of the format you want to convert to (from: `to_raw/2`, `to_tx/1`, `to_txo/1` or `to_bob/1`). # Convert to raw tx iex> Shapeshifter.to_raw(tx) <<1, 0, 0, 0, ...>> # Convert to raw tx with hex encoding iex> Shapeshifter.to_raw(tx, encoding: :hex) "01000000..." # Convert to BSV.Tx struct iex> Shapeshifter.to_tx(tx) %BSV.Tx{} # Convert to TXO map iex> Shapeshifter.to_txo(tx) %{"in" => [...], "out" => [...], ...} # Convert to BOB map iex> Shapeshifter.to_bob(tx) %{"in" => [...], "out" => [...], ...} For more advanced use, Shapeshifter can also be used to convert individual inputs and outputs between the supported formats. Refer to `Shapeshifter.TXO` and `Shapeshifter.BOB` for more details. """ defstruct [:src, :format] @typedoc "Shapeshifter struct" @type t :: %__MODULE__{ src: BSV.Tx.t | txo | bob, format: :tx | :txo | :bob } @typedoc """ Source transaction Shapeshifter accepts and effortlessly switches between the following transaction formats: * Raw tx binary (with or without hex encoding) * [`BSV Transaction`](`t:BSV.Tx.t/0`) struct * [`TXO`](`t:txo/0`) formatted map * [`BOB`](`t:bob/0`) formatted map """ @type tx :: binary | BSV.Tx.t | txo | bob @typedoc """ Transaction Object format Tranaction objects as given by [Bitbus](https://bitbus.network) or [Bitsocket](https://bitsocket.network) using the [Transaction Object](https://bitquery.planaria.network/#/?id=txo) format. """ @type txo :: %{ required(String.t) => String.t | integer | list } @typedoc """ Bitcoin OP_RETURN Bytecode format Tranaction objects as given by [Bitbus](https://bitbus.network) or [Bitsocket](https://bitsocket.network) using the [Bitcoin OP_RETURN Bytecode](https://bitquery.planaria.network/#/?id=bob) format. """ @type bob :: %{ required(String.t) => String.t | integer | list } @doc """ Creates a new [`Shapeshifter`](`t:t/o`) from the given transaction. Accepts either a raw tx binary (with or without hex encoding), [`BSV Transaction`](`t:BSV.Tx.t/0`) struct, or [`TXO`](`t:txo/0`) or [`BOB`](`t:bob/0`) formatted maps. Returns the [`Shapeshifter`](`t:t/o`) struct in an `:ok` tuple pair, or returns an `:error` tuple pair if the given transaction format is not recognised. """ @spec new(tx) :: {:ok, t} | {:error, Exception.t} def new(tx) when is_binary(tx) do encoding = cond do rem(byte_size(tx), 2) == 0 && String.match?(tx, ~r/^[a-f0-9]+$/i) -> :hex true -> :binary end case BSV.Tx.from_binary(tx, encoding: encoding) do {:ok, tx} -> validate(%__MODULE__{src: tx, format: :tx}) _ -> {:error, %ArgumentError{message: "The source tx is not a valid Bitcoin transaction."}} end end def new(%BSV.Tx{} = tx), do: validate(%__MODULE__{src: tx, format: :tx}) def new(%{"in" => ins, "out" => outs} = tx) when is_list(ins) and is_list(outs) do format = cond do Enum.any?(ins ++ outs, & is_list(&1["tape"])) -> :bob true -> :txo end validate(%__MODULE__{src: tx, format: format}) end def new(src) when is_map(src), do: validate(%__MODULE__{src: src, format: :txo}) @doc """ Converts the given transaction to a raw tx binary, with or without hex encoding. Accepts either a [`BSV Transaction`](`t:BSV.Tx.t/0`) struct, or [`TXO`](`t:txo/0`) or [`BOB`](`t:bob/0`) formatted maps. Returns the result in an `:ok` or `:error` tuple pair. ## Options The accepted options are: * `:encoding` - Set `:hex` for hex encoding """ @spec to_raw(t | tx, keyword) :: {:ok, binary} | {:error, Exception.t} def to_raw(tx, options \\ []) def to_raw(%__MODULE__{format: :tx} = tx, options) do encoding = Keyword.get(options, :encoding) {:ok, BSV.Tx.to_binary(tx.src, encoding: encoding)} end def to_raw(%__MODULE__{} = tx, options) do encoding = Keyword.get(options, :encoding) with {:ok, tx} <- to_tx(tx) do {:ok, BSV.Tx.to_binary(tx, encoding: encoding)} end end def to_raw(tx, options) do with {:ok, tx} <- new(tx), do: to_raw(tx, options) end @doc """ Converts the given transaction to a [`BSV Transaction`](`t:BSV.Tx.t/0`) struct. Accepts either a raw tx binary, or [`TXO`](`t:txo/0`) or [`BOB`](`t:bob/0`) formatted maps. Returns the result in an `:ok` or `:error` tuple pair. """ @spec to_tx(t | tx) :: {:ok, BSV.Tx.t} | {:error, Exception.t} def to_tx(tx) def to_tx(%__MODULE__{format: :tx} = tx), do: {:ok, tx.src} def to_tx(%__MODULE__{format: :txo} = tx), do: {:ok, Shapeshifter.TXO.to_tx(tx)} def to_tx(%__MODULE__{format: :bob} = tx), do: {:ok, Shapeshifter.BOB.to_tx(tx)} def to_tx(tx) do with {:ok, tx} <- new(tx), do: to_tx(tx) end @doc """ Converts the given transaction to the [`TXO`](`t:txo/0`) transaction format. Accepts either a raw tx binary, [`BSV Transaction`](`t:BSV.Tx.t/0`) struct, or [`BOB`](`t:bob/0`) formatted map. Returns the result in an `:ok` or `:error` tuple pair. """ @spec to_txo(t | tx) :: {:ok, txo} | {:error, Exception.t} def to_txo(%__MODULE__{} = tx) do {:ok, Shapeshifter.TXO.new(tx)} end def to_txo(tx) do with {:ok, tx} <- new(tx), do: to_txo(tx) end @doc """ Converts the given transaction to the [`BOB`](`t:bob/0`) transaction format. Accepts either a raw tx binary, [`BSV Transaction`](`t:BSV.Tx.t/0`) struct, or [`TXO`](`t:txo/0`) formatted map. Returns the result in an `:ok` or `:error` tuple pair. """ @spec to_bob(t | tx) :: {:ok, bob} | {:error, Exception.t} def to_bob(%__MODULE__{} = tx) do {:ok, Shapeshifter.BOB.new(tx)} end def to_bob(tx) do with {:ok, tx} <- new(tx), do: to_bob(tx) end # Validates the given `Shapeshifter.t\0` struct. defp validate(%__MODULE__{format: :tx} = shifter) do case shifter.src do %BSV.Tx{} -> {:ok, shifter} _ -> {:error, %ArgumentError{message: "The src tx is not a BSV.Tx type."}} end end defp validate(%__MODULE__{format: fmt} = shifter) when fmt in [:txo, :bob] do case Enum.all?(["tx", "in", "out"], & Map.has_key?(shifter.src, &1)) do true -> {:ok, shifter} false -> {:error, %ArgumentError{message: "The src tx is not a valid TXO or BOB map"}} end end end