Skip to content

Commit 857ec74

Browse files
authored
Fix chain index query filtering (#153)
* Adds handling of missing cases when collateral should not be returned by chain-index responses * Adds node query effect like UtxosAt but with filtering - UtxosAtExcluding * Enables other collateral tests (they were not included to suite for some reason) * Fixes some collateral-affected tests
1 parent 67db4a6 commit 857ec74

File tree

9 files changed

+293
-93
lines changed

9 files changed

+293
-93
lines changed

src/BotPlutusInterface/Balance.hs

+11-5
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ module BotPlutusInterface.Balance (
1313

1414
import BotPlutusInterface.BodyBuilder qualified as BodyBuilder
1515
import BotPlutusInterface.CardanoCLI qualified as CardanoCLI
16-
import BotPlutusInterface.CardanoNode.Effects (NodeQuery (UtxosAt))
16+
import BotPlutusInterface.CardanoNode.Effects (NodeQuery (UtxosAt, UtxosAtExcluding))
1717
import BotPlutusInterface.CoinSelection (selectTxIns)
18-
import BotPlutusInterface.Collateral (removeCollateralFromMap)
18+
1919
import BotPlutusInterface.Effects (
2020
PABEffect,
2121
createDirectoryIfMissingCLI,
@@ -247,8 +247,14 @@ utxosAndCollateralAtAddress ::
247247
Eff effs (Either Text (Map TxOutRef Tx.ChainIndexTxOut, Maybe CollateralUtxo))
248248
utxosAndCollateralAtAddress balanceCfg _pabConf changeAddr =
249249
runEitherT $ do
250-
utxos <- firstEitherT (Text.pack . show) $ newEitherT $ queryNode @w (UtxosAt changeAddr)
251250
inMemCollateral <- lift $ getInMemCollateral @w
251+
let nodeQuery =
252+
maybe
253+
(UtxosAt changeAddr)
254+
(UtxosAtExcluding changeAddr . Set.singleton . collateralTxOutRef)
255+
inMemCollateral
256+
257+
utxos <- firstEitherT (Text.pack . show) $ newEitherT $ queryNode @w nodeQuery
252258

253259
-- check if `bcHasScripts` is true, if this is the case then we search of
254260
-- collateral UTxO in the environment, if such collateral is not present we throw Error.
@@ -259,9 +265,9 @@ utxosAndCollateralAtAddress balanceCfg _pabConf changeAddr =
259265
"The given transaction uses script, but there's no collateral provided."
260266
<> "This usually means that, we failed to create Tx and update our ContractEnvironment."
261267
)
262-
(const $ pure (removeCollateralFromMap inMemCollateral utxos, inMemCollateral))
268+
(const $ pure (utxos, inMemCollateral))
263269
inMemCollateral
264-
else pure (removeCollateralFromMap inMemCollateral utxos, Nothing)
270+
else pure (utxos, Nothing)
265271

266272
hasChangeUTxO :: Address -> Tx -> Bool
267273
hasChangeUTxO changeAddr tx =

src/BotPlutusInterface/CardanoNode/Effects.hs

+8-1
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ import Control.Monad.Trans.Class (lift)
4141
import Control.Monad.Trans.Either (firstEitherT, hoistEither, newEitherT, runEitherT)
4242
import Data.Map (Map)
4343
import Data.Map qualified as Map
44+
import Data.Set (Set)
4445
import Data.Set qualified as Set
4546
import Ledger.Address (Address)
46-
import Ledger.Tx (ChainIndexTxOut (..))
47+
import Ledger.Tx (ChainIndexTxOut (..), TxOutRef)
4748
import Ledger.Tx.CardanoAPI qualified as TxApi
4849
import Plutus.V2.Ledger.Tx qualified as V2
4950
import Prelude
@@ -54,6 +55,9 @@ import Prelude
5455
data NodeQuery a where
5556
-- | 'UtxosAt' queries local node to get all the utxos at particular address.
5657
UtxosAt :: Address -> NodeQuery (Either NodeQueryError (Map V2.TxOutRef ChainIndexTxOut))
58+
-- | 'UtxosAtExcluding' queries local node to get all the utxos at particular address
59+
-- excluding `TxOutRefs`'s specified in `Set`.
60+
UtxosAtExcluding :: Address -> Set TxOutRef -> NodeQuery (Either NodeQueryError (Map V2.TxOutRef ChainIndexTxOut))
5761
-- | 'PParams' queries local node to get it's 'ProtocolParameters'.
5862
PParams :: NodeQuery (Either NodeQueryError CApi.S.ProtocolParameters)
5963

@@ -78,6 +82,9 @@ handleNodeQuery =
7882
interpret $ \case
7983
UtxosAt addr -> handleUtxosAt addr
8084
PParams -> queryBabbageEra CApi.QueryProtocolParameters
85+
UtxosAtExcluding addr excluded ->
86+
let filterOuts = Map.filterWithKey (\oref _ -> not $ oref `Set.member` excluded)
87+
in fmap filterOuts <$> handleUtxosAt addr
8188

8289
handleUtxosAt ::
8390
forall effs.

src/BotPlutusInterface/ChainIndex.hs

+11-21
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ module BotPlutusInterface.ChainIndex (
44
handleChainIndexReq,
55
) where
66

7-
import BotPlutusInterface.Collateral (removeCollateralFromPage)
87
import BotPlutusInterface.Types (
98
ContractEnvironment (ContractEnvironment, cePABConfig),
109
PABConfig,
11-
readCollateralUtxo,
1210
)
1311
import Data.Kind (Type)
1412
import Network.HTTP.Client (
@@ -21,10 +19,10 @@ import Network.HTTP.Types (Status (statusCode))
2119
import Plutus.ChainIndex.Api (
2220
QueryAtAddressRequest (QueryAtAddressRequest),
2321
TxoAtAddressRequest (TxoAtAddressRequest),
24-
TxosResponse (TxosResponse),
22+
TxosResponse,
2523
UtxoAtAddressRequest (UtxoAtAddressRequest),
2624
UtxoWithCurrencyRequest (UtxoWithCurrencyRequest),
27-
UtxosResponse (UtxosResponse),
25+
UtxosResponse,
2826
)
2927
import Plutus.ChainIndex.Client qualified as ChainIndexClient
3028
import Plutus.Contract.Effects (ChainIndexQuery (..), ChainIndexResponse (..))
@@ -73,7 +71,7 @@ handleChainIndexReq contractEnv@ContractEnvironment {cePABConfig} =
7371
contractEnv
7472
(ChainIndexClient.getUtxoSetAtAddress (UtxoAtAddressRequest (Just page) credential))
7573
UtxoSetWithCurrency page assetClass ->
76-
UtxoSetAtResponse
74+
UtxoSetWithCurrencyResponse
7775
<$> chainIndexUtxoQuery
7876
contractEnv
7977
(ChainIndexClient.getUtxoSetWithCurrency (UtxoWithCurrencyRequest (Just page) assetClass))
@@ -105,24 +103,16 @@ chainIndexQueryOne pabConf endpoint = do
105103
| otherwise -> error (show failureResp)
106104
Left failureResp -> error (show failureResp)
107105

108-
-- | Query for utxo's and filter collateral utxo from result.
106+
-- | Query for utxo's.
109107
chainIndexUtxoQuery :: forall (w :: Type). ContractEnvironment w -> ClientM UtxosResponse -> IO UtxosResponse
110108
chainIndexUtxoQuery contractEnv query = do
111-
collateralUtxo <- readCollateralUtxo contractEnv
112-
let removeCollateral :: UtxosResponse -> UtxosResponse
113-
removeCollateral (UtxosResponse tip page) = UtxosResponse tip (removeCollateralFromPage collateralUtxo page)
114-
removeCollateral
115-
<$> chainIndexQueryMany
116-
contractEnv.cePABConfig
117-
query
109+
chainIndexQueryMany
110+
contractEnv.cePABConfig
111+
query
118112

119-
-- | Query for txo's and filter collateral txo from result.
113+
-- | Query for txo's.
120114
chainIndexTxoQuery :: forall (w :: Type). ContractEnvironment w -> ClientM TxosResponse -> IO TxosResponse
121115
chainIndexTxoQuery contractEnv query = do
122-
collateralUtxo <- readCollateralUtxo contractEnv
123-
let removeCollateral :: TxosResponse -> TxosResponse
124-
removeCollateral (TxosResponse page) = TxosResponse (removeCollateralFromPage collateralUtxo page)
125-
removeCollateral
126-
<$> chainIndexQueryMany
127-
contractEnv.cePABConfig
128-
query
116+
chainIndexQueryMany
117+
contractEnv.cePABConfig
118+
query

src/BotPlutusInterface/Collateral.hs

+83-13
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
module BotPlutusInterface.Collateral (
22
getInMemCollateral,
33
setInMemCollateral,
4-
filterCollateral,
54
mkCollateralTx,
6-
removeCollateralFromPage,
7-
removeCollateralFromMap,
5+
withCollateralHandling,
86
) where
97

108
import BotPlutusInterface.Types (
@@ -16,12 +14,32 @@ import BotPlutusInterface.Types (
1614
)
1715
import Cardano.Prelude (Void)
1816
import Control.Concurrent.STM (atomically, readTVarIO, writeTVar)
17+
import Control.Monad (unless)
1918
import Data.Kind (Type)
20-
import Data.Map (Map)
21-
import Data.Map qualified as Map
22-
import Ledger (ChainIndexTxOut, PaymentPubKeyHash (PaymentPubKeyHash), TxOutRef)
19+
import Ledger (PaymentPubKeyHash (PaymentPubKeyHash), TxOutRef)
2320
import Ledger.Constraints qualified as Constraints
2421
import Plutus.ChainIndex (Page (pageItems))
22+
import Plutus.ChainIndex.Api (
23+
IsUtxoResponse (IsUtxoResponse),
24+
QueryResponse (QueryResponse),
25+
TxosResponse (paget),
26+
UtxosResponse (page),
27+
)
28+
import Plutus.Contract.Effects (
29+
ChainIndexQuery (..),
30+
ChainIndexResponse (
31+
TxOutRefResponse,
32+
TxoSetAtResponse,
33+
UnspentTxOutResponse,
34+
UnspentTxOutsAtResponse,
35+
UtxoSetAtResponse,
36+
UtxoSetMembershipResponse,
37+
UtxoSetWithCurrencyResponse
38+
),
39+
PABReq (ChainIndexQueryReq),
40+
PABResp (ChainIndexQueryResp),
41+
matches,
42+
)
2543
import Prelude
2644

2745
getInMemCollateral :: forall (w :: Type). ContractEnvironment w -> IO (Maybe CollateralUtxo)
@@ -38,16 +56,68 @@ mkCollateralTx pabConf = Constraints.mkTx @Void mempty txc
3856
txc :: Constraints.TxConstraints Void Void
3957
txc = Constraints.mustPayToPubKey (PaymentPubKeyHash $ pcOwnPubKeyHash pabConf) (collateralValue pabConf)
4058

41-
filterCollateral :: CollateralUtxo -> [TxOutRef] -> [TxOutRef]
42-
filterCollateral (CollateralUtxo collateralTxOutRef) = filter (/= collateralTxOutRef)
59+
-- | Middleware to run `chain-index` queries and filter out collateral output from response.
60+
withCollateralHandling ::
61+
Monad m =>
62+
Maybe CollateralUtxo ->
63+
(ChainIndexQuery -> m ChainIndexResponse) ->
64+
ChainIndexQuery ->
65+
m ChainIndexResponse
66+
withCollateralHandling mCollateral runChainIndexQuery = \query -> do
67+
response <-
68+
adjustChainIndexResponse mCollateral query
69+
<$> runChainIndexQuery query
70+
ensureMatches query response
71+
pure response
72+
where
73+
ensureMatches query result =
74+
unless (matches (ChainIndexQueryReq query) (ChainIndexQueryResp result)) $
75+
error $
76+
mconcat
77+
[ "Chain-index request doesn't match response."
78+
, "\nRequest: " ++ show query
79+
, "\nResponse:" ++ show result
80+
]
81+
82+
adjustChainIndexResponse :: Maybe CollateralUtxo -> ChainIndexQuery -> ChainIndexResponse -> ChainIndexResponse
83+
adjustChainIndexResponse mc ciQuery ciResponse =
84+
case mc of
85+
Nothing -> ciResponse
86+
Just (CollateralUtxo collateralOref) -> case (ciQuery, ciResponse) of
87+
-- adjustment based on response
88+
(_, UtxoSetAtResponse utxosResp) ->
89+
let newPage = removeCollateralFromPage mc (page utxosResp)
90+
in UtxoSetAtResponse $ utxosResp {page = newPage}
91+
(_, TxoSetAtResponse txosResp) ->
92+
let newPaget = removeCollateralFromPage mc (paget txosResp)
93+
in TxoSetAtResponse $ txosResp {paget = newPaget}
94+
(_, UnspentTxOutsAtResponse (QueryResponse refsAndOuts nq)) ->
95+
let filtered = filter (\v -> fst v /= collateralOref) refsAndOuts
96+
in UnspentTxOutsAtResponse $ QueryResponse filtered nq
97+
(_, UtxoSetWithCurrencyResponse utxosResp) ->
98+
let newPage = removeCollateralFromPage mc (page utxosResp)
99+
in UtxoSetWithCurrencyResponse $ utxosResp {page = newPage}
100+
-- adjustment based on request
101+
(UtxoSetMembership oref, UtxoSetMembershipResponse (IsUtxoResponse ct isU)) ->
102+
UtxoSetMembershipResponse $
103+
IsUtxoResponse ct $
104+
oref /= collateralOref && isU
105+
(TxOutFromRef oref, TxOutRefResponse _) ->
106+
if collateralOref == oref
107+
then TxOutRefResponse Nothing
108+
else ciResponse
109+
(UnspentTxOutFromRef oref, UnspentTxOutResponse _) ->
110+
if collateralOref == oref
111+
then UnspentTxOutResponse Nothing
112+
else ciResponse
113+
-- all other cases
114+
(_, rest) -> rest
43115

44116
-- | Removes collateral utxo from the UtxoResponse page. Receives `Nothing` if Collateral uninitialized.
45117
removeCollateralFromPage :: Maybe CollateralUtxo -> Page TxOutRef -> Page TxOutRef
46118
removeCollateralFromPage = \case
47119
Nothing -> id
48-
Just txOutRef -> \page -> page {pageItems = filterCollateral txOutRef (pageItems page)}
120+
Just txOutRef -> \page' -> page' {pageItems = filterCollateral txOutRef (pageItems page')}
49121

50-
removeCollateralFromMap :: Maybe CollateralUtxo -> Map TxOutRef ChainIndexTxOut -> Map TxOutRef ChainIndexTxOut
51-
removeCollateralFromMap = \case
52-
Nothing -> id
53-
Just (CollateralUtxo collateral) -> Map.filterWithKey (\oref _ -> collateral /= oref)
122+
filterCollateral :: CollateralUtxo -> [TxOutRef] -> [TxOutRef]
123+
filterCollateral (CollateralUtxo collateralTxOutRef) = filter (/= collateralTxOutRef)

src/BotPlutusInterface/Effects.hs

+7-2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ module BotPlutusInterface.Effects (
3737

3838
import BotPlutusInterface.CardanoNode.Effects (NodeQuery, runNodeQuery)
3939
import BotPlutusInterface.ChainIndex (handleChainIndexReq)
40+
import BotPlutusInterface.Collateral (withCollateralHandling)
4041
import BotPlutusInterface.Collateral qualified as Collateral
4142
import BotPlutusInterface.ExBudget qualified as ExBudget
4243
import BotPlutusInterface.TimeSlot qualified as TimeSlot
@@ -193,8 +194,12 @@ handlePABEffect contractEnv =
193194
Local -> pure ()
194195
Remote ipAddr ->
195196
void $ readProcess "scp" ["-r", Text.unpack dir, Text.unpack $ ipAddr <> ":$HOME"] ""
196-
QueryChainIndex query ->
197-
handleChainIndexReq contractEnv query
197+
QueryChainIndex query -> do
198+
collateralUtxo <- Collateral.getInMemCollateral contractEnv
199+
withCollateralHandling
200+
collateralUtxo
201+
(handleChainIndexReq contractEnv)
202+
query
198203
QueryNode query -> runNodeQuery contractEnv.cePABConfig (send query)
199204
EstimateBudget txPath ->
200205
ExBudget.estimateBudget contractEnv.cePABConfig txPath

test/Spec.hs

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module Main (main) where
33
import Spec.BotPlutusInterface.AdjustUnbalanced qualified
44
import Spec.BotPlutusInterface.Balance qualified
55
import Spec.BotPlutusInterface.CoinSelection qualified
6+
import Spec.BotPlutusInterface.Collateral qualified
67
import Spec.BotPlutusInterface.Contract qualified
78
import Spec.BotPlutusInterface.ContractStats qualified
89
import Spec.BotPlutusInterface.Server qualified
@@ -30,4 +31,5 @@ tests =
3031
, Spec.BotPlutusInterface.ContractStats.tests
3132
, Spec.BotPlutusInterface.TxStatusChange.tests
3233
, Spec.BotPlutusInterface.AdjustUnbalanced.tests
34+
, Spec.BotPlutusInterface.Collateral.tests
3335
]

0 commit comments

Comments
 (0)