defmodule Shapeshifter.BOB do
@moduledoc """
Module for converting to and from [`BOB`](`t:Shapeshifter.bob/0`) structured
maps.
Usually used internally, although can be used directly for specific use cases
such as converting single inputs and outputs to and from [`BOB`](`t:Shapeshifter.bob/0`)
formatted maps.
"""
import Shapeshifter.Shared
@doc """
Creates a new [`BOB`](`t:Shapeshifter.bob/0`) formatted map from the given
[`Shapeshifter`](`t:Shapeshifter.t/0`) struct.
"""
@spec new(Shapeshifter.t) :: map
def new(%Shapeshifter{src: tx, format: :tx}) do
txid = BSV.Tx.get_txid(tx)
ins = tx.inputs
|> Enum.with_index()
|> Enum.map(&cast_input/1)
outs = tx.outputs
|> Enum.with_index()
|> Enum.map(&cast_output/1)
%{
"tx" => %{"h" => txid},
"in" => ins,
"out" => outs,
"lock" => 0
}
end
def new(%Shapeshifter{src: src, format: :txo}) do
ins = Enum.map(src["in"], &cast_input/1)
outs = Enum.map(src["out"], &cast_output/1)
src
|> Map.delete("_id")
|> Map.put("in", ins)
|> Map.put("out", outs)
end
def new(%Shapeshifter{src: src, format: :bob}), do: src
@doc """
Converts the given input parameters to a [`BOB`](`t:Shapeshifter.bob/0`)
formatted input.
Accepts either a [`BSV Input`](`t:BSV.TxIn.t/0`) struct or a
[`TXO`](`t:Shapeshifter.txo/0`) formatted input.
"""
@spec cast_input({BSV.TxIn.t | map, integer}) :: map
def cast_input({%BSV.TxIn{} = src, index}) do
input = %{
"i" => index,
"seq" => src.sequence,
"e" => %{
"h" => BSV.OutPoint.get_txid(src.outpoint),
"i" => src.outpoint.vout,
"a" => script_address(src.script.chunks)
}
}
tape = src.script.chunks
|> Enum.with_index()
|> Enum.reduce({[%{"i" => 0}], 0}, &from_script_chunk/2)
|> elem(0)
|> Enum.reject(& &1 == %{})
|> Enum.map(fn t -> Map.update!(t, "cell", &Enum.reverse/1) end)
|> Enum.reverse()
Map.put(input, "tape", tape)
end
def cast_input(%{"len" => _len} = src),
do: from_txo_object(src)
@doc """
Converts the given output parameters to a [`BOB`](`t:Shapeshifter.bob/0`)
formatted output.
Accepts either a [`BSV Output`](`t:BSV.TxOut.t/0`) struct or a
[`TXO`](`t:Shapeshifter.txo/0`) formatted output.
"""
@spec cast_output({BSV.TxOut.t | map, integer}) :: map
def cast_output({%BSV.TxOut{} = src, index}) do
output = %{
"i" => index,
"e" => %{
"v" => src.satoshis,
"i" => index,
"a" => script_address(src.script.chunks)
}
}
tape = src.script.chunks
|> Enum.with_index()
|> Enum.reduce({[%{"i" => 0}], 0}, &from_script_chunk/2)
|> elem(0)
|> Enum.filter(& Map.has_key?(&1, "cell"))
|> Enum.map(fn t -> Map.update!(t, "cell", &Enum.reverse/1) end)
|> Enum.reverse()
Map.put(output, "tape", tape)
end
def cast_output(%{"len" => _len} = src),
do: from_txo_object(src)
@doc """
Converts the given [`BOB`](`t:Shapeshifter.bob/0`) formatted transaction back
to a [`BSV Transaction`](`t:BSV.Tx.t/0`) struct.
"""
@spec to_tx(%Shapeshifter{
src: map,
format: :bob
}) :: BSV.Tx.t
def to_tx(%Shapeshifter{
src: %{"in" => ins, "out" => outs} = src,
format: :bob
}) do
%BSV.Tx{
inputs: Enum.map(ins, &to_tx_input/1),
outputs: Enum.map(outs, &to_tx_output/1),
lock_time: src["lock"]
}
end
@doc """
Converts the given [`BOB`](`t:Shapeshifter.bob/0`) formatted input back to a
[`BSV Input`](`t:BSV.TxIn.t/0`) struct.
"""
@spec to_tx_input(map) :: BSV.TxIn.t
def to_tx_input(%{} = src) do
%BSV.TxIn{
outpoint: %BSV.OutPoint{
hash: get_in(src, ["e", "h"]) |> BSV.Util.decode!(:hex) |> BSV.Util.reverse_bin(),
vout: get_in(src, ["e", "i"])
},
sequence: src["seq"],
script: to_tx_script(src["tape"])
}
end
@doc """
Converts the given [`BOB`](`t:Shapeshifter.bob/0`) formatted output back to a
[`BSV Output`](`t:BSV.TxOut.t/0`) struct.
"""
@spec to_tx_output(map) :: BSV.TxOut.t
def to_tx_output(%{} = src) do
%BSV.TxOut{
satoshis: get_in(src, ["e", "v"]),
script: to_tx_script(src["tape"])
}
end
# Converts a BSV Script chunk to BOB parameters. The index is given with the
# script chunk.
defp from_script_chunk({opcode, index}, {[%{"i" => i} = head | tape], t})
when is_atom(opcode)
do
head = head
|> Map.put_new("cell", [])
|> Map.update!("cell", fn cells ->
cell = %{
"op" => BSV.OpCode.to_integer(opcode),
"ops" => Atom.to_string(opcode),
"i" => index - t,
"ii" => index
}
[cell | cells]
end)
case opcode do
:OP_RETURN ->
{[%{"i" => i+1} | [head | tape]], index + 1}
_ ->
{[head | tape], t}
end
end
defp from_script_chunk({"|", index}, {[%{"i" => i } = head | tape], _t}) do
{[%{"i" => i+1} | [head | tape]], index + 1}
end
defp from_script_chunk({data, index}, {[head | tape], t})
when is_binary(data)
do
head = head
|> Map.put_new("cell", [])
|> Map.update!("cell", fn cells ->
cell = %{
"s" => data,
"h" => Base.encode16(data, case: :lower),
"b" => Base.encode64(data),
"i" => index - t,
"ii" => index
}
[cell | cells]
end)
{[head | tape], t}
end
# Converts a TXO formatted input/output to a BOB formatted tape.
defp from_txo_object(%{"len" => len} = src) do
target = Map.take(src, ["i", "seq", "e"])
tape = 0..len-1
|> Enum.reduce({[%{"i" => 0}], 0}, fn i, {tape, t} ->
src
|> Map.take(["o#{i}", "s#{i}", "h#{i}", "b#{i}"])
|> Enum.map(fn {k, v} -> {String.replace(k, ~r/\d+$/, ""), v} end)
|> Enum.into(%{"ii" => i})
|> from_txo_attr({tape, t})
end)
|> elem(0)
|> Enum.filter(& Map.has_key?(&1, "cell"))
|> Enum.map(fn t -> Map.update!(t, "cell", &Enum.reverse/1) end)
|> Enum.reverse()
Map.put(target, "tape", tape)
end
# Converts TXO formatted parameters to a BOB formatted cell.
defp from_txo_attr(
%{"o" => opcode, "ii" => index},
{[%{"i" => i} = head | tape], t}
) do
head = head
|> Map.put_new("cell", [])
|> Map.update!("cell", fn cells ->
cell = %{
"op" => BSV.OpCode.to_integer(opcode),
"ops" => opcode,
"i" => index - t,
"ii" => index
}
[cell | cells]
end)
case opcode do
"OP_RETURN" ->
{[%{"i" => i+1} | [head | tape]], index+1}
_ ->
{[head | tape], t}
end
end
defp from_txo_attr(
%{"s" => "|", "ii" => index},
{[%{"i" => i} = head | tape], _t}
) do
{[%{"i" => i+1} | [head | tape]], index+1}
end
defp from_txo_attr(%{"ii" => index} = cell, {[head | tape], t}) do
head = head
|> Map.put_new("cell", [])
|> Map.update!("cell", fn cells ->
cell = Map.put(cell, "i", index - t)
[cell | cells]
end)
{[head | tape], t}
end
# Converts a BOB formatted tape into a BSV Script struct.
defp to_tx_script(tape) when is_list(tape) do
tape
|> Enum.intersperse("|")
|> Enum.reduce(%BSV.Script{}, &to_tx_script/2)
end
defp to_tx_script(%{"cell" => cells}, script) do
Enum.reduce(cells, script, fn cell, script ->
data = cond do
Map.has_key?(cell, "ops") ->
Map.get(cell, "ops") |> String.to_atom
Map.has_key?(cell, "b") ->
Map.get(cell, "b") |> Base.decode64!
Map.has_key?(cell, "h") ->
Map.get(cell, "h") |> Base.decode16!(case: :mixed)
end
BSV.Script.push(script, data)
end)
end
defp to_tx_script("|", script) do
case List.last(script.chunks) do
:OP_RETURN -> script
_ -> BSV.Script.push(script, "|")
end
end
end