{-# LANGUAGE ViewPatterns #-}
module Plutus.Contract.Test.MissingLovelace
  ( calculateDelta
  ) where

import Ledger.Ada qualified as Ada
import Ledger.Value (Value, noAdaValue)
import PlutusTx.Prelude qualified as P

-- | Returns the calculated delta between initial and final values. Might be false positive.
--
-- The tests check if a wallet's funds are equal to some expected value at the end.
-- Unfortunately, because of the adjustion of transactions, the outputs' costs change
-- and it's hard to track these changes in the tests layer.
--
-- This function tries to check if the difference between final and initial values ('realDelta')
-- is a result of combination of operations between output's costs and the expected delta.
--
-- There is a risk when expected delta has only ada part and expected delta /= realDelta
-- and realDelta is divisible by some delta from deltas, then we will return realDelta's ada.
-- Which means that the test will pass but without strong confidence in wallets' funds consistency.
-- For example, we expected -n, but there is n among deltas and realDelta is n,
-- it is divisible by n, then the test will pass. So please be careful.
calculateDelta
  :: Value
  -- ^ Expected delta of the test
  -> Ada.Ada
  -- ^ Initial value of the wallet before the test
  -> Ada.Ada
  -- ^ Final value of the wallet after the test
  -> [Ada.Ada]
  -- ^ Missing lovelace costs of outputs from 'AdjustingUnbalancedTx' logs
  -> Value
calculateDelta :: Value -> Ada -> Ada -> [Ada] -> Value
calculateDelta Value
expectedDelta Ada
initialValue Ada
finalValue [Ada]
allWalletsTxOutCosts =
  let
    expectedAda :: Ada
expectedAda = Value -> Ada
Ada.fromValue Value
expectedDelta

    -- the list of deltas: combinations (+/-) between outputs' costs,
    -- the expected delta and the wallet's output costs.
    deltas :: [Ada]
deltas = (Ada -> Ada) -> [Ada] -> [Ada]
forall a b. (a -> b) -> [a] -> [b]
map Ada -> Ada
forall n. (Ord n, AdditiveGroup n) => n -> n
P.abs ([Ada] -> [Ada]) -> [Ada] -> [Ada]
forall a b. (a -> b) -> a -> b
$ [[Ada]] -> [Ada]
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat
      [ [ Ada -> Ada
forall n. (Ord n, AdditiveGroup n) => n -> n
P.abs Ada
val Ada -> Ada -> Ada
forall a. AdditiveGroup a => a -> a -> a
P.- Ada -> Ada
forall n. (Ord n, AdditiveGroup n) => n -> n
P.abs Ada
wCost
        , Ada -> Ada
forall n. (Ord n, AdditiveGroup n) => n -> n
P.abs Ada
val Ada -> Ada -> Ada
forall a. AdditiveSemigroup a => a -> a -> a
P.+ Ada -> Ada
forall n. (Ord n, AdditiveGroup n) => n -> n
P.abs Ada
wCost ] | Ada
val <- [Ada
expectedAda, Ada
0] [Ada] -> [Ada] -> [Ada]
forall a. [a] -> [a] -> [a]
++ [Ada]
allWalletsTxOutCosts
                                      , Ada
wCost <- [Ada]
allWalletsTxOutCosts ]

    realDelta :: Ada
realDelta = Ada
finalValue Ada -> Ada -> Ada
forall a. AdditiveGroup a => a -> a -> a
P.- Ada
initialValue

    missingDelta :: Value
missingDelta =
      -- We check if 'realDelta' is a result of combination of operations between initial delta and outputs' costs
      -- by checking if 'realDelta''s is divisible by any delta without a reminder.
      if [Bool] -> Bool
forall (t :: * -> *). Foldable t => t Bool -> Bool
or [(Ada -> Ada
forall n. (Ord n, AdditiveGroup n) => n -> n
P.abs Ada
realDelta) Ada -> Ada -> Ada
forall a. Integral a => a -> a -> a
`mod` Ada
d Ada -> Ada -> Bool
forall a. Eq a => a -> a -> Bool
== Ada
0 | Ada
d <- [Ada]
deltas, Ada
d Ada -> Ada -> Bool
forall a. Eq a => a -> a -> Bool
/= Ada
0] then
        -- if yes, we return a sum of 'realDelta''s ada with non-ada value of the expected delta
        let missingAda :: Value
missingAda = Ada -> Value
Ada.toValue Ada
realDelta
            missingNonAda :: Value
missingNonAda = Value -> Value
noAdaValue Value
expectedDelta
        in Value
missingAda Value -> Value -> Value
forall a. Semigroup a => a -> a -> a
<> Value
missingNonAda
      -- otherwise we just return the expected delta
      else Value
expectedDelta
  in
    -- if ada in the expected delta is the same as the real delta, then we don't need to check anything
    -- and can just return it
    if Ada
expectedAda Ada -> Ada -> Bool
forall a. Eq a => a -> a -> Bool
== Ada
realDelta then Value
expectedDelta
    -- otherwise we return the missing delta that is needed to pass the test
    else Value
missingDelta