Skip to content

Commit 533f2b9

Browse files
Add Script context to budget estimation
Update tests to support this
1 parent 3dce48a commit 533f2b9

File tree

3 files changed

+116
-23
lines changed

3 files changed

+116
-23
lines changed

src/BotPlutusInterface/CardanoCLI.hs

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ module BotPlutusInterface.CardanoCLI (
1414
queryTip,
1515
) where
1616

17-
import BotPlutusInterface.Effects (PABEffect, ShellArgs (..), callCommand)
17+
import BotPlutusInterface.Effects (PABEffect, ShellArgs (..), callCommand, queryChainIndex)
1818
import BotPlutusInterface.Files (
1919
DummyPrivKey (FromSKey, FromVKey),
2020
datumJsonFilePath,
@@ -24,7 +24,7 @@ import BotPlutusInterface.Files (
2424
txFilePath,
2525
validatorScriptFilePath,
2626
)
27-
import BotPlutusInterface.Types (PABConfig, Tip)
27+
import BotPlutusInterface.Types (PABConfig (pcSlotConfig), Tip)
2828
import BotPlutusInterface.UtxoParser qualified as UtxoParser
2929
import Cardano.Api.Shelley (NetworkId (Mainnet, Testnet), NetworkMagic (..), serialiseAddress)
3030
import Codec.Serialise qualified as Codec
@@ -33,6 +33,7 @@ import Control.Monad.Freer (Eff, Member)
3333
import Data.Aeson qualified as JSON
3434
import Data.Aeson.Extras (encodeByteString)
3535
import Data.Attoparsec.Text (parseOnly)
36+
import Data.Bifunctor (second)
3637
import Data.Bool (bool)
3738
import Data.ByteString.Lazy qualified as LazyByteString
3839
import Data.ByteString.Lazy.Char8 qualified as Char8
@@ -41,7 +42,7 @@ import Data.Either (fromRight)
4142
import Data.Either.Combinators (mapLeft, maybeToRight)
4243
import Data.Hex (hex)
4344
import Data.Kind (Type)
44-
import Data.List (sort)
45+
import Data.List (nub, sort)
4546
import Data.Map (Map)
4647
import Data.Map qualified as Map
4748
import Data.Maybe (fromMaybe, maybeToList)
@@ -63,6 +64,7 @@ import Ledger.Interval (
6364
)
6465
import Ledger.Scripts (Datum, DatumHash (..))
6566
import Ledger.Scripts qualified as Scripts
67+
import Ledger.TimeSlot (slotRangeToPOSIXTimeRange)
6668
import Ledger.Tx (
6769
ChainIndexTxOut,
6870
RedeemerPtr (..),
@@ -77,7 +79,9 @@ import Ledger.Tx (
7779
import Ledger.TxId (TxId (..))
7880
import Ledger.Value (Value)
7981
import Ledger.Value qualified as Value
82+
import Plutus.ChainIndex.Tx (txOutRefMap)
8083
import Plutus.Contract.CardanoAPI (toCardanoAddress)
84+
import Plutus.Contract.Effects (ChainIndexQuery (TxsFromTxIds), ChainIndexResponse (TxIdsResponse))
8185
import Plutus.V1.Ledger.Ada (fromValue, getLovelace)
8286
import Plutus.V1.Ledger.Api (
8387
BuiltinData,
@@ -86,7 +90,10 @@ import Plutus.V1.Ledger.Api (
8690
ExCPU (..),
8791
ExMemory (..),
8892
Script,
93+
ScriptContext (ScriptContext),
8994
TokenName (..),
95+
TxInInfo (TxInInfo),
96+
TxInfo (TxInfo),
9097
)
9198
import Plutus.V1.Ledger.Api qualified as Plutus
9299
import PlutusTx.Builtins (fromBuiltin)
@@ -176,6 +183,59 @@ calculateMinFee pabConf tx =
176183
, cmdOutParser = mapLeft Text.pack . parseOnly UtxoParser.feeParser . Text.pack
177184
}
178185

186+
queryTxOuts ::
187+
forall (w :: Type) (effs :: [Type -> Type]).
188+
Member (PABEffect w) effs =>
189+
[TxId] ->
190+
Eff effs (Either Text (Map TxOutRef TxOut))
191+
queryTxOuts txIds = do
192+
res <- queryChainIndex @w $ TxsFromTxIds txIds
193+
return $ case res of
194+
TxIdsResponse chainTxs -> Right $ foldMap (fmap fst . txOutRefMap) chainTxs
195+
_ -> Left "Wrong PAB response"
196+
197+
-- There is no match txOutRefs request, and we don't want a separate PAB query per input.
198+
-- So, for efficiency, we're going to query the transactions for all inputs combined,
199+
-- then pick out the outputs we care about
200+
getTxInInfos ::
201+
forall (w :: Type) (effs :: [Type -> Type]).
202+
Member (PABEffect w) effs =>
203+
[TxOutRef] ->
204+
Eff effs (Either Text [TxInInfo])
205+
getTxInInfos txOutRefs = do
206+
let ids = nub $ txOutRefId <$> txOutRefs
207+
eAllOutRefs <- queryTxOuts @w ids
208+
return $
209+
eAllOutRefs >>= \allOutRefs ->
210+
sequence $ (\ref -> toEither $ TxInInfo ref <$> Map.lookup ref allOutRefs) <$> txOutRefs
211+
where
212+
toEither :: Maybe TxInInfo -> Either Text TxInInfo
213+
toEither = maybeToRight "Couldn't find TxOut"
214+
215+
buildTxInfo ::
216+
forall (w :: Type) (effs :: [Type -> Type]).
217+
Member (PABEffect w) effs =>
218+
PABConfig ->
219+
Tx ->
220+
Eff effs (Either Text TxInfo)
221+
buildTxInfo pabConf tx = do
222+
let txOutRefs = txInRef <$> Set.toList (txInputs tx)
223+
eTxInInfos <- getTxInInfos @w txOutRefs
224+
return $
225+
(`second` eTxInInfos) $ \txInInfos ->
226+
TxInfo
227+
{ txInfoInputs = txInInfos
228+
, txInfoOutputs = txOutputs tx
229+
, txInfoFee = txFee tx
230+
, txInfoMint = txMint tx
231+
, txInfoDCert = [] -- We don't support staking or stake redeeming at this time
232+
, txInfoWdrl = []
233+
, txInfoValidRange = slotRangeToPOSIXTimeRange (pcSlotConfig pabConf) $ txValidRange tx
234+
, txInfoSignatories = Ledger.pubKeyHash <$> Map.keys (txSignatures tx)
235+
, txInfoData = Map.toList $ txData tx
236+
, txInfoId = Ledger.txId tx
237+
}
238+
179239
-- | Build a tx body and write it to disk
180240
buildTx ::
181241
forall (w :: Type) (effs :: [Type -> Type]).
@@ -184,8 +244,11 @@ buildTx ::
184244
Map PubKeyHash DummyPrivKey ->
185245
Tx ->
186246
Eff effs (Either Text ())
187-
buildTx pabConf privKeys tx =
188-
callCommand @w $ ShellArgs "cardano-cli" opts (const ())
247+
buildTx pabConf privKeys tx = do
248+
eTxInfo <- buildTxInfo @w pabConf tx
249+
case eTxInfo of
250+
Right txInfo -> callCommand @w $ ShellArgs "cardano-cli" (opts txInfo) (const ())
251+
Left e -> return $ Left e
189252
where
190253
requiredSigners =
191254
concatMap
@@ -200,13 +263,13 @@ buildTx pabConf privKeys tx =
200263
[]
201264
)
202265
(Map.keys (Ledger.txSignatures tx))
203-
opts =
266+
opts txInfo =
204267
mconcat
205268
[ ["transaction", "build-raw", "--alonzo-era"]
206-
, txInOpts pabConf (txInputs tx)
269+
, txInOpts pabConf txInfo (txInputs tx)
207270
, txInCollateralOpts (txCollateral tx)
208271
, txOutOpts pabConf (txData tx) (txOutputs tx)
209-
, mintOpts pabConf (txMintScripts tx) (txRedeemers tx) (txMint tx)
272+
, mintOpts pabConf txInfo (txMintScripts tx) (txRedeemers tx) (txMint tx)
210273
, validRangeOpts (txValidRange tx)
211274
, requiredSigners
212275
, ["--fee", showText . getLovelace . fromValue $ txFee tx]
@@ -259,19 +322,20 @@ submitTx pabConf tx =
259322
)
260323
(const ())
261324

262-
txInOpts :: PABConfig -> Set TxIn -> [Text]
263-
txInOpts pabConf =
325+
txInOpts :: PABConfig -> TxInfo -> Set TxIn -> [Text]
326+
txInOpts pabConf txInfo =
264327
concatMap
265328
( \(TxIn txOutRef txInType) ->
266329
mconcat
267330
[ ["--tx-in", txOutRefToCliArg txOutRef]
268331
, case txInType of
269332
Just (ConsumeScriptAddress validator redeemer datum) ->
270-
let exBudget =
333+
let scriptContext = ScriptContext txInfo $ Plutus.Spending txOutRef
334+
exBudget =
271335
fromRight (ExBudget (ExCPU 0) (ExMemory 0)) $
272336
calculateExBudget
273337
(Scripts.unValidatorScript validator)
274-
[Plutus.getRedeemer redeemer, Plutus.getDatum datum]
338+
[Plutus.getRedeemer redeemer, Plutus.getDatum datum, Plutus.toBuiltinData scriptContext]
275339
in mconcat
276340
[
277341
[ "--tx-in-script-file"
@@ -302,20 +366,21 @@ txInCollateralOpts =
302366
concatMap (\(TxIn txOutRef _) -> ["--tx-in-collateral", txOutRefToCliArg txOutRef]) . Set.toList
303367

304368
-- Minting options
305-
mintOpts :: PABConfig -> Set Scripts.MintingPolicy -> Redeemers -> Value -> [Text]
306-
mintOpts pabConf mintingPolicies redeemers mintValue =
369+
mintOpts :: PABConfig -> TxInfo -> Set Scripts.MintingPolicy -> Redeemers -> Value -> [Text]
370+
mintOpts pabConf txInfo mintingPolicies redeemers mintValue =
307371
mconcat
308372
[ mconcat $
309373
concatMap
310374
( \(idx, policy) ->
311375
let redeemerPtr = RedeemerPtr Mint idx
312376
redeemer = Map.lookup redeemerPtr redeemers
313377
curSymbol = Value.mpsSymbol $ Scripts.mintingPolicyHash policy
378+
scriptContext = ScriptContext txInfo $ Plutus.Minting curSymbol
314379
exBudget r =
315380
fromRight (ExBudget (ExCPU 0) (ExMemory 0)) $
316381
calculateExBudget
317382
(Scripts.unMintingPolicyScript policy)
318-
[Plutus.getRedeemer r]
383+
[Plutus.getRedeemer r, Plutus.toBuiltinData scriptContext]
319384
toOpts r =
320385
[ ["--mint-script-file", policyScriptFilePath pabConf curSymbol]
321386
, ["--mint-redeemer-file", redeemerJsonFilePath pabConf (Ledger.redeemerHash r)]
@@ -401,7 +466,7 @@ calculateExBudget :: Script -> [BuiltinData] -> Either Text ExBudget
401466
calculateExBudget script builtinData = do
402467
modelParams <- maybeToRight "Cost model params invalid." Plutus.defaultCostModelParams
403468
let serialisedScript = ShortByteString.toShort $ LazyByteString.toStrict $ Codec.serialise script
404-
let pData = map Plutus.builtinDataToData builtinData
469+
pData = map Plutus.builtinDataToData builtinData
405470
mapLeft showText $
406471
snd $
407472
Plutus.evaluateScriptCounting Plutus.Verbose modelParams serialisedScript pData

test/Spec/BotPlutusInterface/Contract.hs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ mintTokens = do
518518
--tx-out ${addr2}+1000 + 5 ${curSymbol'}.74657374546F6B656E
519519
--mint-script-file ./result-scripts/policy-${curSymbol'}.plutus
520520
--mint-redeemer-file ./result-scripts/redeemer-${redeemerHash}.json
521-
--mint-execution-units (297830,1100)
521+
--mint-execution-units (387149,1400)
522522
--mint 5 ${curSymbol'}.74657374546F6B656E
523523
--required-signer ./signing-keys/signing-key-${pkh1'}.skey
524524
--fee 0
@@ -534,7 +534,7 @@ mintTokens = do
534534
--tx-out ${addr2}+1000 + 5 ${curSymbol'}.74657374546F6B656E
535535
--mint-script-file ./result-scripts/policy-${curSymbol'}.plutus
536536
--mint-redeemer-file ./result-scripts/redeemer-${redeemerHash}.json
537-
--mint-execution-units (297830,1100)
537+
--mint-execution-units (387149,1400)
538538
--mint 5 ${curSymbol'}.74657374546F6B656E
539539
--required-signer ./signing-keys/signing-key-${pkh1'}.skey
540540
--fee 200
@@ -701,7 +701,7 @@ redeemFromValidator = do
701701
--tx-in-script-file ./result-scripts/validator-${valHash'}.plutus
702702
--tx-in-datum-file ./result-scripts/datum-${datumHash'}.json
703703
--tx-in-redeemer-file ./result-scripts/redeemer-${redeemerHash}.json
704-
--tx-in-execution-units (387149,1400)
704+
--tx-in-execution-units (476468,1700)
705705
--tx-in-collateral ${inTxId}#0
706706
--tx-out ${addr2}+500
707707
--required-signer ./signing-keys/signing-key-${pkh1'}.skey
@@ -716,7 +716,7 @@ redeemFromValidator = do
716716
--tx-in-script-file ./result-scripts/validator-${valHash'}.plutus
717717
--tx-in-datum-file ./result-scripts/datum-${datumHash'}.json
718718
--tx-in-redeemer-file ./result-scripts/redeemer-${redeemerHash}.json
719-
--tx-in-execution-units (387149,1400)
719+
--tx-in-execution-units (476468,1700)
720720
--tx-in-collateral ${inTxId}#0
721721
--tx-out ${addr1}+450
722722
--tx-out ${addr2}+500

test/Spec/MockContract.hs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ import Data.Default (Default (def))
8989
import Data.Either.Combinators (mapLeft)
9090
import Data.Hex (hex)
9191
import Data.Kind (Type)
92-
import Data.List (isPrefixOf)
92+
import Data.List (isPrefixOf, sortOn)
9393
import Data.Map (Map)
9494
import Data.Map qualified as Map
9595
import Data.Maybe (fromMaybe)
@@ -112,10 +112,12 @@ import Ledger.TxId (TxId (TxId))
112112
import Ledger.Value qualified as Value
113113
import NeatInterpolation (text)
114114
import Plutus.ChainIndex.Api (UtxosResponse (..))
115+
import Plutus.ChainIndex.Tx (ChainIndexTx (..), ChainIndexTxOutputs (ValidTx))
115116
import Plutus.ChainIndex.Types (BlockId (..), BlockNumber (unBlockNumber), Tip (..))
116117
import Plutus.Contract (Contract (Contract))
117118
import Plutus.Contract.Effects (ChainIndexQuery (..), ChainIndexResponse (..))
118119
import Plutus.PAB.Core.ContractInstance.STM (Activity (Active))
120+
import Plutus.V1.Ledger.Credential (Credential (PubKeyCredential))
119121
import PlutusTx.Builtins (fromBuiltin)
120122
import System.IO.Unsafe (unsafePerformIO)
121123
import Text.Read (readMaybe)
@@ -531,9 +533,35 @@ mockQueryChainIndex = \case
531533
UtxosResponse
532534
(state ^. tip)
533535
(pageOf pageQuery (Set.fromList (state ^. utxos ^.. traverse . _1)))
534-
TxsFromTxIds _ ->
535-
throwError @Text "TxsFromIxIds is unimplemented"
536+
TxsFromTxIds ids -> do
537+
-- TODO: Track some kind of state here, add tests to ensure this works correctly
538+
-- For now, empty txs
539+
state <- get @(MockContractState w)
540+
let knownUtxos = state ^. utxos
541+
pure . TxIdsResponse . (<$> ids) $ \txId ->
542+
ChainIndexTx
543+
{ _citxTxId = txId
544+
, _citxInputs = mempty
545+
, _citxOutputs = buildOutputsFromKnownUTxOs knownUtxos txId
546+
, _citxValidRange = Ledger.always
547+
, _citxData = mempty
548+
, _citxRedeemers = mempty
549+
, _citxScripts = mempty
550+
, _citxCardanoTx = Nothing
551+
}
536552
TxoSetAtAddress _ _ ->
537553
throwError @Text "TxoSetAtAddress is unimplemented"
538554
GetTip ->
539555
throwError @Text "GetTip is unimplemented"
556+
557+
-- | Fills in gaps of inputs with garbage TxOuts, so that the indexes we know about are in the correct positions
558+
buildOutputsFromKnownUTxOs :: [(TxOutRef, TxOut)] -> TxId -> ChainIndexTxOutputs
559+
buildOutputsFromKnownUTxOs knownUtxos txId = ValidTx $ fillGaps sortedRelatedRefs 0
560+
where
561+
sortedRelatedRefs = sortOn (Tx.txOutRefIdx . fst) $ filter ((== txId) . Tx.txOutRefId . fst) knownUtxos
562+
fillGaps :: [(TxOutRef, TxOut)] -> Integer -> [TxOut]
563+
fillGaps [] _ = []
564+
fillGaps (out@(TxOutRef _ n', txOut) : outs) n
565+
| n' == n = txOut : fillGaps outs (n + 1)
566+
| otherwise = defTxOut : fillGaps (out : outs) (n + 1)
567+
defTxOut = TxOut (Ledger.Address (PubKeyCredential "") Nothing) mempty Nothing

0 commit comments

Comments
 (0)