defmodule Ethers.Transaction.Eip1559 do
  @moduledoc """
  Transaction struct and protocol implementation for Ethereum Improvement Proposal (EIP) 1559
  transactions. EIP-1559 introduced a new fee market mechanism with base fee and priority fee.

  See: https://eips.ethereum.org/EIPS/eip-1559
  """

  import Ethers.Transaction.Helpers

  alias Ethers.Types
  alias Ethers.Utils

  @behaviour Ethers.Transaction

  @type_id 2

  @enforce_keys [:chain_id, :nonce, :max_priority_fee_per_gas, :max_fee_per_gas, :gas]
  defstruct [
    :chain_id,
    :nonce,
    :max_priority_fee_per_gas,
    :max_fee_per_gas,
    :gas,
    :to,
    :value,
    :input,
    access_list: []
  ]

  @typedoc """
  A transaction type following EIP-1559 (Type-2) and incorporating the following fields:
  - `chain_id` - chain ID of network where the transaction is to be executed
  - `nonce` - sequence number for the transaction from this sender
  - `max_priority_fee_per_gas` - maximum fee per gas (in wei) to give to validators as priority fee (introduced in EIP-1559)
  - `max_fee_per_gas` - maximum total fee per gas (in wei) willing to pay (introduced in EIP-1559)
  - `gas` - maximum amount of gas allowed for transaction execution
  - `to` - destination address for transaction, nil for contract creation
  - `value` - amount of ether (in wei) to transfer
  - `input` - data payload of the transaction
  - `access_list` - list of addresses and storage keys to warm up (introduced in EIP-2930)
  """
  @type t :: %__MODULE__{
          chain_id: non_neg_integer(),
          nonce: non_neg_integer(),
          max_priority_fee_per_gas: non_neg_integer(),
          max_fee_per_gas: non_neg_integer(),
          gas: non_neg_integer(),
          to: Types.t_address() | nil,
          value: non_neg_integer(),
          input: binary(),
          access_list: [{binary(), [binary()]}]
        }

  @impl Ethers.Transaction
  def new(params) do
    to = params[:to]
    input = params[:input] || params[:data] || ""
    value = params[:value] || 0

    with :ok <- validate_non_neg_integer(params.chain_id),
         :ok <- validate_non_neg_integer(params.nonce),
         :ok <- validate_non_neg_integer(params.max_priority_fee_per_gas),
         :ok <- validate_non_neg_integer(params.max_fee_per_gas),
         :ok <- validate_non_neg_integer(params.gas),
         :ok <- validate_non_neg_integer(value),
         :ok <- validate_binary(input) do
      {:ok,
       %__MODULE__{
         chain_id: params.chain_id,
         nonce: params.nonce,
         max_priority_fee_per_gas: params.max_priority_fee_per_gas,
         max_fee_per_gas: params.max_fee_per_gas,
         gas: params.gas,
         to: to && Utils.to_checksum_address(to),
         value: value,
         input: input,
         access_list: params[:access_list] || []
       }}
    end
  end

  @impl Ethers.Transaction
  def auto_fetchable_fields do
    [:chain_id, :nonce, :max_priority_fee_per_gas, :max_fee_per_gas, :gas]
  end

  @impl Ethers.Transaction
  def type_envelope, do: <<type_id()>>

  @impl Ethers.Transaction
  def type_id, do: @type_id

  @impl Ethers.Transaction
  def from_rlp_list([
        chain_id,
        nonce,
        max_priority_fee_per_gas,
        max_fee_per_gas,
        gas,
        to,
        value,
        input,
        access_list | rest
      ]) do
    {:ok,
     %__MODULE__{
       chain_id: :binary.decode_unsigned(chain_id),
       nonce: :binary.decode_unsigned(nonce),
       max_priority_fee_per_gas: :binary.decode_unsigned(max_priority_fee_per_gas),
       max_fee_per_gas: :binary.decode_unsigned(max_fee_per_gas),
       gas: :binary.decode_unsigned(gas),
       to: (to != "" && Utils.encode_address!(to)) || nil,
       value: :binary.decode_unsigned(value),
       input: input,
       access_list: access_list
     }, rest}
  end

  def from_rlp_list(_rlp_list), do: {:error, :transaction_decode_failed}

  defimpl Ethers.Transaction.Protocol do
    def type_id(_transaction), do: @for.type_id()

    def type_envelope(_transaction), do: @for.type_envelope()

    def to_rlp_list(tx, _mode) do
      # Eip1559 does not discriminate in RLP encoding between payload and hash
      [
        tx.chain_id,
        tx.nonce,
        tx.max_priority_fee_per_gas,
        tx.max_fee_per_gas,
        tx.gas,
        (tx.to && Utils.decode_address!(tx.to)) || "",
        tx.value,
        tx.input,
        tx.access_list || []
      ]
    end
  end
end
