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: <>} 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: <>} end defp append_input_count(%{transaction: %Transaction{inputs: inputs}, encoded: encoded} = map) do encoded_input_count = inputs |> Enum.count() |> CompactInteger.encode() %{map | encoded: <>} 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 -> <> 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: <>} 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 -> <> end) %{map | encoded: encoded_with_outputs} end defp maybe_append_witnesses(%{transaction: %Transaction{witness: []}} = map), do: map defp maybe_append_witnesses( %{transaction: %Transaction{witness: [public_key, signature]}, encoded: encoded} = map ) do encoded_witness = P2wpkhWitness.encode(signature, public_key) %{map | encoded: <>} end defp append_locktime(%{transaction: %Transaction{locktime: locktime}, encoded: encoded} = map) do %{map | encoded: <>} end end