lib/tx_build/default.ex

defmodule Stellar.TxBuild.Default do
  @moduledoc """
  Default TxBuild implementation.
  """
  alias StellarBase.XDR.{TransactionExt, SorobanTransactionData}
  alias Stellar.{TxBuild, Network}

  alias Stellar.TxBuild.{
    Account,
    Memo,
    BaseFee,
    Operation,
    Operations,
    SequenceNumber,
    Signature,
    Preconditions,
    TimeBounds,
    Transaction,
    TransactionEnvelope,
    TransactionSignature
  }

  @behaviour Stellar.TxBuild.Spec

  @preconditions_keys [
    :time_bounds,
    :ledger_bounds,
    :min_seq_num,
    :min_seq_age,
    :min_seq_ledger_gap,
    :extra_signers
  ]

  @impl true
  def new(%Account{} = source_account, opts) do
    network_passphrase = Keyword.get(opts, :network_passphrase, Network.testnet_passphrase())
    sequence_number = Keyword.get(opts, :sequence_number, SequenceNumber.new())
    base_fee = Keyword.get(opts, :base_fee, BaseFee.new())
    memo = Keyword.get(opts, :memo, Memo.new())
    operations = Keyword.get(opts, :operations, Operations.new())

    preconditions =
      opts
      |> Keyword.take(@preconditions_keys)
      |> Preconditions.new()

    case Transaction.new(
           source_account: source_account,
           sequence_number: sequence_number,
           base_fee: base_fee,
           preconditions: preconditions,
           memo: memo,
           operations: operations
         ) do
      %Transaction{} = transaction ->
        {:ok,
         %TxBuild{
           tx: transaction,
           signatures: [],
           tx_envelope: nil,
           network_passphrase: network_passphrase
         }}

      error ->
        error
    end
  end

  def new(_source_account, _opts), do: {:error, :invalid_source_account}

  @impl true
  def set_network_passphrase({:ok, %TxBuild{} = tx_build}, network_passphrase),
    do: {:ok, %{tx_build | network_passphrase: network_passphrase}}

  @impl true
  def add_memo({:ok, %TxBuild{tx: tx} = tx_build}, %Memo{} = memo) do
    transaction = %{tx | memo: memo}
    {:ok, %{tx_build | tx: transaction}}
  end

  def add_memo({:ok, %TxBuild{}}, _memo), do: {:error, :invalid_memo}
  def add_memo(error, _memo), do: error

  @impl true
  def set_time_bounds(
        {:ok, %TxBuild{tx: %{preconditions: %{type: :none} = preconditions} = tx} = tx_build},
        %TimeBounds{} = time_bounds
      ) do
    preconditions = %{preconditions | type: :precond_time, preconditions: time_bounds}
    transaction = %{tx | preconditions: preconditions}
    {:ok, %{tx_build | tx: transaction}}
  end

  def set_time_bounds(
        {:ok,
         %TxBuild{tx: %{preconditions: %{type: :precond_time} = preconditions} = tx} = tx_build},
        %TimeBounds{} = time_bounds
      ) do
    preconditions = %{preconditions | preconditions: time_bounds}
    transaction = %{tx | preconditions: preconditions}
    {:ok, %{tx_build | tx: transaction}}
  end

  def set_time_bounds(
        {:ok,
         %TxBuild{
           tx:
             %{
               preconditions:
                 %{type: :precond_v2, preconditions: inner_preconditions} = preconditions
             } = tx
         } = tx_build},
        %TimeBounds{} = time_bounds
      ) do
    inner_preconditions = Keyword.put(inner_preconditions, :time_bounds, time_bounds)
    preconditions = %{preconditions | preconditions: inner_preconditions}
    transaction = %{tx | preconditions: preconditions}
    {:ok, %{tx_build | tx: transaction}}
  end

  def set_time_bounds({:ok, %TxBuild{}}, _time_bounds), do: {:error, :invalid_time_bounds}
  def set_time_bounds(error, _time_bounds), do: error

  @impl true
  def set_preconditions({:ok, %TxBuild{tx: tx} = tx_build}, %Preconditions{} = preconditions) do
    transaction = %{tx | preconditions: preconditions}
    {:ok, %{tx_build | tx: transaction}}
  end

  def set_preconditions({:ok, %TxBuild{}}, _preconditions), do: {:error, :invalid_preconditions}
  def set_preconditions(error, _preconditions), do: error

  @impl true
  def set_base_fee({:ok, %TxBuild{tx: tx} = tx_build}, %BaseFee{} = base_fee) do
    %Transaction{operations: %Operations{count: ops_count}} = tx
    transaction = %{tx | base_fee: BaseFee.increment(base_fee, ops_count)}

    {:ok, %{tx_build | tx: transaction}}
  end

  def set_base_fee({:ok, %TxBuild{}}, _base_fee), do: {:error, :invalid_base_fee}
  def set_base_fee(error, _base_fee), do: error

  @impl true
  def set_sequence_number({:ok, %TxBuild{tx: tx} = tx_build}, %SequenceNumber{} = seq_num) do
    transaction = %{tx | sequence_number: seq_num}
    {:ok, %{tx_build | tx: transaction}}
  end

  def set_sequence_number({:ok, %TxBuild{}}, _seq_num), do: {:error, :invalid_sequence_number}
  def set_sequence_number(error, _seq_num), do: error

  @impl true
  def add_operations({:ok, %TxBuild{}} = tx_build, []), do: tx_build

  def add_operations({:ok, %TxBuild{}} = tx_build, [operation | operations]) do
    tx_build
    |> add_operation(operation)
    |> add_operations(operations)
  end

  def add_operations({:ok, %TxBuild{}}, _operations), do: {:error, :invalid_operation}
  def add_operations(error, _operations), do: error

  @impl true
  def add_operation({:ok, %TxBuild{tx: tx} = tx_build}, operation_body) do
    with %Operation{} = operation <- Operation.new(operation_body),
         %Operations{} = operations <- Operations.add(tx.operations, operation) do
      transaction = %{tx | operations: operations, base_fee: BaseFee.increment(tx.base_fee)}
      {:ok, %{tx_build | tx: transaction}}
    end
  end

  def add_operation(error, _operation), do: error

  @impl true
  def set_soroban_data({:ok, %TxBuild{}} = tx_build, soroban_data)
      when is_binary(soroban_data) do
    case check_soroban_data(soroban_data) do
      {:ok, soroban_tx_data} ->
        set_soroban_data(tx_build, soroban_tx_data)

      error ->
        error
    end
  end

  def set_soroban_data(
        {:ok, %TxBuild{tx: tx} = tx_build},
        %SorobanTransactionData{} = soroban_tx_data
      ) do
    ext = TransactionExt.new(soroban_tx_data, 1)
    transaction = %{tx | ext: ext}
    {:ok, %{tx_build | tx: transaction}}
  end

  def set_soroban_data({:ok, %TxBuild{}}, _soroban_tx_data),
    do: {:error, :invalid_soroban_data}

  def set_soroban_data(error, _soroban_data), do: error

  @impl true
  def sign({:ok, %TxBuild{}} = tx_build, []), do: tx_build

  def sign({:ok, %TxBuild{}} = tx_build, [%Signature{} = signature | signatures]) do
    tx_build
    |> sign(signature)
    |> sign(signatures)
  end

  def sign({:ok, %TxBuild{signatures: signatures} = tx_build}, %Signature{} = signature) do
    {:ok, %{tx_build | signatures: signatures ++ [signature]}}
  end

  def sign({:ok, %TxBuild{}}, _signature), do: {:error, :invalid_signature}
  def sign(error, _signature), do: error

  @impl true
  def build(
        {:ok,
         %TxBuild{tx: tx, signatures: signatures, network_passphrase: network_passphrase} =
           tx_build}
      ) do
    tx_envelope =
      TransactionEnvelope.new(
        tx: tx,
        signatures: signatures,
        network_passphrase: network_passphrase
      )

    {:ok, %{tx_build | tx_envelope: tx_envelope}}
  end

  def build(error), do: error

  @impl true
  def envelope(
        {:ok, %TxBuild{tx: tx, signatures: signatures, network_passphrase: network_passphrase}}
      ) do
    [tx: tx, signatures: signatures, network_passphrase: network_passphrase]
    |> TransactionEnvelope.new()
    |> TransactionEnvelope.to_xdr()
    |> TransactionEnvelope.to_base64()
    |> (&{:ok, &1}).()
  end

  def envelope(error), do: error

  @impl true
  def sign_envelope(tx_base64, [], _network_passphrase), do: tx_base64

  def sign_envelope({:ok, tx_base64}, signatures, network_passphrase),
    do: sign_envelope(tx_base64, signatures, network_passphrase)

  def sign_envelope(tx_base64, [%Signature{} = signature | signatures], network_passphrase) do
    tx_base64
    |> sign_envelope(signature, network_passphrase)
    |> sign_envelope(signatures, network_passphrase)
  end

  def sign_envelope(tx_base64, %Signature{} = signature, network_passphrase) do
    tx_base64
    |> TransactionEnvelope.add_signature(signature, network_passphrase)
    |> TransactionEnvelope.to_base64()
    |> (&{:ok, &1}).()
  end

  def sign_envelope(_tx_base64, _signature, _network_passphrase), do: {:error, :invalid_signature}

  @impl true
  def hash({:ok, %TxBuild{tx: tx, network_passphrase: network_passphrase}}) do
    tx
    |> Transaction.to_xdr()
    |> TransactionSignature.base_signature(network_passphrase)
    |> Base.encode16(case: :lower)
    |> (&{:ok, &1}).()
  end

  def hash(error), do: error

  @spec check_soroban_data(soroban_data :: binary()) ::
          {:ok, SorobanTransactionData.t()} | {:error, atom()}
  defp check_soroban_data(soroban_data) do
    with {:ok, raw_soroban_data} <- Base.decode64(soroban_data),
         {:ok, {%SorobanTransactionData{} = soroban_tx_data, ""}} <-
           SorobanTransactionData.decode_xdr(raw_soroban_data) do
      {:ok, soroban_tx_data}
    else
      _ ->
        {:error, :invalid_soroban_data}
    end
  end
end