defmodule BitcoinLib.Transaction do @moduledoc """ Based on https://learnmeabitcoin.com/technical/transaction-data#fields """ defstruct [:version, :inputs, :outputs, :locktime] alias BitcoinLib.Key.PrivateKey alias BitcoinLib.Signing.Psbt.CompactInteger alias BitcoinLib.Transaction alias BitcoinLib.Transaction.{Input, Output, Encoder, Signer} @doc """ Converts a bitstring into a %Transaction{} ## Examples iex> <<0x01000000017b1eabe0209b1fe794124575ef807057c77ada2138ae4fa8d6c4de0398a14f3f0000000000ffffffff01f0ca052a010000001976a914cbc20a7664f2f69e5355aa427045bc15e7c6c77288ac00000000::680>> ...> |> BitcoinLib.Transaction.decode() { :ok, %BitcoinLib.Transaction{ version: 1, inputs: [ %BitcoinLib.Transaction.Input{ txid: "3f4fa19803dec4d6a84fae3821da7ac7577080ef75451294e71f9b20e0ab1e7b", vout: 0, script_sig: [], sequence: 4294967295 } ], outputs: [ %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>>} ] } ], locktime: 0 } } """ @spec decode(bitstring()) :: {:ok, %Transaction{}} | {:error, binary()} def decode(encoded_transaction) do result = %{remaining: encoded_transaction} |> extract_version |> extract_input_count |> extract_inputs |> extract_output_count |> extract_outputs |> extract_locktime |> validate_outputs case result do %{error: message} -> {:error, message} %{version: version, inputs: inputs, outputs: outputs, locktime: locktime} -> {:ok, %Transaction{version: version, inputs: inputs, outputs: outputs, locktime: locktime}} end end @doc """ Encodes a transaction into binary format based on https://medium.com/@bitaps.com/exploring-bitcoin-signing-the-p2pkh-input-b8b4d5c4809c#50a6 ## Examples iex> transaction = %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.encode(transaction) <<0x0100000001449d45bbbfe7fc93bbe649bb7b6106b248a15da5dbd6fdc9bdfc7efede83235e0100000000ffffffff014062b007000000001976a914f86f0bc0a2232970ccdf4569815db500f126836188ac00000000::680>> """ @spec encode(%Transaction{}) :: binary() def encode(%Transaction{} = transaction) do transaction |> Encoder.from_struct() end @doc """ Checks if a transaction is NOT signed ## Examples iex> %BitcoinLib.Transaction{ ...> inputs: [ ...> %BitcoinLib.Transaction.Input{ ...> txid: "39bc5c3b33d66ce3d7852a7942331e3ec10f8ba50f225fc41fb5dfa523239a27", ...> vout: 0, ...> script_sig: [], ...> sequence: 4294967295 ...> } ...> ] ...> } ...> |> BitcoinLib.Transaction.check_if_unsigned true """ @spec check_if_unsigned(%Transaction{}) :: boolean() def check_if_unsigned(%Transaction{inputs: inputs}) do inputs |> Enum.reduce(true, fn input, unsigned? -> unsigned? && input.script_sig == [] end) end @doc """ Signs a transaction and transforms it into a binary that can be sent to the network ## Examples iex> private_key = BitcoinLib.Key.PrivateKey.from_mnemonic_phrase( ...> "rally celery split order almost twenty ignore record legend learn chaos decade" ...> ) ...> %BitcoinLib.Transaction{ ...> version: 1, ...> inputs: [ ...> %BitcoinLib.Transaction.Input{ ...> txid: "e4c226432a9319d603b2ed1fa609bffe4cd91f89b3176a9e73b19f7891a92bb6", ...> vout: 0, ...> sequence: 0xFFFFFFFF, ...> script_sig: <<0x76A914AFC3E518577316386188AF748A816CD14CE333F288AC::200>> |> BitcoinLib.Script.parse() ...> } ...> ], ...> outputs: [%BitcoinLib.Transaction.Output{ ...> script_pub_key: <<0x76a9283265393261373463333431393661303236653839653061643561633431386366393430613361663288ac::360>> |> BitcoinLib.Script.parse(), ...> value: 10_000} ...> ], ...> locktime: 0 ...> } ...> |> BitcoinLib.Transaction.sign_and_encode(private_key) "0100000001b62ba991789fb1739e6a17b3891fd94cfebf09a61fedb203d619932a4326c2e4000000006a47304402207d2ff650acf4bd2f413dc04ded50fbbfc315bcb0aa97636b3c4caf55333d1c6a02207590f62363b2263b3d9b65dad3cd56e840e0d61dc0feab8f7e7956831c7e5103012102702ded1cca9816fa1a94787ffc6f3ace62cd3b63164f76d227d0935a33ee48c3ffffffff0110270000000000002d76a9283265393261373463333431393661303236653839653061643561633431386366393430613361663288ac00000000" """ @spec sign_and_encode(%Transaction{}, %PrivateKey{}) :: binary() def sign_and_encode(%Transaction{} = transaction, %PrivateKey{} = private_key) do Signer.sign_and_encode(transaction, private_key) end defp extract_version(%{remaining: <>} = map) do %{map | remaining: remaining} |> Map.put(:version, version) end defp extract_input_count(%{remaining: remaining} = map) do %CompactInteger{value: input_count, remaining: remaining} = CompactInteger.extract_from(remaining, :big_endian) %{map | remaining: remaining} |> Map.put(:input_count, input_count) end defp extract_inputs(%{input_count: input_count, remaining: remaining} = map) do {inputs, remaining} = case input_count do 0 -> {[], remaining} _ -> 1..input_count |> Enum.reduce({[], remaining}, fn _nb, {inputs, remaining} -> {input, remaining} = Input.extract_from(remaining) {[input | inputs], remaining} end) end %{map | remaining: remaining} |> Map.put(:inputs, Enum.reverse(inputs)) end defp extract_output_count(%{remaining: remaining} = map) do %CompactInteger{value: output_count, remaining: remaining} = CompactInteger.extract_from(remaining, :big_endian) %{map | remaining: remaining} |> Map.put(:output_count, output_count) end defp extract_outputs(%{output_count: output_count, remaining: remaining} = map) do {outputs, remaining} = 1..output_count |> Enum.reduce({[], remaining}, fn _nb, {outputs, remaining} -> {output, remaining} = Output.extract_from(remaining) {[output | outputs], remaining} end) %{map | remaining: remaining} |> Map.put(:outputs, Enum.reverse(outputs)) end defp extract_locktime(%{remaining: remaining} = map) do <> = remaining %{map | remaining: remaining} |> Map.put(:locktime, locktime) end defp validate_outputs(%{outputs: outputs} = map) do error = outputs |> Enum.find_value(fn output -> Map.get(output, :error) end) case error do nil -> map message -> map |> Map.put(:error, message) end end end