Skip to content

Commit 575ba8d

Browse files
committed
waitTxStatusChange rework
- docs - clenaup - testnet tested
1 parent c8e1c17 commit 575ba8d

File tree

5 files changed

+93
-70
lines changed

5 files changed

+93
-70
lines changed

src/BotPlutusInterface/Config.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,12 @@ txStatusPollingSpec =
9595
reqSection'
9696
"milliseconds"
9797
naturalSpec
98-
"Interval between chain-index queries for transacions status change detection"
98+
"Interval between chain-index queries for transactions status change detection"
9999
spBlocksTimeOut <-
100100
reqSection'
101101
"blocks"
102102
naturalSpec
103-
"Timeout (in blocks) after which awating of transaction status change will be cancelled and current staus returned"
103+
"Timeout (in blocks) after which awaiting of transaction status change will be cancelled and current Status returned"
104104
pure $ TxStatusPolling {..}
105105

106106
{- ORMOLU_DISABLE -}
@@ -242,7 +242,7 @@ pabConfigSpec = sectionsSpec "PABConfig" $ do
242242
(pcTxStatusPolling def)
243243
"pcTxStatusPolling"
244244
txStatusPollingSpec
245-
"TODO: TxStatusPolling config help" -- FIXME
245+
"Set interval between `chain-index` queries and number of blocks to wait until timeout while await Transaction status to change"
246246
pure PABConfig {..}
247247

248248
docPABConfig :: String

src/BotPlutusInterface/Contract.hs

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@ import Control.Monad.Trans.Class (lift)
4747
import Control.Monad.Trans.Either (EitherT, eitherT, firstEitherT, newEitherT)
4848
import Data.Aeson (ToJSON, Value (Array, Bool, Null, Number, Object, String))
4949
import Data.Aeson.Extras (encodeByteString)
50+
import Data.Function (fix)
5051
import Data.HashMap.Strict qualified as HM
5152
import Data.Kind (Type)
5253
import Data.Map qualified as Map
5354
import Data.Row (Row)
5455
import Data.Text (Text)
5556
import Data.Text qualified as Text
5657
import Data.Vector qualified as V
57-
import Debug.Trace (traceM)
5858
import Ledger (POSIXTime)
5959
import Ledger qualified
6060
import Ledger.Address (PaymentPubKeyHash (PaymentPubKeyHash))
@@ -205,49 +205,49 @@ handlePABReq contractEnv req = do
205205
printBpiLog @w Debug $ pretty resp
206206
pure resp
207207

208+
{- | Await till transaction status change to something from `Unknown`.
209+
Uses `chain-index` to query transaction by id.
210+
Important notes:
211+
* if transaction is not found in `chain-index` status considered to be `Unknown`
212+
* if transaction is found but `transactionStatus` failed to make status - status considered to be `Unknown`
213+
* uses `TxStatusPolling` to set `chain-index` polling interval and number of blocks to wait until timeout,
214+
if timeout is reached, returns whatever status it was able to get during last check
215+
-}
208216
awaitTxStatusChange ::
209217
forall (w :: Type) (effs :: [Type -> Type]).
210218
Member (PABEffect w) effs =>
211219
ContractEnvironment w ->
212220
Ledger.TxId ->
213221
Eff effs TxStatus
214222
awaitTxStatusChange contractEnv txId = do
215-
traceM "@@ await stats change"
216223
checkStartedBlock <- currentBlock contractEnv
217224
printBpiLog @w Debug $ pretty $ "Awaiting status change for " ++ show txId
218-
txStausCheckLoop txId contractEnv checkStartedBlock
219225

220-
txStausCheckLoop ::
221-
forall (w :: Type) (effs :: [Type -> Type]).
222-
Member (PABEffect w) effs =>
223-
Ledger.TxId ->
224-
ContractEnvironment w ->
225-
Integer ->
226-
Eff effs TxStatus
227-
txStausCheckLoop txId contractEnv checkStartedBlock = do
228226
let txStatusPolling = contractEnv.cePABConfig.pcTxStatusPolling
229227
pollInterval = fromIntegral $ txStatusPolling.spInterval
230228
pollTimeout = txStatusPolling.spBlocksTimeOut
231229
cutOffBlock = checkStartedBlock + fromIntegral pollTimeout
232-
currBlock <- currentBlock contractEnv
233-
txStatus <- getStatus
234-
case (txStatus, currBlock > cutOffBlock) of
235-
(status, True) -> do
236-
logDebug . mconcat $
237-
[ "Timeout period for waiting `TxId "
238-
, show txId
239-
, "` status cahnge is over"
240-
, " - waited "
241-
, show pollTimeout
242-
, " blocks."
243-
, " Current status: "
244-
, show status
245-
]
246-
return status
247-
(Unknown, _) -> do
248-
threadDelay @w pollInterval
249-
retry
250-
(status, _) -> return status
230+
231+
fix $ \loop -> do
232+
currBlock <- currentBlock contractEnv
233+
txStatus <- getStatus
234+
case (txStatus, currBlock > cutOffBlock) of
235+
(status, True) -> do
236+
logDebug . mconcat $
237+
[ "Timeout for waiting `TxId "
238+
, show txId
239+
, "` status cahnge reached"
240+
, " - waited "
241+
, show pollTimeout
242+
, " blocks."
243+
, " Current status: "
244+
, show status
245+
]
246+
return status
247+
(Unknown, _) -> do
248+
threadDelay @w pollInterval
249+
loop
250+
(status, _) -> return status
251251
where
252252
getStatus = do
253253
mTx <- queryChainIndexForTxState
@@ -260,7 +260,7 @@ txStausCheckLoop txId contractEnv checkStartedBlock = do
260260
blk <- fromInteger <$> currentBlock contractEnv
261261
case transactionStatus blk txState txId of
262262
Left e -> do
263-
logDebug $ "Staus check for TxId " ++ show txId ++ " failed with " ++ show e
263+
logDebug $ "Status check for TxId " ++ show txId ++ " failed with " ++ show e
264264
return Unknown
265265
Right st -> do
266266
logDebug $ "Status for TxId " ++ show txId ++ " is " ++ show st
@@ -276,9 +276,6 @@ txStausCheckLoop txId contractEnv checkStartedBlock = do
276276
Nothing -> pure Nothing
277277

278278
logDebug = printBpiLog @w Debug . pretty
279-
-- logDebug = traceM . show . pretty
280-
281-
retry = txStausCheckLoop txId contractEnv checkStartedBlock
282279

283280
-- | This will FULLY balance a transaction
284281
balanceTx ::

src/BotPlutusInterface/Types.hs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,16 @@ data PABConfig = PABConfig
8888
}
8989
deriving stock (Show, Eq)
9090

91+
{- | Settings for `Contract.awaitTxStatusChange` implementation.
92+
See also `BotPlutusInterface.Contract.awaitTxStatusChange`
93+
-}
9194
data TxStatusPolling = TxStatusPolling
92-
{ -- | mocroseconds
95+
{ -- | Interval between `chain-index` queries, microseconds
9396
spInterval :: !Natural
94-
, -- | blocks until timeout, most likely `Unknown` state will be returned
97+
, -- | Number of blocks to wait until timeout.
98+
-- Timeout is required because transaction can be silently discarded from node mempool
99+
-- and never appear in `chain-index` even if it was submitted successfully to the node
100+
-- (chain-sync protocol won't help here also)
95101
spBlocksTimeOut :: !Natural
96102
}
97103
deriving stock (Show, Eq)

test/Spec/BotPlutusInterface/TxStatusChange.hs

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,26 @@ module Spec.BotPlutusInterface.TxStatusChange (tests) where
22

33
import BotPlutusInterface.Types (
44
ContractEnvironment (cePABConfig),
5-
PABConfig (pcOwnPubKeyHash),
5+
PABConfig (pcOwnPubKeyHash, pcTxStatusPolling),
6+
TxStatusPolling (spBlocksTimeOut),
67
)
7-
import Control.Lens ((&), (.~))
8-
import Control.Monad (void)
8+
import Control.Lens ((&), (.~), (^.))
99
import Data.Default (def)
1010
import Data.Text (Text)
1111
import Data.Text qualified as Text
1212
import Ledger (PaymentPubKeyHash (unPaymentPubKeyHash), getCardanoTxId)
1313
import Ledger.Ada qualified as Ada
1414
import Ledger.Constraints qualified as Constraints
1515
import Ledger.Tx (TxOut (TxOut), TxOutRef (TxOutRef))
16-
import Plutus.ChainIndex (RollbackState (Unknown), TxStatus)
16+
import Plutus.ChainIndex (RollbackState (Unknown), Tip (TipAtGenesis), TxStatus)
17+
import Plutus.ChainIndex.Types (Tip (Tip))
1718
import Plutus.Contract (
1819
Contract (..),
1920
Endpoint,
2021
awaitTxStatusChange,
22+
getTip,
2123
submitTx,
24+
throwError,
2225
)
2326
import Spec.MockContract (
2427
contractEnv,
@@ -27,10 +30,11 @@ import Spec.MockContract (
2730
paymentPkh2,
2831
pkhAddr1,
2932
runContractPure,
33+
tip,
3034
utxos,
3135
)
3236
import Test.Tasty (TestTree, testGroup)
33-
import Test.Tasty.HUnit (Assertion, assertFailure, testCase, (@?=))
37+
import Test.Tasty.HUnit (Assertion, assertBool, assertFailure, testCase, (@?=))
3438
import Prelude
3539

3640
tests :: TestTree
@@ -51,28 +55,45 @@ testTxFoundAndConfirmed = do
5155
pabConf = def {pcOwnPubKeyHash = unPaymentPubKeyHash paymentPkh1}
5256
contractEnv' = def {cePABConfig = pabConf}
5357

54-
contract :: Contract () (Endpoint "SendAda" ()) Text ()
58+
contract :: Contract () (Endpoint "SendAda" ()) Text TxStatus
5559
contract = do
5660
let constraints =
5761
Constraints.mustPayToPubKey paymentPkh2 (Ada.lovelaceValueOf 1000)
5862
tx <- submitTx constraints
59-
void $ awaitTxStatusChange $ getCardanoTxId tx
63+
awaitTxStatusChange $ getCardanoTxId tx
6064

6165
case runContractPure contract initState of
6266
(Left err, _) -> assertFailure $ Text.unpack err
67+
(Right Unknown, _) -> assertFailure "State should not be Unknown"
6368
(Right _, _) -> pure ()
6469

6570
testStopWaitingByTimeout :: Assertion
6671
testStopWaitingByTimeout = do
6772
let initState =
6873
def & contractEnv .~ contractEnv'
6974
pabConf = def {pcOwnPubKeyHash = unPaymentPubKeyHash paymentPkh1}
75+
timeoutBlocks = fromIntegral . spBlocksTimeOut . pcTxStatusPolling $ pabConf
7076
contractEnv' = def {cePABConfig = pabConf}
7177

72-
contract :: Contract () (Endpoint "SendAda" ()) Text TxStatus
73-
contract =
74-
awaitTxStatusChange nonExistingTxId
78+
contract :: Contract () (Endpoint "SendAda" ()) Text (Tip, TxStatus)
79+
contract = do
80+
awaitStartBlock <- getTip
81+
case awaitStartBlock of
82+
TipAtGenesis -> throwError "Should not happen: TipAtGenesis"
83+
tip' -> do
84+
txStatus <- awaitTxStatusChange nonExistingTxId
85+
return (tip', txStatus)
7586

7687
case runContractPure contract initState of
7788
(Left err, _) -> assertFailure $ Text.unpack err
78-
(Right txStatus, _) -> txStatus @?= Unknown
89+
(Right (startTip, txStatus), state) -> do
90+
startAwaitingBlockNo <- getBlock startTip
91+
endAwaitingBlockNo <- getBlock $ state ^. tip
92+
assertBool
93+
"Current block should be GT than start + timeout"
94+
(endAwaitingBlockNo > startAwaitingBlockNo + timeoutBlocks)
95+
txStatus @?= Unknown
96+
where
97+
getBlock = \case
98+
TipAtGenesis -> assertFailure "Should not happen: TipAtGenesis"
99+
Tip _ _ blockNo -> pure blockNo

test/Spec/MockContract.hs

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -566,26 +566,25 @@ mockQueryChainIndex = \case
566566
TxOutFromRef txOutRef -> do
567567
state <- get @(MockContractState w)
568568
pure $ TxOutRefResponse $ Tx.fromTxOut =<< lookup txOutRef (state ^. utxos)
569-
TxFromTxId txId -> case txId == nonExistingTxId of
570-
True -> pure $ TxIdResponse Nothing
571-
False -> do
572-
-- TODO: Track some kind of state here, add tests to ensure this works correctly
573-
-- For now, empty txs
574-
state <- get @(MockContractState w)
575-
let knownUtxos = state ^. utxos
576-
pure $
577-
TxIdResponse $
578-
Just $
579-
ChainIndexTx
580-
{ _citxTxId = txId
581-
, _citxInputs = mempty
582-
, _citxOutputs = buildOutputsFromKnownUTxOs knownUtxos txId
583-
, _citxValidRange = Ledger.always
584-
, _citxData = mempty
585-
, _citxRedeemers = mempty
586-
, _citxScripts = mempty
587-
, _citxCardanoTx = Nothing
588-
}
569+
TxFromTxId txId ->
570+
if txId == nonExistingTxId
571+
then pure $ TxIdResponse Nothing
572+
else do
573+
-- TODO: Track some kind of state here, add tests to ensure this works correctly
574+
-- For now, empty txs
575+
state <- get @(MockContractState w)
576+
let knownUtxos = state ^. utxos
577+
pure . TxIdResponse . Just $
578+
ChainIndexTx
579+
{ _citxTxId = txId
580+
, _citxInputs = mempty
581+
, _citxOutputs = buildOutputsFromKnownUTxOs knownUtxos txId
582+
, _citxValidRange = Ledger.always
583+
, _citxData = mempty
584+
, _citxRedeemers = mempty
585+
, _citxScripts = mempty
586+
, _citxCardanoTx = Nothing
587+
}
589588
UtxoSetMembership _ ->
590589
throwError @Text "UtxoSetMembership is unimplemented"
591590
UtxoSetAtAddress pageQuery _ -> do

0 commit comments

Comments
 (0)