lib/transaction/encoder.ex

defmodule BitcoinLib.Transaction.Encoder do
  @moduledoc """
  Can convert a Transaction into binary formats
  """

  alias BitcoinLib.Transaction
  alias BitcoinLib.Transaction.{Input, Output}
  alias BitcoinLib.Transaction.Witnesses.{P2wpkhWitness}
  alias BitcoinLib.Signing.Psbt.CompactInteger

  @flag 1
  @marker 0
  @byte 8

  @doc """
  Encodes a transaction in binary format from a structure

  ## Examples
      iex> %BitcoinLib.Transaction{
      ...>   inputs: [
      ...>     %BitcoinLib.Transaction.Input{
      ...>       script_sig: nil,
      ...>       sequence: 0xFFFFFFFF,
      ...>       txid: "5e2383defe7efcbdc9fdd6dba55da148b206617bbb49e6bb93fce7bfbb459d44",
      ...>       vout: 1
      ...>     }
      ...>   ],
      ...>   outputs: [
      ...>     %BitcoinLib.Transaction.Output{
      ...>       script_pub_key: [
      ...>         %BitcoinLib.Script.Opcodes.Stack.Dup{},
      ...>         %BitcoinLib.Script.Opcodes.Crypto.Hash160{},
      ...>         %BitcoinLib.Script.Opcodes.Data{value: <<0xf86f0bc0a2232970ccdf4569815db500f1268361::160>>},
      ...>         %BitcoinLib.Script.Opcodes.BitwiseLogic.EqualVerify{},
      ...>         %BitcoinLib.Script.Opcodes.Crypto.CheckSig{}
      ...>       ],
      ...>       value: 129000000
      ...>     }
      ...>   ],
      ...>   locktime: 0,
      ...>   version: 1
      ...> }
      ...> |> BitcoinLib.Transaction.Encoder.from_struct()
      <<1, 0, 0, 0>> <> # version
      <<1>> <>          # input_count
      <<0x449d45bbbfe7fc93bbe649bb7b6106b248a15da5dbd6fdc9bdfc7efede83235e0100000000ffffffff::328>> <> # inputs
      <<1>> <>          # output_count
      <<0x4062b007000000001976a914f86f0bc0a2232970ccdf4569815db500f126836188ac::272>> <> # outputs
      <<0, 0, 0, 0>>    # locktime
  """
  @spec from_struct(%Transaction{}) :: bitstring()
  def from_struct(%Transaction{} = transaction) do
    %{transaction: transaction, encoded: <<>>}
    |> append_version
    |> maybe_append_marker
    |> append_input_count
    |> append_inputs
    |> append_output_count
    |> append_outputs
    |> maybe_append_witnesses
    |> append_locktime
    |> Map.get(:encoded)
  end

  @doc """
  Specific encoding meant to generate Transaction ID's

  ## Examples

      iex> %BitcoinLib.Transaction{
      ...>   inputs: [
      ...>     %BitcoinLib.Transaction.Input{
      ...>       script_sig: nil,
      ...>       sequence: 0xFFFFFFFF,
      ...>       txid: "5e2383defe7efcbdc9fdd6dba55da148b206617bbb49e6bb93fce7bfbb459d44",
      ...>       vout: 1
      ...>     }
      ...>   ],
      ...>   outputs: [
      ...>     %BitcoinLib.Transaction.Output{
      ...>       script_pub_key: [
      ...>         %BitcoinLib.Script.Opcodes.Stack.Dup{},
      ...>         %BitcoinLib.Script.Opcodes.Crypto.Hash160{},
      ...>         %BitcoinLib.Script.Opcodes.Data{value: <<0xf86f0bc0a2232970ccdf4569815db500f1268361::160>>},
      ...>         %BitcoinLib.Script.Opcodes.BitwiseLogic.EqualVerify{},
      ...>         %BitcoinLib.Script.Opcodes.Crypto.CheckSig{}
      ...>       ],
      ...>       value: 129000000
      ...>     }
      ...>   ],
      ...>   locktime: 0,
      ...>   version: 1
      ...> }
      ...> |> BitcoinLib.Transaction.Encoder.for_txid()
      <<0x0100000001449d45bbbfe7fc93bbe649bb7b6106b248a15da5dbd6fdc9bdfc7efede83235e0100000000ffffffff014062b007000000001976a914f86f0bc0a2232970ccdf4569815db500f126836188ac00000000::680>>
  """
  def for_txid(%Transaction{} = transaction) do
    %{transaction: transaction, encoded: <<>>}
    |> append_version
    |> append_input_count
    |> append_inputs
    |> append_output_count
    |> append_outputs
    |> append_locktime
    |> Map.get(:encoded)
  end

  defp append_version(%{transaction: %Transaction{version: version}, encoded: encoded} = map) do
    %{map | encoded: <<encoded::binary, version::little-32>>}
  end

  # No witness, and thus the marker shouldn't be added
  defp maybe_append_marker(%{transaction: %Transaction{witness: []}} = map), do: map

  # Add a marker since witnesses are present
  defp maybe_append_marker(
         %{transaction: %Transaction{witness: _witness}, encoded: encoded} = map
       ) do
    %{map | encoded: <<encoded::binary, @marker::@byte, @flag::@byte>>}
  end

  defp append_input_count(%{transaction: %Transaction{inputs: inputs}, encoded: encoded} = map) do
    encoded_input_count =
      inputs
      |> Enum.count()
      |> CompactInteger.encode()

    %{map | encoded: <<encoded::bitstring, encoded_input_count::bitstring>>}
  end

  defp append_inputs(%{transaction: %Transaction{inputs: inputs}, encoded: encoded} = map) do
    encoded_with_inputs =
      inputs
      |> Enum.map(&Input.encode(&1))
      |> Enum.reduce(encoded, fn encoded_input, acc ->
        <<acc::bitstring, encoded_input::bitstring>>
      end)

    %{map | encoded: encoded_with_inputs}
  end

  defp append_output_count(%{transaction: %Transaction{outputs: outputs}, encoded: encoded} = map) do
    encoded_output_count =
      outputs
      |> Enum.count()
      |> CompactInteger.encode()

    %{map | encoded: <<encoded::bitstring, encoded_output_count::bitstring>>}
  end

  defp append_outputs(%{transaction: %Transaction{outputs: outputs}, encoded: encoded} = map) do
    encoded_with_outputs =
      outputs
      |> Enum.map(&Output.encode(&1))
      |> Enum.reduce(encoded, fn encoded_output, acc ->
        <<acc::bitstring, encoded_output::bitstring>>
      end)

    %{map | encoded: encoded_with_outputs}
  end

  defp maybe_append_witnesses(%{transaction: %Transaction{witness: []}} = map), do: map

  defp maybe_append_witnesses(
         %{transaction: %Transaction{witness: [[signature, public_key]]}, encoded: encoded} = map
       ) do
    encoded_witness = P2wpkhWitness.encode(signature, public_key)

    %{map | encoded: <<encoded::bitstring, encoded_witness::bitstring>>}
  end

  defp append_locktime(%{transaction: %Transaction{locktime: locktime}, encoded: encoded} = map) do
    %{map | encoded: <<encoded::bitstring, locktime::little-32>>}
  end
end