defmodule Shapeshifter do
@moduledoc """


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