lib/tx_build/soroban_authorization_entry.ex

defmodule Stellar.TxBuild.SorobanAuthorizationEntry do
  @moduledoc """
  `SorobanAuthorizationEntry` struct definition.
  """

  alias StellarBase.XDR.HashIDPreimage, as: HashIDPreimageXDR

  alias StellarBase.XDR.HashIDPreimageSorobanAuthorization,
    as: HashIDPreimageSorobanAuthorizationXDR

  alias StellarBase.XDR.SorobanAddressCredentials, as: SorobanAddressCredentialsXDR
  alias StellarBase.XDR.SorobanAuthorizedInvocation, as: SorobanAuthorizedInvocationXDR
  alias StellarBase.XDR.{EnvelopeType, Int64, SorobanAuthorizationEntry, UInt32}
  alias Stellar.{KeyPair, Network}

  alias Stellar.TxBuild.{
    HashIDPreimage,
    HashIDPreimageSorobanAuthorization,
    SCMapEntry,
    SCVal,
    SorobanAddressCredentials,
    SorobanCredentials,
    SorobanAuthorizedInvocation
  }

  @behaviour Stellar.TxBuild.XDR

  @type error :: {:error, atom()}
  @type validation :: {:ok, any()} | error()
  @type base_64 :: String.t()
  @type secret_key :: String.t()
  @type sign_authorization :: String.t()
  @type latest_ledger :: non_neg_integer()
  @type network_passphrase :: String.t()
  @type credentials :: SorobanCredentials.t()
  @type root_invocation :: SorobanAuthorizedInvocation.t()

  @type t :: %__MODULE__{
          credentials: credentials(),
          root_invocation: root_invocation()
        }

  defstruct [:credentials, :root_invocation]

  @impl true
  def new(value, opts \\ [])

  def new(args, _opts) when is_list(args) do
    credentials = Keyword.get(args, :credentials)
    root_invocation = Keyword.get(args, :root_invocation)

    with {:ok, credentials} <- validate_credentials(credentials),
         {:ok, root_invocation} <- validate_root_invocation(root_invocation) do
      %__MODULE__{
        credentials: credentials,
        root_invocation: root_invocation
      }
    end
  end

  def new(_value, _opts), do: {:error, :invalid_auth_entry_args}

  @impl true
  def to_xdr(%__MODULE__{
        credentials: credentials,
        root_invocation: root_invocation
      }) do
    root_invocation = SorobanAuthorizedInvocation.to_xdr(root_invocation)

    credentials
    |> SorobanCredentials.to_xdr()
    |> SorobanAuthorizationEntry.new(root_invocation)
  end

  def to_xdr(_struct), do: {:error, :invalid_struct}

  @spec sign(
          credentials :: t(),
          secret_key :: secret_key(),
          network_passphrase :: network_passphrase()
        ) :: t() | error()
  def sign(
        %__MODULE__{
          credentials: %SorobanCredentials{
            value:
              %SorobanAddressCredentials{
                nonce: nonce,
                signature_expiration_ledger: signature_expiration_ledger,
                signature: %SCVal{value: signature_args}
              } = soroban_address_credentials
          },
          root_invocation: root_invocation
        } = credentials,
        secret_key,
        network_passphrase
      )
      when is_binary(secret_key) do
    {public_key, _secret_key} = KeyPair.from_secret_seed(secret_key)
    raw_public_key = KeyPair.raw_public_key(public_key)
    network_id = Network.network_id(network_passphrase)

    signature =
      [
        network_id: network_id,
        nonce: nonce,
        signature_expiration_ledger: signature_expiration_ledger + 3,
        invocation: root_invocation
      ]
      |> HashIDPreimageSorobanAuthorization.new()
      |> (&HashIDPreimage.new(soroban_auth: &1)).()
      |> HashIDPreimage.to_xdr()
      |> HashIDPreimageXDR.encode_xdr!()
      |> hash()
      |> KeyPair.sign(secret_key)

    public_key_map_entry =
      SCMapEntry.new(
        SCVal.new(symbol: "public_key"),
        SCVal.new(bytes: raw_public_key)
      )

    signature_map_entry =
      SCMapEntry.new(
        SCVal.new(symbol: "signature"),
        SCVal.new(bytes: signature)
      )

    signature_sc_val = SCVal.new(map: [public_key_map_entry, signature_map_entry])

    soroban_address_credentials = %{
      soroban_address_credentials
      | signature: SCVal.new(vec: signature_args ++ [signature_sc_val])
    }

    %{credentials | credentials: soroban_address_credentials}
  end

  def sign(_args, _secret_key, _network_passphrase), do: {:error, :invalid_sign_args}

  @spec sign_xdr(
          base_64 :: base_64(),
          secret_key :: secret_key(),
          latest_ledger :: latest_ledger(),
          network_passphrase :: network_passphrase()
        ) :: sign_authorization() | error()
  def sign_xdr(base_64, secret_key, latest_ledger, network_passphrase)
      when is_binary(base_64) and is_binary(secret_key) and is_integer(latest_ledger) and
             is_binary(network_passphrase) do
    {%SorobanAuthorizationEntry{
       credentials:
         %{
           value:
             %SorobanAddressCredentialsXDR{
               nonce: nonce
             } = soroban_address_credentials
         } = credentials,
       root_invocation: root_invocation
     } = soroban_auth,
     ""} =
      base_64
      |> Base.decode64!()
      |> SorobanAuthorizationEntry.decode_xdr!()

    signature_expiration_ledger = UInt32.new(latest_ledger + 3)

    signature =
      nonce
      |> build_signature_from_xdr(
        signature_expiration_ledger,
        root_invocation,
        secret_key,
        network_passphrase
      )
      |> (&SCVal.new(vec: [&1])).()
      |> SCVal.to_xdr()

    soroban_address_credentials = %{
      soroban_address_credentials
      | signature: signature,
        signature_expiration_ledger: signature_expiration_ledger
    }

    credentials = %{credentials | value: soroban_address_credentials}

    %{soroban_auth | credentials: credentials}
    |> SorobanAuthorizationEntry.encode_xdr!()
    |> Base.encode64()
  end

  def sign_xdr(_base_64, _secret_key, _latest_ledger, _network_passphrase),
    do: {:error, :invalid_sign_args}

  @spec hash(data :: binary()) :: binary()
  defp hash(data), do: :crypto.hash(:sha256, data)

  @spec validate_credentials(credentials :: credentials()) :: validation()
  defp validate_credentials(%SorobanCredentials{} = credentials), do: {:ok, credentials}
  defp validate_credentials(_credentials), do: {:error, :invalid_credentials}

  @spec validate_root_invocation(root_invocation :: root_invocation()) :: validation()
  defp validate_root_invocation(%SorobanAuthorizedInvocation{} = root_invocation),
    do: {:ok, root_invocation}

  defp validate_root_invocation(_root_invocation), do: {:error, :invalid_root_invocation}

  @spec build_signature_from_xdr(
          nonce :: Int64.t(),
          signature_expiration_ledger :: UInt32.t(),
          root_invocation :: SorobanAuthorizedInvocationXDR.t(),
          secret_key :: secret_key(),
          network_passphrase :: network_passphrase()
        ) :: SCVal.t() | error()
  defp build_signature_from_xdr(
         nonce,
         signature_expiration_ledger,
         root_invocation,
         secret_key,
         network_passphrase
       ) do
    {public_key, _secret_key} = KeyPair.from_secret_seed(secret_key)
    raw_public_key = KeyPair.raw_public_key(public_key)
    envelope_type = EnvelopeType.new(:ENVELOPE_TYPE_SOROBAN_AUTHORIZATION)

    signature =
      network_passphrase
      |> Network.network_id_xdr()
      |> HashIDPreimageSorobanAuthorizationXDR.new(
        nonce,
        signature_expiration_ledger,
        root_invocation
      )
      |> HashIDPreimageXDR.new(envelope_type)
      |> HashIDPreimageXDR.encode_xdr!()
      |> hash()
      |> KeyPair.sign(secret_key)

    public_key_map_entry =
      SCMapEntry.new(
        SCVal.new(symbol: "public_key"),
        SCVal.new(bytes: raw_public_key)
      )

    signature_map_entry =
      SCMapEntry.new(
        SCVal.new(symbol: "signature"),
        SCVal.new(bytes: signature)
      )

    SCVal.new(map: [public_key_map_entry, signature_map_entry])
  end
end