defmodule BitcoinLib.Signing.Psbt do
defstruct [:global, :inputs, :outputs]
alias BitcoinLib.Signing.Psbt
alias BitcoinLib.Signing.Psbt.{KeypairList, Global, InputList, OutputList}
@magic 0x70736274
@separator 0xFF
# TODO: document
@spec parse(binary()) :: {:ok, %Psbt{}} | {:error, binary()}
def parse(encoded) do
map =
%{encoded: encoded}
|> base64_decode()
|> extract_data()
|> extract_global()
|> extract_keypair_lists([])
|> dispatch_keypair_lists()
|> extract_inputs()
|> extract_outputs()
case Map.get(map, :error) do
nil ->
{:ok, %Psbt{global: map.global, inputs: map.inputs, outputs: map.outputs}}
message ->
{:error, message}
end
end
defp base64_decode(%{encoded: encoded} = map) do
map
|> Map.put(:decoded, Base.decode64!(encoded))
end
defp extract_data(%{decoded: <<@magic::32, @separator::8, data::binary>>} = map) do
map
|> Map.put(:data, data)
end
defp extract_data(map) do
map
|> Map.put(:error, "magic bytes missing")
end
defp extract_global(%{error: error} = map) when is_binary(error) do
map
end
defp extract_global(map) do
{keypairs, remaining_data} = KeypairList.from_data(map.data)
case Global.from_keypair_list(keypairs) do
{:ok, global} -> %{Map.put(map, :global, global) | data: remaining_data}
{:error, message} -> %{Map.put(map, :error, message) | data: remaining_data}
end
end
defp extract_keypair_lists(%{error: error} = map, _) when is_binary(error) do
map
end
defp extract_keypair_lists(%{data: <<>>} = map, keypair_lists) do
map
|> Map.put(:keypair_lists, Enum.reverse(keypair_lists))
end
defp extract_keypair_lists(%{data: remaining} = map, keypair_lists) do
{keypair_list, remaining} = KeypairList.from_data(remaining)
keypair_lists = [keypair_list | keypair_lists]
extract_keypair_lists(%{map | data: remaining}, keypair_lists)
end
defp dispatch_keypair_lists(%{error: error} = map) when is_binary(error) do
map
end
defp dispatch_keypair_lists(%{global: %Global{unsigned_tx: nil}, keypair_lists: []} = map) do
map
end
defp dispatch_keypair_lists(%{global: %Global{unsigned_tx: nil}, keypair_lists: _} = map) do
map
|> Map.put(
:error,
"inputs and/or outputs in PSBT without an unsigned transaction in the global section"
)
end
defp dispatch_keypair_lists(
%{global: %Global{unsigned_tx: transaction}, keypair_lists: remaining_keypair_lists} =
map
) do
transaction_inputs_count = Enum.count(transaction.inputs)
transaction_output_count = Enum.count(transaction.outputs)
{input_keypair_lists, output_keypair_lists} =
remaining_keypair_lists
|> Enum.split(transaction_inputs_count)
case output_keypair_lists |> Enum.count() < transaction_output_count do
true ->
map
|> Map.put(:error, "some outputs are missing")
false ->
map
|> Map.put(:input_keypair_lists, input_keypair_lists)
|> Map.put(:output_keypair_lists, output_keypair_lists)
end
end
defp extract_inputs(%{error: error} = map) when is_binary(error) do
map
end
defp extract_inputs(%{input_keypair_lists: keypair_lists} = map) do
case InputList.extract(keypair_lists) do
{:ok, inputs} ->
map
|> Map.put(:inputs, inputs)
{:error, message} ->
map
|> Map.put(:error, message)
end
end
defp extract_outputs(%{error: error} = map) when is_binary(error) do
map
end
defp extract_outputs(%{error: error} = map) when is_binary(error) do
map
end
defp extract_outputs(%{output_keypair_lists: keypair_lists} = map) do
case OutputList.extract(keypair_lists) do
{:ok, outputs} ->
map
|> Map.put(:outputs, outputs)
{:error, message} ->
map
|> Map.put(:error, message)
end
end
end