defmodule BitcoinLib.Transaction.Output do
@moduledoc """
Based on https://learnmeabitcoin.com/technical/output
"""
defstruct [:value, :script_pub_key, :invalid_script, :error_message]
alias BitcoinLib.Signing.Psbt.CompactInteger
alias BitcoinLib.Transaction.Output
alias BitcoinLib.Script
@type t :: Output
@byte 8
@doc """
Extracts a transaction output from a bitstring
## Examples
iex> <<0xf0ca052a010000001976a914cbc20a7664f2f69e5355aa427045bc15e7c6c77288ac00000000::304>>
...> |> BitcoinLib.Transaction.Output.extract_from
{
:ok,
%BitcoinLib.Transaction.Output{
value: 4999990000,
script_pub_key: [
%BitcoinLib.Script.Opcodes.Stack.Dup{},
%BitcoinLib.Script.Opcodes.Crypto.Hash160{},
%BitcoinLib.Script.Opcodes.Data{value: <<0xcbc20a7664f2f69e5355aa427045bc15e7c6c772::160>>},
%BitcoinLib.Script.Opcodes.BitwiseLogic.EqualVerify{},
%BitcoinLib.Script.Opcodes.Crypto.CheckSig{script: <<0x76a914cbc20a7664f2f69e5355aa427045bc15e7c6c77288ac::200>>}
]
},
<<0, 0, 0, 0>>
}
"""
@spec extract_from(binary()) ::
{:ok, Output.t(), bitstring()} | {:error, binary(), bitstring(), bitstring()}
def extract_from(<<value::little-64, remaining::bitstring>>) do
case extract_script_pub_key(remaining) do
{:ok, script_pub_key, remaining} ->
output = %Output{value: value, script_pub_key: script_pub_key}
{:ok, output, remaining}
{:error, message, remaining_from_script, remaining} ->
output = %Output{
value: value,
script_pub_key: [],
invalid_script: remaining_from_script,
error_message: message
}
{:ok, output, remaining}
end
end
@doc """
Encodes an output into a bitstring
## Examples
iex> %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
...> } |> BitcoinLib.Transaction.Output.encode
<<0x4062b00700000000::64>> <> # value
<<0x19::8>> <> # script_pub_key size
<<0x76a914f86f0bc0a2232970ccdf4569815db500f126836188ac::200>> # script_pub_key
"""
@spec encode(Output.t()) :: bitstring()
def encode(%Output{} = output) do
{script_pub_key_size, script_pub_key} =
output.script_pub_key
|> Script.encode()
<<output.value::little-64, script_pub_key_size::bitstring, script_pub_key::bitstring>>
end
defp extract_script_pub_key(remaining) do
%CompactInteger{value: script_pub_key_size, remaining: remaining} =
CompactInteger.extract_from(remaining)
case script_pub_key_size <= byte_size(remaining) do
true ->
script_pub_key_bit_size = script_pub_key_size * @byte
<<script_pub_key::bitstring-size(script_pub_key_bit_size), remaining::bitstring>> =
remaining
case Script.parse(script_pub_key) do
{:ok, script_pub_key} ->
{:ok, script_pub_key, remaining}
{:error, message} ->
{:error, message, script_pub_key, remaining}
end
false ->
{:error, "badly formatted script pub key", remaining}
end
end
end