lib/transaction.ex

defmodule EthWallet.Transaction do
  @moduledoc """
    Tx about Ethereum.
  """
  alias EthWallet.Utils.{Crypto, TypeTranslator}

  alias EthWallet.Transaction

  defstruct nonce: 0,
            gas_price: 0,
            gas_limit: 0,
            to: <<>>,
            value: 0,
            v: nil,
            r: nil,
            s: nil,
            init: <<>>,
            data: <<>>

  @type t :: %__MODULE__{
          nonce: integer(),
          gas_price: integer(),
          gas_limit: integer(),
          to: <<_::160>> | <<_::0>>,
          value: integer(),
          v: integer(),
          r: integer(),
          s: integer(),
          # contract machine code
          init: binary(),
          data: binary()
        }

  def build_tx(to_str, value, data, nonce, gas_price, gas_limit) do

    to_bin =
      TypeTranslator.addr_to_bin(to_str)

    %Transaction{
      nonce: nonce,
      gas_price: gas_price,
      gas_limit: gas_limit,
      to: to_bin,
      value: value,
      init: <<>>,
      data: data
    }
  end

  @spec signed_tx_to_raw_tx(Transaction.t()) :: String.t()
  def signed_tx_to_raw_tx(signed_tx) do
    raw_tx =
      signed_tx
      |> serialize()
      |> ExRLP.encode(encoding: :hex)

    "0x" <> raw_tx
  end

  @doc """

    > https://b10c.me/blog/006-evolution-of-the-bitcoin-signature-length/

    > https://bitcoin.stackexchange.com/questions/12554/why-the-signature-is-always-65-13232-bytes-long

    Bitcoin:  0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S]

    Attetion: SigHash Flag should be append to the last in finally signature.

    Ethereum: R <> S <> V (V = recid + chainID * 2 + 35)
  """
  @spec sign_tx(Transaction.t(), binary(), integer() | nil) :: Transaction.t()
  def sign_tx(tx, private_key, chain_id \\ nil) do
    %{v: v, r: r, s: s} =
      tx
      |> hash_for_signing(chain_id)
      |> Crypto.sign_compact(private_key, chain_id)

    %{tx | v: v, r: r, s: s}
  end

  @spec hash_for_signing(Transaction.t(), integer() | nil) :: binary()
  def hash_for_signing(tx, chain_id \\ nil) do
    # See EIP-155
    tx
    |> serialize(false)
    |> Kernel.++(if chain_id, do: [:binary.encode_unsigned(chain_id), <<>>, <<>>], else: [])
    |> ExRLP.encode(encoding: :binary)
    |> Crypto.kec()
  end

  @spec serialize(Transaction.t(), boolean()) :: ExRLP.t()
  def serialize(tx, include_vrs \\ true) do
    base = [
      encode_unsigned(tx.nonce),
      encode_unsigned(tx.gas_price),
      encode_unsigned(tx.gas_limit),
      tx.to,
      encode_unsigned(tx.value),
      if(tx.to == <<>>, do: tx.init, else: tx.data)
    ]

    if include_vrs do
      base ++
        [
          encode_unsigned(tx.v),
          encode_unsigned(tx.r),
          encode_unsigned(tx.s)
        ]
    else
      base
    end
  end

  @doc """
    iex(80)> :binary.encode_unsigned(12345)

    "09"

    iex(81)> "09" <> <<0>>

    <<48, 57, 0>>

    iex(83)> 48 * 256 + 57

    12345
  """
  @spec encode_unsigned(number()) :: binary()
  def encode_unsigned(0), do: <<>>
  def encode_unsigned(n), do: :binary.encode_unsigned(n)
end