# Nimbus
# Copyright (c) 2018-2024 Status Research & Development GmbH
# Licensed under either of
#  * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
#    http://www.apache.org/licenses/LICENSE-2.0)
#  * MIT license ([LICENSE-MIT](LICENSE-MIT) or
#    http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.

## Transaction Pool Descriptor
## ===========================
##

import
  std/[times],
  ../../common/common,
  ./tx_chain,
  ./tx_info,
  ./tx_item,
  ./tx_tabs,
  ./tx_tabs/tx_sender, # for verify()
  eth/keys

{.push raises: [].}

type
  TxPoolCallBackRecursion* = object of Defect
    ## Attempt to recurse a call back function

  TxPoolFlags* = enum ##\
    ## Processing strategy selector symbols

    stageItems1559MinFee ##\
      ## Stage tx items with `tx.maxFee` at least `minFeePrice`. Other items
      ## are left or set pending. This symbol affects post-London tx items,
      ## only.

    stageItems1559MinTip ##\
      ## Stage tx items with `tx.effectiveGasTip(baseFee)` at least
      ## `minTipPrice`. Other items are considered underpriced and left
      ## or set pending. This symbol affects post-London tx items, only.

    stageItemsPlMinPrice ##\
      ## Stage tx items with `tx.gasPrice` at least `minPreLondonGasPrice`.
      ## Other items are considered underpriced and left or set pending.
      ## This symbol affects pre-London tx items, only.

    # -----------

    packItemsMaxGasLimit ##\
      ## It set, the *packer* will execute and collect additional items from
      ## the `staged` bucket while accumulating `gasUsed` as long as
      ## `maxGasLimit` is not exceeded. If `packItemsTryHarder` flag is also
      ## set, the *packer* will not stop until at least `hwmGasLimit` is
      ## reached.
      ##
      ## Otherwise the *packer* will accumulate up until `trgGasLimit` is
      ## not exceeded, and not stop until at least `lwmGasLimit` is reached
      ## in case `packItemsTryHarder` is also set,

    packItemsTryHarder ##\
      ## It set, the *packer* will *not* stop accumulaing transactions up until
      ## the `lwmGasLimit` or `hwmGasLimit` is reached, depending on whether
      ## the `packItemsMaxGasLimit` is set. Otherwise, accumulating stops
      ## immediately before the next transaction exceeds `trgGasLimit`, or
      ## `maxGasLimit` depending on `packItemsMaxGasLimit`.

    # -----------

    autoUpdateBucketsDB ##\
      ## Automatically update the state buckets after running batch jobs if
      ## the `dirtyBuckets` flag is also set.

    autoZombifyUnpacked ##\
      ## Automatically dispose *pending* or *staged* txs that were queued
      ## at least `lifeTime` ago.

    autoZombifyPacked ##\
      ## Automatically dispose *packed* txs that were queued
      ## at least `lifeTime` ago.

  TxPoolParam* = tuple          ## Getter/setter accessible parameters
    minFeePrice: GasPrice       ## Gas price enforced by the pool, `gasFeeCap`
    minTipPrice: GasPrice       ## Desired tip-per-tx target, `effectiveGasTip`
    minPlGasPrice: GasPrice     ## Desired pre-London min `gasPrice`
    dirtyBuckets: bool          ## Buckets need to be updated
    doubleCheck: seq[TxItemRef] ## Check items after moving block chain head
    flags: set[TxPoolFlags]     ## Processing strategy symbols

  TxPoolRef* = ref object of RootObj ##\
    ## Transaction pool descriptor
    startDate: Time             ## Start date (read-only)

    chain: TxChainRef           ## block chain state
    txDB: TxTabsRef             ## Transaction lists & tables

    lifeTime*: times.Duration   ## Maximum life time of a tx in the system
    priceBump*: uint            ## Min precentage price when superseding
    blockValue*: UInt256        ## Sum of reward received by feeRecipient

    param: TxPoolParam          ## Getter/Setter parameters

const
  txItemLifeTime = ##\
    ## Maximum amount of time transactions can be held in the database\
    ## unless they are packed already for a block. This default is chosen\
    ## as found in core/tx_pool.go(184) of the geth implementation.
    initDuration(hours = 3)

  txPriceBump = ##\
    ## Minimum price bump percentage to replace an already existing\
    ## transaction (nonce). This default is chosen as found in\
    ## core/tx_pool.go(177) of the geth implementation.
    10u

  txMinFeePrice = 1.GasPrice
  txMinTipPrice = 1.GasPrice
  txPoolFlags = {stageItems1559MinTip,
                  stageItems1559MinFee,
                  stageItemsPlMinPrice,
                  packItemsTryHarder,
                  autoUpdateBucketsDB,
                  autoZombifyUnpacked}

# ------------------------------------------------------------------------------
# Public functions, constructor
# ------------------------------------------------------------------------------

proc init*(xp: TxPoolRef; com: CommonRef)
    {.gcsafe,raises: [CatchableError].} =
  ## Constructor, returns new tx-pool descriptor.
  xp.startDate = getTime().utc.toTime

  xp.chain = TxChainRef.new(com)
  xp.txDB = TxTabsRef.new

  xp.lifeTime = txItemLifeTime
  xp.priceBump = txPriceBump

  xp.param.reset
  xp.param.minFeePrice = txMinFeePrice
  xp.param.minTipPrice = txMinTipPrice
  xp.param.flags = txPoolFlags

# ------------------------------------------------------------------------------
# Public functions, getters
# ------------------------------------------------------------------------------

func chain*(xp: TxPoolRef): TxChainRef =
  ## Getter, block chain DB
  xp.chain

func pFlags*(xp: TxPoolRef): set[TxPoolFlags] =
  ## Returns the set of algorithm strategy symbols for labelling items
  ## as`packed`
  xp.param.flags

func pDirtyBuckets*(xp: TxPoolRef): bool =
  ## Getter, buckets need update
  xp.param.dirtyBuckets

func pDoubleCheck*(xp: TxPoolRef): seq[TxItemRef] =
  ## Getter, cached block chain head was moved back
  xp.param.doubleCheck

func pMinFeePrice*(xp: TxPoolRef): GasPrice =
  ## Getter
  xp.param.minFeePrice

func pMinTipPrice*(xp: TxPoolRef): GasPrice =
  ## Getter
  xp.param.minTipPrice

func pMinPlGasPrice*(xp: TxPoolRef): GasPrice =
  ## Getter
  xp.param.minPlGasPrice

func startDate*(xp: TxPoolRef): Time =
  ## Getter
  xp.startDate

func txDB*(xp: TxPoolRef): TxTabsRef =
  ## Getter, pool database
  xp.txDB

# ------------------------------------------------------------------------------
# Public functions, setters
# ------------------------------------------------------------------------------

func `pDirtyBuckets=`*(xp: TxPoolRef; val: bool) =
  ## Setter
  xp.param.dirtyBuckets = val

func pDoubleCheckAdd*(xp: TxPoolRef; val: seq[TxItemRef]) =
  ## Pseudo setter
  xp.param.doubleCheck.add val

func pDoubleCheckFlush*(xp: TxPoolRef) =
  ## Pseudo setter
  xp.param.doubleCheck.setLen(0)

func `pFlags=`*(xp: TxPoolRef; val: set[TxPoolFlags]) =
  ## Install a set of algorithm strategy symbols for labelling items as`packed`
  xp.param.flags = val

func `pMinFeePrice=`*(xp: TxPoolRef; val: GasPrice) =
  ## Setter
  xp.param.minFeePrice = val

func `pMinTipPrice=`*(xp: TxPoolRef; val: GasPrice) =
  ## Setter
  xp.param.minTipPrice = val

func `pMinPlGasPrice=`*(xp: TxPoolRef; val: GasPrice) =
  ## Setter
  xp.param.minPlGasPrice = val

# ------------------------------------------------------------------------------
# Public functions, heplers (debugging only)
# ------------------------------------------------------------------------------

proc verify*(xp: TxPoolRef): Result[void,TxInfo]
    {.gcsafe, raises: [CatchableError].} =
  ## Verify descriptor and subsequent data structures.

  block:
    let rc = xp.txDB.verify
    if rc.isErr:
      return rc

  # verify consecutive nonces per sender
  var
    initOk = false
    lastSender: EthAddress
    lastNonce: AccountNonce
    lastSublist: TxSenderSchedRef

  for (_,nonceList) in xp.txDB.incAccount:
    for item in nonceList.incNonce:
      if not initOk or lastSender != item.sender:
        initOk = true
        lastSender = item.sender
        lastNonce = item.tx.nonce
        lastSublist = xp.txDB.bySender.eq(item.sender).value.data
      elif lastNonce + 1 == item.tx.nonce:
        lastNonce = item.tx.nonce
      else:
        return err(txInfoVfyNonceChain)

      # verify bucket boundary conditions
      case item.status:
      of txItemPending:
        discard
      of txItemStaged:
        if lastSublist.eq(txItemPending).eq(item.tx.nonce - 1).isOk:
          return err(txInfoVfyNonceChain)
      of txItemPacked:
        if lastSublist.eq(txItemPending).eq(item.tx.nonce - 1).isOk:
          return err(txInfoVfyNonceChain)
        if lastSublist.eq(txItemStaged).eq(item.tx.nonce - 1).isOk:
          return err(txInfoVfyNonceChain)

  ok()

# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------