diff --git a/.gitignore b/.gitignore index 92ceffe9a..6940e17d8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ dist-newstyle *.swp docs/ .pre-commit-config.yaml +.github/copilot-instructions.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dd415fa7..46f7e88b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,44 @@ ### Added +- New `txSkelOptProceedAfterValidationFailures` boolean option in `TxSkelOpts` + (with its `txSkelOptProceedAfterValidationFailuresL` optic). When set to + `True`, transaction validation failures no longer abort the mockchain run. + Default is `False`. +- New `UserScriptHash` constructor for `User`, representing an allocation-mode + script owner known only by its `Api.ScriptHash` (no script body). It can be + used to pay to a bare script hash through `receives` (a new + `IsTxSkelOutAllowedOwner Api.ScriptHash` instance). Spending an output owned by + such a user requires providing the full script through a matching reference + input; otherwise a new `MCESpendingHashOnlyScript` error is raised. The + `userVScriptL` optic is now restricted to `User IsScript Redemption`, since an + allocation-mode script owner may no longer carry a script body. +- New `SomeTxSkelOutDatumHash` constructor for `TxSkelOutDatum`, representing an + output datum known only by its hash (no datum content). It is mirrored by a + new `UtxoPayloadDatumHash` constructor in the resulting `UtxoState`, and a new + `MCESpendingHashOnlyDatum` error is raised when attempting to build the + spending witness of a script output whose datum is only a hash. + ### Changed +- Time-related primitives have been regrouped into a new dedicated + `Cooked.MockChain.Effect.Time.MockChainTime` effect. The time queries + (`currentSlot`, `currentMSRange`, `getEnclosingSlot`, `slotToMSRange`, + `slotRangeBefore`, `slotRangeAfter`) that used to live in `MockChainReadChain` + and the waiting primitives (`waitNSlots`, `awaitSlot`, `awaitEnclosingSlot`, + `waitNMSFromSlotLowerBound`, `waitNMSFromSlotUpperBound`) that used to live in + `MockChainWrite` are now all provided by `MockChainTime`, with `waitNSlots` as + its sole state-modifying primitive. +- The former `MockChainState` has been split into two independent records, each + backed by its own state monad: `EmulatorState` (the emulator `Params` and + `EmulatedLedgerState`, only relevant when running against the emulated ledger) + and `ChainIndex` (the map of known outputs and the constitution script, which + is backend-agnostic and also meaningful for the node backend). Accordingly, + `mcstToUtxoState` is now `chainIndexToUtxoState`, the `mcst*L` optics are + replaced by `emulatorState*L`/`chainIndex*L`, `MockChainConf` now carries + `mccInitialEmulatorState` and `mccInitialChainIndex`, and + `RunnableMockChain.runMockChain` takes an `EmulatorState` and a `ChainIndex`. + ### Removed ### Fixed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 89b30d2a8..6e903efce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -164,6 +164,52 @@ When you are creating an enhancement request, follow these guidelines: - Document all your functions using [Haddock]'s syntax. +### Module organization (umbrella modules) + +Cohesive-but-multi-concern parts of the library are split into focused, +per-concern submodules that live in a directory, and re-exported from an +_umbrella module_ bearing the name of that directory (e.g. `Cooked.Tweak` +re-exports `Cooked.Tweak.Guard`, `Cooked.Tweak.Insert`, ...). Follow these +conventions when adding or reorganizing modules: + +- **One umbrella per directory.** When a directory groups several submodules + that form a coherent sub-system, add an umbrella module of the same name that + re-exports them all, typically as: + + ```haskell + -- | One-line description of the sub-system. + module Cooked.Foo (module X) where + + import Cooked.Foo.Bar as X + import Cooked.Foo.Baz as X + ``` + +- **Re-export instance-only submodules with `()`.** When a submodule only + provides instances (empty export list), import it in the umbrella as + `import Cooked.Foo.Bar ()` so its instances are brought in without polluting + the export list. If _all_ submodules are instance-only, the umbrella itself + has an empty export list (`module Cooked.Foo () where`). + +- **Umbrellas are aggregators first.** An umbrella may additionally host the + central type or entry point of its sub-system (as `Cooked.Skeleton` hosts + `TxSkel` and `Cooked.Automation` hosts `runAutomationPipeline`), but should + not accumulate unrelated logic. Everything else belongs in a submodule. + +- **Do not systematically nest umbrellas.** A sub-directory whose submodules are + a mere refinement of their parent sub-system may be flattened into the parent + umbrella rather than getting its own (e.g. `Cooked.Automation.GenerateTx.*` + and `Cooked.Effect.Read.*` are re-exported directly from `Cooked.Automation` + and `Cooked.Effect`). Add an intermediate umbrella only when the sub-directory + is itself an independently meaningful sub-system. + +- **Transverse utilities** (type aliases, wallets, low-level helpers) live under + `Cooked.Utilities.*` and are re-exported from `Cooked.Utilities`, keeping the + top level of `Cooked.*` for the functional pillars of the library. + +- The top-level `Cooked` module is the global umbrella: it re-exports the + per-directory umbrellas only, never individual submodules. Adding a new + sub-system therefore means adding a single import line there. + ### Nix style guide - All Nix code is formatted with [nixfmt]. diff --git a/README.md b/README.md index 975478802..6795e0318 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ handling fees or balancing. 6. Submit the transaction: ``` haskell validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOuts = [bob `receives` Value (Script.ada 10)], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } diff --git a/cooked-validators.cabal b/cooked-validators.cabal index 1a929ac8b..759b40be6 100644 --- a/cooked-validators.cabal +++ b/cooked-validators.cabal @@ -21,39 +21,44 @@ library Cooked.Attack.RedeemerTampering Cooked.Attack.TokenDuplication Cooked.Attack.ValidityTampering - Cooked.Families - Cooked.Ltl + Cooked.Automation + Cooked.Automation.AutoFilling.Constitution + Cooked.Automation.AutoFilling.MinAda + Cooked.Automation.AutoFilling.ReferenceScripts + Cooked.Automation.AutoFilling.Withdrawals + Cooked.Automation.Balancing + Cooked.Automation.GenerateTx.Anchor + Cooked.Automation.GenerateTx.Body + Cooked.Automation.GenerateTx.Certificate + Cooked.Automation.GenerateTx.Collateral + Cooked.Automation.GenerateTx.Credential + Cooked.Automation.GenerateTx.Input + Cooked.Automation.GenerateTx.Mint + Cooked.Automation.GenerateTx.Output + Cooked.Automation.GenerateTx.Proposal + Cooked.Automation.GenerateTx.ReferenceInputs + Cooked.Automation.GenerateTx.Withdrawals + Cooked.Automation.GenerateTx.Witness + Cooked.BlockChain + Cooked.BlockChain.Config + Cooked.BlockChain.Instances + Cooked.BlockChain.Run + Cooked.Effect + Cooked.Effect.Log + Cooked.Effect.Misc + Cooked.Effect.Override + Cooked.Effect.Params + Cooked.Effect.Query + Cooked.Effect.Submission + Cooked.Effect.Time + Cooked.Effect.Validation Cooked.MockChain - Cooked.MockChain.Automation.AutoFilling.Constitution - Cooked.MockChain.Automation.AutoFilling.MinAda - Cooked.MockChain.Automation.AutoFilling.ReferenceScripts - Cooked.MockChain.Automation.AutoFilling.Withdrawals - Cooked.MockChain.Automation.Balancing - Cooked.MockChain.Automation.GenerateTx.Anchor - Cooked.MockChain.Automation.GenerateTx.Body - Cooked.MockChain.Automation.GenerateTx.Certificate - Cooked.MockChain.Automation.GenerateTx.Collateral - Cooked.MockChain.Automation.GenerateTx.Credential - Cooked.MockChain.Automation.GenerateTx.Input - Cooked.MockChain.Automation.GenerateTx.Mint - Cooked.MockChain.Automation.GenerateTx.Output - Cooked.MockChain.Automation.GenerateTx.Proposal - Cooked.MockChain.Automation.GenerateTx.ReferenceInputs - Cooked.MockChain.Automation.GenerateTx.Withdrawals - Cooked.MockChain.Automation.GenerateTx.Witness - Cooked.MockChain.Common - Cooked.MockChain.Effect.Log - Cooked.MockChain.Effect.Misc - Cooked.MockChain.Effect.Read - Cooked.MockChain.Effect.Write - Cooked.MockChain.Run.Instances - Cooked.MockChain.Run.Runnable - Cooked.MockChain.Run.Tweak - Cooked.MockChain.Runtime.Error - Cooked.MockChain.Runtime.Journal - Cooked.MockChain.Runtime.State + Cooked.MockChain.Config + Cooked.MockChain.Instances + Cooked.MockChain.Ltl + Cooked.MockChain.Run Cooked.MockChain.Testing - Cooked.MockChain.UtxoSearch + Cooked.MockChain.Tweak Cooked.Pretty Cooked.Pretty.Class Cooked.Pretty.Hashable @@ -61,7 +66,10 @@ library Cooked.Pretty.Options Cooked.Pretty.Plutus Cooked.Pretty.Skeleton - Cooked.ShowBS + Cooked.Runtime + Cooked.Runtime.Error + Cooked.Runtime.Journal + Cooked.Runtime.State Cooked.Skeleton Cooked.Skeleton.Anchor Cooked.Skeleton.Certificate @@ -85,7 +93,11 @@ library Cooked.Tweak.Query Cooked.Tweak.Remove Cooked.Tweak.Update - Cooked.Wallet + Cooked.Utilities + Cooked.Utilities.Aliases + Cooked.Utilities.Families + Cooked.Utilities.ShowBS + Cooked.Utilities.Wallet other-modules: Paths_cooked_validators autogen-modules: @@ -127,15 +139,18 @@ library , bytestring , cardano-api , cardano-crypto + , cardano-ledger-alonzo , cardano-ledger-conway , cardano-ledger-core , cardano-ledger-shelley , cardano-node-emulator + , cardano-slotting , cardano-strict-containers , containers , data-default , either , exceptions + , extra , http-conduit , lens , microlens @@ -154,6 +169,8 @@ library , tasty-hunit , tasty-quickcheck , text + , time + , witherable default-language: Haskell2010 test-suite spec diff --git a/doc/BALANCING.md b/doc/BALANCING.md index a311692d8..30353e956 100644 --- a/doc/BALANCING.md +++ b/doc/BALANCING.md @@ -43,14 +43,14 @@ Our balancing function is signed as follows: ``` haskell balanceTxSkel :: - (Members '[MockChainRead, MockChainLog, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => + (Members '[Query, Log, Error ChainError, Error P.Ledger.ToCardanoError, Fail] effs) => TxSkel -> Sem effs ExtendedTxSkel ``` The library is built on [Polysemy] effects rather than a concrete monad, so the balancing capabilities are expressed as the effect constraints -`Members '[MockChainRead, MockChainLog, Error MockChainError, Error +`Members '[Query, Log, Error ChainError, Error P.Ledger.ToCardanoError, Fail] effs` and the result lives in `Sem effs`. This function takes a skeleton and returns an `ExtendedTxSkel`, a record bundling @@ -379,7 +379,7 @@ signature: ``` haskell reachValue :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + (Members '[Query, Error P.Ledger.ToCardanoError] effs) => Utxos -> -- candidate utxos, type Utxos = [(Api.TxOutRef, TxSkelOut)] Api.Value -> -- the target value to reach Integer -> -- the maximum number of utxos allowed in a subset @@ -476,7 +476,7 @@ within this interval. The function that performs this computation is ``` haskell computeFeeAndBalance :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => + (Members '[Query, Error ChainError, Error P.Ledger.ToCardanoError, Fail] effs) => Peer -> -- the balancing user Fee -> -- lower bound of the search interval Fee -> -- upper bound of the search interval diff --git a/doc/CHEATSHEET.md b/doc/CHEATSHEET.md index b1a6265cc..8abbd439a 100644 --- a/doc/CHEATSHEET.md +++ b/doc/CHEATSHEET.md @@ -124,7 +124,7 @@ myTrace = do * In a direct set of custom or builtin effects: ```haskell -myTrace :: (Members '[MockChainLog, MockChainRead, MyFirstEff, ...] effs) => Sem effs () +myTrace :: (Members '[Log, Query, MyFirstEff, ...] effs) => Sem effs () myTrace = do ... ``` @@ -466,7 +466,7 @@ options. It is built upon a transaction skeleton template. Each field can then be overridden. ```haskell -myTxSkel = txSkelTemplate +myTxSkel = txSkelEmulatorTemplate { txSkelInputs = ..., txSkelOutputs = ..., txSkelOpts = ..., @@ -496,7 +496,7 @@ Transaction can be signed with one of more wallets. They will both be part of the required and actual signers of the transaction. ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelSignatories = txSkelSignatoriesFromList [wallet 1, ...] ... @@ -513,7 +513,7 @@ myUser1 myUser2 :: MyType myUser1 = ... myUser2 = ... -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelSignatories = signatoryPubKey <$> [myUser1, myUser2, ...] ... @@ -558,7 +558,7 @@ Payments can automatically be adjusted in terms of minimal ADA requirements: Payments are given in the transaction using the `txSkelOutputs` field: ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelOutputs = [party1 `receives` payment1, party2 `receives` payment2, ...] ... @@ -590,7 +590,7 @@ myRedeemer = ## Inputs ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelInputs = Map.fromList [ (txOutRef1, someTxSkelRedeemer red), @@ -610,7 +610,7 @@ txSkelTemplate * Burn a single kind of token for a given minting policy: `burn barPolicy myTxSkelRedeemer "barName" 6` ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelMints = txSkelMintsFromList [ Mint ..., @@ -628,7 +628,7 @@ txSkelTemplate * Within redeemers manually ``withReferenceInput myTxSkelRedeemer myRefInput`` * Additional reference inputs not bound to redeemers: ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelReferenceInputs = Set.fromList [txOutRef1, txOutRef2, ...] ... @@ -642,7 +642,7 @@ also be provided manually. * From first signer (default): ``` -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelSignatories = [signatory1, signatory2], ... @@ -651,7 +651,7 @@ txSkelTemplate * From another wallet: ``` -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelSignatories = [TxSkelSignatory user1 ... , TxSkelSignatory user2 ...], txSkelOpts = def {txSkelOptCollateralUtxos = CollateralUtxosFromUser user2} @@ -661,7 +661,7 @@ txSkelTemplate * From a direct UTxO list (make sure the owner of these utxo sign the transaction): ``` -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelOpts = def {txSkelOptCollateralUtxos = CollateralUtxosFromSet (Set.fromList [txOutRef1, txOutRef2]) user2} ... @@ -695,7 +695,7 @@ do * Using the builtin constructor for proposals. ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelProposals = [ TxSkelProposal @@ -721,7 +721,7 @@ txSkelTemplate * Using smart constructors and (optional) helpers. ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelProposals = [ simpleProposal @@ -746,7 +746,7 @@ redeemer, logging an `MCLogAutoFilledConstitution` event. * Automatic withdrawal of the available rewards ```haskell -txSkelTemplate +txSkelEmulatorTemplate { txSkelWithdrawals = txSkelWithdrawalsFromList [ scriptWithdrawal myWithdrawingScript myTxSkelRedeemer, pubKeyWithdrawal myWithdrawingPubKey, @@ -759,7 +759,7 @@ txSkelTemplate * Manual withdrawal of a certain amount (for testing purposes only) ```haskell - txSkelTemplate + txSkelEmulatorTemplate { txSkelWithdrawals = txSkelWithdrawalsFromList [ Withdrawal (UserPubKey myWithdrawingPeer) (Just $ Api.Lovelace 2_000_000), ... @@ -781,7 +781,7 @@ myCertificateAction2 = DRepUpdate ... corresponds to the kind of allowed user. ```haskell -txSkelTemplate +txSkelEmulatorTemplate { txSkelCertificates = [ TxSkelCertificate myUser myCertificateAction, pubKeyCertificate myPubKey myCertificateAction1, @@ -797,7 +797,7 @@ txSkelTemplate * First signatory (default): ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelSignatories = [signatory1, signatory2] ... @@ -806,7 +806,7 @@ txSkelTemplate * Another signatory: ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelSignatories = [signatory1, signatory2], txSkelOpts = def {txSkelOptBalancingPolicy = BalanceWith (wallet 2)} @@ -817,7 +817,7 @@ txSkelTemplate ### Do not automatically balance ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelOpts = def {txSkelOptBalancingPolicy = DoNotBalance} ... diff --git a/package.yaml b/package.yaml index c55466a87..28763733d 100644 --- a/package.yaml +++ b/package.yaml @@ -12,15 +12,18 @@ library: - bytestring - cardano-api - cardano-crypto + - cardano-ledger-alonzo - cardano-ledger-core - cardano-ledger-shelley - cardano-ledger-conway - cardano-node-emulator + - cardano-slotting - cardano-strict-containers - containers - data-default - either - exceptions + - extra - http-conduit - lens - microlens @@ -39,6 +42,8 @@ library: - tasty-hunit - tasty-quickcheck - text + - time + - witherable ghc-options: -Wall -Wcompat diff --git a/src/Cooked.hs b/src/Cooked.hs index bde37fdfb..9ab486b8d 100644 --- a/src/Cooked.hs +++ b/src/Cooked.hs @@ -3,11 +3,12 @@ module Cooked (module X) where import Cooked.Attack as X -import Cooked.Families as X -import Cooked.Ltl as X +import Cooked.Automation as X +import Cooked.BlockChain as X +import Cooked.Effect as X import Cooked.MockChain as X import Cooked.Pretty as X -import Cooked.ShowBS as X +import Cooked.Runtime as X import Cooked.Skeleton as X import Cooked.Tweak as X -import Cooked.Wallet as X +import Cooked.Utilities as X diff --git a/src/Cooked/Automation.hs b/src/Cooked/Automation.hs new file mode 100644 index 000000000..a097c2b4b --- /dev/null +++ b/src/Cooked/Automation.hs @@ -0,0 +1,71 @@ +-- | This module runs the full automation pipeline that completes a +-- `Cooked.Skeleton.TxSkel` into an actual transaction. It also serves as an +-- umbrella re-exporting all the automation submodules (auto-filling, balancing +-- and transaction generation). +module Cooked.Automation + ( runAutomationPipeline, + module X, + ) +where + +import Control.Monad +import Cooked.Automation.AutoFilling.Constitution as X +import Cooked.Automation.AutoFilling.MinAda as X +import Cooked.Automation.AutoFilling.ReferenceScripts as X +import Cooked.Automation.AutoFilling.Withdrawals as X +import Cooked.Automation.Balancing as X +import Cooked.Automation.GenerateTx.Anchor as X +import Cooked.Automation.GenerateTx.Body as X +import Cooked.Automation.GenerateTx.Certificate as X +import Cooked.Automation.GenerateTx.Collateral as X +import Cooked.Automation.GenerateTx.Credential as X +import Cooked.Automation.GenerateTx.Input as X +import Cooked.Automation.GenerateTx.Mint as X +import Cooked.Automation.GenerateTx.Output as X +import Cooked.Automation.GenerateTx.Proposal as X +import Cooked.Automation.GenerateTx.ReferenceInputs as X +import Cooked.Automation.GenerateTx.Withdrawals as X +import Cooked.Automation.GenerateTx.Witness as X +import Cooked.Effect.Log +import Cooked.Effect.Params +import Cooked.Effect.Query +import Cooked.Runtime.Error +import Cooked.Skeleton +import Cooked.Tweak.Common +import Ledger.Orphans () +import Ledger.Tx qualified as P.Ledger +import Polysemy +import Polysemy.Error +import Polysemy.Fail + +-- | This runs the full automation pipeline: +-- 1. autofill min ada on eligible outputs +-- 2. autofill constution on eligible proposals +-- 3. autofill reference inputs on eligible redeemers +-- 4. autofill amount on eligible withdrawals +-- 5. balance the skeleton +-- 6. compute fees and collaterals +-- 7. generate a cardano transaction body +-- 8. fetch phase 2 failures +runAutomationPipeline :: + ( Members + '[ Error P.Ledger.ToCardanoError, + Error ChainError, + Log, + Query, + Params, + Fail + ] + effs + ) => + TxSkel -> + Sem effs ExtendedTxSkel +runAutomationPipeline = + ( `execTweak` + do + autoFillMinAda + autoFillConstitution + autoFillReferenceScripts + autoFillWithdrawalAmounts + ) + >=> balanceTxSkel diff --git a/src/Cooked/MockChain/Automation/AutoFilling/Constitution.hs b/src/Cooked/Automation/AutoFilling/Constitution.hs similarity index 65% rename from src/Cooked/MockChain/Automation/AutoFilling/Constitution.hs rename to src/Cooked/Automation/AutoFilling/Constitution.hs index 3d0e437c5..cba107a90 100644 --- a/src/Cooked/MockChain/Automation/AutoFilling/Constitution.hs +++ b/src/Cooked/Automation/AutoFilling/Constitution.hs @@ -1,14 +1,16 @@ -- | This module exposes a function to automatically fill the constitution -- scripts of the proposals in a 'Cooked.Skeleton.TxSkel' based on the current -- state of the blockchain. -module Cooked.MockChain.Automation.AutoFilling.Constitution +module Cooked.Automation.AutoFilling.Constitution ( autoFillConstitution, ) where import Control.Monad -import Cooked.MockChain.Effect.Log -import Cooked.MockChain.Effect.Read +import Control.Monad.Extra +import Cooked.Effect.Log +import Cooked.Effect.Query +import Cooked.Runtime.Journal import Cooked.Skeleton import Cooked.Tweak.Common import Cooked.Tweak.Update @@ -23,16 +25,22 @@ import Polysemy -- existing specified script in such proposals. Logs an event when the -- constitution script has been successfully auto-filled. autoFillConstitution :: - (Members '[MockChainRead, Tweak, MockChainLog] effs) => + ( Members + '[ Query, + Tweak, + Log + ] + effs + ) => Sem effs () autoFillConstitution = do - currentConstitution <- getConstitutionScript - case currentConstitution of - Nothing -> return () - Just constitutionScript -> do - traverseTweak (txSkelProposalsL % traversed) $ \prop -> do + maybeM + (return ()) + ( \constitutionScript -> traverseTweak (txSkelProposalsL % traversed) $ \prop -> do when (isn't txSkelProposalConstitutionAT prop) $ logEvent $ - MCLogAutoFilledConstitution $ + CLogAutoFilledConstitution $ Script.toScriptHash constitutionScript - return (fillConstitution constitutionScript prop) + return (fillConstitutionWhenEmpty constitutionScript prop) + ) + getConstitutionScript diff --git a/src/Cooked/MockChain/Automation/AutoFilling/MinAda.hs b/src/Cooked/Automation/AutoFilling/MinAda.hs similarity index 80% rename from src/Cooked/MockChain/Automation/AutoFilling/MinAda.hs rename to src/Cooked/Automation/AutoFilling/MinAda.hs index 2d5e2112c..48e1fbb79 100644 --- a/src/Cooked/MockChain/Automation/AutoFilling/MinAda.hs +++ b/src/Cooked/Automation/AutoFilling/MinAda.hs @@ -1,7 +1,7 @@ -- | This module exposes functions to automatically adjust the ADA contained in -- the outputs of a 'Cooked.Skeleton.TxSkel' to satisfy the minimal amount -- required by the protocol parameters. -module Cooked.MockChain.Automation.AutoFilling.MinAda +module Cooked.Automation.AutoFilling.MinAda ( getTxSkelOutMinAda, toTxSkelOutWithMinAda, autoFillMinAda, @@ -10,11 +10,12 @@ where import Cardano.Api qualified as Cardano import Cardano.Ledger.Shelley.Core qualified as Shelley -import Cardano.Node.Emulator.Internal.Node.Params qualified as Emulator import Control.Monad -import Cooked.MockChain.Automation.GenerateTx.Output -import Cooked.MockChain.Effect.Log -import Cooked.MockChain.Effect.Read +import Cooked.Automation.GenerateTx.Output +import Cooked.Effect.Log +import Cooked.Effect.Params +import Cooked.Effect.Query +import Cooked.Runtime.Journal import Cooked.Skeleton import Cooked.Tweak.Common import Cooked.Tweak.Update @@ -28,11 +29,11 @@ import Polysemy.Error -- | Compute the required minimal ADA for a given output getTxSkelOutMinAda :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + (Members '[Query, Params, Error P.Ledger.ToCardanoError] effs) => TxSkelOut -> Sem effs Integer getTxSkelOutMinAda txSkelOut = do - params <- Emulator.pEmulatorPParams <$> getParams + params <- getParams Cardano.unCoin . Shelley.getMinCoinTxOut params . Cardano.toShelleyTxOut Cardano.ShelleyBasedEraConway @@ -45,7 +46,7 @@ getTxSkelOutMinAda txSkelOut = do -- will increase the size of the UTXO which in turn might need more ADA. toTxSkelOutWithMinAda :: forall effs. - (Members '[MockChainRead, MockChainLog, Error P.Ledger.ToCardanoError] effs) => + (Members '[Query, Params, Log, Error P.Ledger.ToCardanoError] effs) => TxSkelOut -> Sem effs TxSkelOut -- The auto adjustment is disabled so nothing is done here @@ -55,7 +56,7 @@ toTxSkelOutWithMinAda txSkelOut = do txSkelOut' <- go txSkelOut let originalAda = view (txSkelOutValueL % valueLovelaceL) txSkelOut updatedAda = view (txSkelOutValueL % valueLovelaceL) txSkelOut' - when (originalAda /= updatedAda) $ logEvent $ MCLogAdjustedTxSkelOut txSkelOut updatedAda + when (originalAda /= updatedAda) $ logEvent $ CLogAdjustedTxSkelOut txSkelOut updatedAda return txSkelOut' where go :: TxSkelOut -> Sem effs TxSkelOut @@ -72,6 +73,6 @@ toTxSkelOutWithMinAda txSkelOut = do -- their ada value when requested by the user and required by the protocol -- parameters. Logs an event whenever such a change occurs. autoFillMinAda :: - (Members '[Tweak, MockChainRead, MockChainLog, Error P.Ledger.ToCardanoError] effs) => + (Members '[Tweak, Query, Params, Log, Error P.Ledger.ToCardanoError] effs) => Sem effs () autoFillMinAda = traverseTweak (txSkelOutputsL % traversed) toTxSkelOutWithMinAda diff --git a/src/Cooked/MockChain/Automation/AutoFilling/ReferenceScripts.hs b/src/Cooked/Automation/AutoFilling/ReferenceScripts.hs similarity index 81% rename from src/Cooked/MockChain/Automation/AutoFilling/ReferenceScripts.hs rename to src/Cooked/Automation/AutoFilling/ReferenceScripts.hs index 066792b4e..acb460a21 100644 --- a/src/Cooked/MockChain/Automation/AutoFilling/ReferenceScripts.hs +++ b/src/Cooked/Automation/AutoFilling/ReferenceScripts.hs @@ -1,22 +1,23 @@ -- | This module exposes functions to automatically attach reference inputs -- carrying reference scripts to the redeemers of a 'Cooked.Skeleton.TxSkel', -- based on the current state of the blockchain. -module Cooked.MockChain.Automation.AutoFilling.ReferenceScripts +module Cooked.Automation.AutoFilling.ReferenceScripts ( updateRedeemedScript, autoFillReferenceScripts, ) where import Control.Monad -import Cooked.MockChain.Effect.Log -import Cooked.MockChain.Effect.Read -import Cooked.MockChain.UtxoSearch +import Cooked.Effect.Log +import Cooked.Effect.Query +import Cooked.Runtime.Journal import Cooked.Skeleton import Cooked.Tweak.Common import Cooked.Tweak.Query import Cooked.Tweak.Update import Data.List (find) import Data.Map qualified as Map +import Data.Set qualified as Set import Optics.Core import Plutus.Script.Utils.Scripts qualified as Script import PlutusLedgerApi.V3 qualified as Api @@ -28,7 +29,7 @@ import Polysemy -- given script hash, and attaches it to a redeemer when it does not yet have a -- reference input and when it is allowed, in which case an event is logged. updateRedeemedScript :: - (Members '[MockChainLog, MockChainRead] effs) => + (Members '[Log, Query] effs) => [Api.TxOutRef] -> User IsScript Redemption -> Sem effs (User IsScript Redemption) @@ -38,21 +39,24 @@ updateRedeemedScript (toVScript -> vScript) txSkelRed@(TxSkelRedeemer {txSkelRedeemerAutoFill = True}) ) = do - oRefsInInputs <- getTxOutRefs $ allUtxosSearch $ ensureProperReferenceScript vScript + oRefsInInputs <- + allUtxos + >>= ensureAFoldIs (txSkelOutReferenceScriptHashAF % filtered (== Script.toScriptHash vScript)) + >>= retrieveTxOutRefs maybe -- We leave the redeemer unchanged if no reference input was found (return rs) -- If a reference input is found, we assign it and log the event ( \oRef -> do - logEvent $ MCLogAddedReferenceScript txSkelRed oRef (Script.toScriptHash vScript) + logEvent $ CLogAddedReferenceScript txSkelRed oRef (Script.toScriptHash vScript) return $ over userRedeemerAT (fillReferenceInput oRef) rs ) $ case oRefsInInputs of - [] -> Nothing + s | null s -> Nothing -- If possible, we use a reference input appearing in regular inputs - l | Just oRefM' <- find (`elem` inputs) l -> Just oRefM' + s | Just oRefM' <- find (`elem` inputs) s -> Just oRefM' -- If none exist, we use the first one we find elsewhere - (oRefM' : _) -> Just oRefM' + s -> Just $ Set.elemAt 0 s updateRedeemedScript _ rs = return rs -- | Goes through the various parts of the skeleton where a redeemer can appear, @@ -60,7 +64,7 @@ updateRedeemedScript _ rs = return rs -- allowed and one has not already been set. Logs an event whenever such an -- addition occurs. autoFillReferenceScripts :: - (Members '[Tweak, MockChainRead, MockChainLog] effs) => + (Members '[Tweak, Query, Log] effs) => Sem effs () autoFillReferenceScripts = do inputsKeys <- viewTweak $ txSkelInputsL % to Map.keys diff --git a/src/Cooked/MockChain/Automation/AutoFilling/Withdrawals.hs b/src/Cooked/Automation/AutoFilling/Withdrawals.hs similarity index 84% rename from src/Cooked/MockChain/Automation/AutoFilling/Withdrawals.hs rename to src/Cooked/Automation/AutoFilling/Withdrawals.hs index 12c2271fe..777091e4c 100644 --- a/src/Cooked/MockChain/Automation/AutoFilling/Withdrawals.hs +++ b/src/Cooked/Automation/AutoFilling/Withdrawals.hs @@ -1,12 +1,13 @@ -- | This module exposes a function to automatically fill the withdrawn amounts -- of a 'Cooked.Skeleton.TxSkel' based on the current state of the blockchain. -module Cooked.MockChain.Automation.AutoFilling.Withdrawals +module Cooked.Automation.AutoFilling.Withdrawals ( autoFillWithdrawalAmounts, ) where -import Cooked.MockChain.Effect.Log -import Cooked.MockChain.Effect.Read +import Cooked.Effect.Log +import Cooked.Effect.Query +import Cooked.Runtime.Journal import Cooked.Skeleton import Cooked.Tweak.Common import Cooked.Tweak.Update @@ -21,7 +22,7 @@ import Polysemy -- tamper with an existing specified amount in such withdrawals. Logs an event -- when an amount has been successfully auto-filled. autoFillWithdrawalAmounts :: - (Members '[MockChainRead, Tweak, MockChainLog] effs) => + (Members '[Query, Tweak, Log] effs) => Sem effs () autoFillWithdrawalAmounts = do traverseTweak (txSkelWithdrawalsL % txSkelWithdrawalsListI % traversed) $ \withdrawal -> do @@ -30,7 +31,7 @@ autoFillWithdrawalAmounts = do Just reward | isn't withdrawalAmountAT withdrawal -> do let newWithdrawal = fillAmount reward withdrawal logEvent $ - MCLogAutoFilledWithdrawalAmount + CLogAutoFilledWithdrawalAmount (view (withdrawalUserL % to Script.toCredential) newWithdrawal) reward return newWithdrawal diff --git a/src/Cooked/MockChain/Automation/Balancing.hs b/src/Cooked/Automation/Balancing.hs similarity index 86% rename from src/Cooked/MockChain/Automation/Balancing.hs rename to src/Cooked/Automation/Balancing.hs index 8f6502da3..b08e3b970 100644 --- a/src/Cooked/MockChain/Automation/Balancing.hs +++ b/src/Cooked/Automation/Balancing.hs @@ -1,9 +1,8 @@ -- | This module handles auto-balancing of transaction skeleton. This includes -- computation of fees and collaterals because their computation cannot be -- separated from the balancing. -module Cooked.MockChain.Automation.Balancing - ( Body, - ExtendedTxSkel (..), +module Cooked.Automation.Balancing + ( ExtendedTxSkel (..), balanceTxSkel, getMinAndMaxFee, estimateTxSkelFee, @@ -14,19 +13,19 @@ import Cardano.Api qualified as Cardano import Cardano.Api.Ledger qualified as Cardano import Cardano.Ledger.Conway.Core qualified as Conway import Cardano.Ledger.Conway.PParams qualified as Conway -import Cardano.Node.Emulator.Internal.Node.Params qualified as Emulator import Control.Monad -import Cooked.MockChain.Automation.AutoFilling.MinAda -import Cooked.MockChain.Automation.GenerateTx.Body -import Cooked.MockChain.Automation.GenerateTx.Output -import Cooked.MockChain.Common -import Cooked.MockChain.Effect.Log -import Cooked.MockChain.Effect.Read -import Cooked.MockChain.Runtime.Error -import Cooked.MockChain.UtxoSearch +import Cooked.Automation.AutoFilling.MinAda +import Cooked.Automation.GenerateTx.Body +import Cooked.Automation.GenerateTx.Output +import Cooked.Effect.Log +import Cooked.Effect.Params +import Cooked.Effect.Query +import Cooked.Runtime.Error +import Cooked.Runtime.Journal import Cooked.Skeleton +import Cooked.Utilities.Aliases import Data.ByteString qualified as BS -import Data.List (find, partition) +import Data.Foldable.Extra import Data.Map qualified as Map import Data.Maybe (fromMaybe) import Data.Ratio qualified as Rat @@ -45,10 +44,7 @@ import Polysemy import Polysemy.Error import Polysemy.Fail --- | A transaction body -type Body = Cardano.TxBody Cardano.ConwayEra - --- | A `TxSkel` with extra pieces of information produced during balancing +-- | A 'TxSkel' with extra pieces of information produced during balancing data ExtendedTxSkel = ExtendedTxSkel { -- | The skeleton itself eSkel :: TxSkel, @@ -57,7 +53,9 @@ data ExtendedTxSkel = ExtendedTxSkel -- | The optional collaterals associated with this skeleton eMCollaterals :: Maybe Collaterals, -- | The Cardano body generated from this skeleton - eBody :: Body + eBody :: Body, + -- | The script errors uncovered during body generation + eExUnitsFailures :: ExUnitsFailures } -- | This is the main entry point of our balancing mechanism. This function @@ -67,7 +65,16 @@ data ExtendedTxSkel = ExtendedTxSkel -- skeleton control whether it should be balanced, and how to compute its -- associated elements. balanceTxSkel :: - (Members '[MockChainRead, MockChainLog, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => + ( Members + '[ Query, + Params, + Log, + Error ChainError, + Error P.Ledger.ToCardanoError, + Fail + ] + effs + ) => TxSkel -> Sem effs ExtendedTxSkel balanceTxSkel skelUnbal@TxSkel {..} = do @@ -76,7 +83,7 @@ balanceTxSkel skelUnbal@TxSkel {..} = do -- with the @BalancingUtxosFromBalancingUser@ policy balancingUser <- case txSkelOptBalancingPolicy txSkelOpts of BalanceWithFirstSignatory -> case txSkelSignatories of - [] -> throw $ MCEBalancingError MissingBalancingUser + [] -> throw $ CEBalancingError MissingBalancingUser bw : _ -> return $ Just $ UserPubKey bw BalanceWith bUser -> return $ Just $ UserPubKey bUser DoNotBalance -> return Nothing @@ -96,9 +103,9 @@ balanceTxSkel skelUnbal@TxSkel {..} = do mCollaterals <- do case (nbOfScripts == 0, txSkelOptCollateralUtxos txSkelOpts) of -- No script involved, but manual collateral UTxOs provided - (True, CollateralUtxosFromSet utxos _) -> logEvent (MCLogUnusedCollaterals $ Right utxos) >> return Nothing + (True, CollateralUtxosFromSet utxos _) -> logEvent (CLogUnusedCollaterals $ Right utxos) >> return Nothing -- No script involved, but manual collateral user provided - (True, CollateralUtxosFromUser cUser) -> logEvent (MCLogUnusedCollaterals $ Left $ UserPubKey cUser) >> return Nothing + (True, CollateralUtxosFromUser cUser) -> logEvent (CLogUnusedCollaterals $ Left $ UserPubKey cUser) >> return Nothing -- No script involved, and no particular collateral option provided (True, CollateralUtxosFromBalancingUser) -> return Nothing -- Some scripts involved, and a specific set of UTxOs, alongside a @@ -107,14 +114,20 @@ balanceTxSkel skelUnbal@TxSkel {..} = do -- Some scripts involved, and a specific collateral user provided. -- We fetch vanilla UTxOs from this user and return them. (False, CollateralUtxosFromUser (Script.toPubKeyHash -> cUser)) -> - Just . (,UserPubKey cUser) . Set.fromList - <$> getTxOutRefs (utxosAtSearch cUser ensureOnlyValueOutputs) + utxosAt cUser + >>= ensureOnlyValueOutputs + >>= retrieveTxOutRefs + >>= retrieve (Just . (,UserPubKey cUser)) -- Some scripts involved, and no specific collateral options provided. (False, CollateralUtxosFromBalancingUser) -> case balancingUser of -- If no balancing wallet exists, we throw an error - Nothing -> throw $ MCEBalancingError MissingBalancingUser + Nothing -> throw $ CEBalancingError MissingBalancingUser -- If a balancing wallet exists, we use it as collateral user - Just bUser -> Just . (,bUser) . Set.fromList <$> getTxOutRefs (utxosAtSearch bUser ensureOnlyValueOutputs) + Just bUser -> + utxosAt bUser + >>= ensureOnlyValueOutputs + >>= retrieveTxOutRefs + >>= retrieve (Just . (,bUser)) -- At this point, the presence (or absence) of balancing user dictates -- whether the transaction should be automatically balanced or not. @@ -126,23 +139,24 @@ balanceTxSkel skelUnbal@TxSkel {..} = do AutoFeeComputation -> maxFee ManualFee fee' -> fee' mCols <- collateralsFromFee fee mCollaterals - cBody <- txSkelToTxBody skelUnbal fee mCols - return $ ExtendedTxSkel skelUnbal fee mCols cBody + (cBody, cExUnitsFailures) <- txSkelToTxBody skelUnbal fee mCols + return $ ExtendedTxSkel skelUnbal fee mCols cBody cExUnitsFailures Just bUser -> do -- The balancing should be performed. We collect the candidates balancing -- utxos based on the associated policy balancingUtxos <- case txSkelOptBalancingUtxos txSkelOpts of - BalancingUtxosFromBalancingUser -> getTxOutRefsAndOutputs $ utxosAtSearch bUser ensureOnlyValueOutputs + BalancingUtxosFromBalancingUser -> utxosAt bUser >>= ensureOnlyValueOutputs >>= retrieveUtxos BalancingUtxosFromSet utxos -> -- We resolve the given set of utxos - getTxOutRefsAndOutputs (txSkelOutByRefSearch' (Set.toList utxos)) + utxosFromRefs utxos + >>= retrieveUtxos -- We filter out those belonging to scripts, while throwing a -- warning if any was actually discarded. - >>= filterAndWarn (is (txSkelOutOwnerL % userPubKeyHashAT) . snd) "They belong to scripts." + >>= filterAndWarn (const $ is (txSkelOutOwnerL % userPubKeyHashAT)) "They belong to scripts." -- We filter the candidate utxos by removing those already present in the -- skeleton, throwing a warning if any was actually discarded - >>= filterAndWarn ((`notElem` txSkelKnownTxOutRefs skelUnbal) . fst) "They are already used in the skeleton." + >>= filterAndWarn (flip $ const (`notElem` txSkelKnownTxOutRefs skelUnbal)) "They are already used in the skeleton." case txSkelOptFeePolicy txSkelOpts of -- If fees are left for us to compute, we run a dichotomic search. This @@ -154,17 +168,29 @@ balanceTxSkel skelUnbal@TxSkel {..} = do ManualFee fee -> do mCols <- collateralsFromFee fee mCollaterals balancedSkel <- computeBalancedTxSkel bUser balancingUtxos skelUnbal fee - cBody <- txSkelToTxBody balancedSkel fee mCols - return $ ExtendedTxSkel balancedSkel fee mCols cBody + (cBody, cExUnitsFailures) <- txSkelToTxBody balancedSkel fee mCols + return $ ExtendedTxSkel balancedSkel fee mCols cBody cExUnitsFailures where filterAndWarn f s l - | (ok, toInteger . length -> koLength) <- partition f l = - unless (koLength == 0) (logEvent $ MCLogDiscardedUtxos koLength s) >> return ok + | (ok, toInteger . length -> koLength) <- Map.partitionWithKey f l = + unless (koLength == 0) (logEvent $ CLogDiscardedUtxos koLength s) >> return ok + ensureOnlyValueOutputs = + ensureAFoldIsn't txSkelOutReferenceScriptAT + >=> ensureAFoldIsn't txSkelOutStakingCredentialAT + >=> ensureAFoldIsn't (txSkelOutDatumL % txSkelOutDatumKindAT) -- | Computes optimal fee for a given skeleton and balances it around those fees. -- This uses a dichotomic search for an optimal "balanceable around" fee. computeFeeAndBalance :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => + ( Members + '[ Query, + Params, + Error ChainError, + Error P.Ledger.ToCardanoError, + Fail + ] + effs + ) => Peer -> Fee -> Fee -> @@ -183,14 +209,14 @@ computeFeeAndBalance balancingUser minFee maxFee balancingUtxos mCollaterals ske ( do newSkel <- computeBalancedTxSkel balancingUser balancingUtxos skel fee mCols <- collateralsFromFee fee mCollaterals - (newFee, body) <- estimateTxSkelFee newSkel fee mCols + (newFee, body, sErrors) <- estimateTxSkelFee newSkel fee mCols if -- The skeleton was balanceable, we cannot try smaller fee, but -- the used fee is sufficient for the generated body - | minFee == maxFee && newFee <= fee -> return $ ExtendedTxSkel newSkel newFee mCols body + | minFee == maxFee && newFee <= fee -> return $ ExtendedTxSkel newSkel newFee mCols body sErrors -- The skeleton was balanceable, we cannot try smaller fee, but -- the used fee is insufficient for the generated body - | minFee == maxFee -> throw $ MCEBalancingError $ NotEnoughFundForProperFee balancingUser + | minFee == maxFee -> throw $ CEBalancingError $ NotEnoughFundForProperFee balancingUser -- Current fee is insufficient, we look on the right (strictly) | newFee > fee -> computeFeeAndBalance balancingUser newFee maxFee balancingUtxos mCollaterals skel -- Current fee is sufficient, but the set of balancing utxos cannot @@ -211,7 +237,7 @@ computeFeeAndBalance balancingUser minFee maxFee balancingUtxos mCollaterals ske -- If it fails, and the remaining fee interval is not reduced to the -- current fee attempt, we can still hope for a solution by trying with -- smaller fee. - MCEBalancingError {} | fee > minFee -> computeFeeAndBalance balancingUser minFee (fee - 1) balancingUtxos mCollaterals skel + CEBalancingError {} | fee > minFee -> computeFeeAndBalance balancingUser minFee (fee - 1) balancingUtxos mCollaterals skel -- Otherwise, the whole balancing process fails and we spread the error: -- the skeleton was not balanceable. err -> throw err @@ -221,7 +247,14 @@ computeFeeAndBalance balancingUser minFee maxFee balancingUtxos mCollaterals ske -- min ada requirements in the associated return collateral and the maximum -- number of collateral inputs authorized by protocol parameters. collateralsFromFee :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + ( Members + '[ Query, + Params, + Error ChainError, + Error P.Ledger.ToCardanoError + ] + effs + ) => -- | The fee from which these collaterals should be computed Fee -> -- | The optional candidate UTxOs to be used as collaterals, alongside the @@ -233,7 +266,7 @@ collateralsFromFee :: collateralsFromFee _ Nothing = return Nothing collateralsFromFee fee (Just (collateralIns, returnCollateralUser)) = do -- We retrieve the protocol parameters - params <- Emulator.pEmulatorPParams <$> getParams + params <- getParams -- We retrieve the max number of collateral inputs, with a default of 10. In -- practice this will be around 3. let nbMax = toInteger $ Microlens.view Conway.ppMaxCollateralInputsL params @@ -244,20 +277,26 @@ collateralsFromFee fee (Just (collateralIns, returnCollateralUser)) = do -- add one because of ledger requirement which seem to round up this value. let totalCollateral = Script.lovelace . (+ 1) . (`div` 100) . (* percentage) $ fee -- Collateral tx outputs sorted by decreasing ada amount - collateralTxOuts <- getTxOutRefsAndOutputs $ txSkelOutByRefSearch' $ Set.toList collateralIns + collateralTxOuts <- utxosFromRefs collateralIns >>= retrieveUtxos -- Candidate subsets of utxos to be used as collaterals reachedValue <- reachValue collateralTxOuts totalCollateral nbMax $ Right returnCollateralUser -- A value might, or might not have been reached case reachedValue of -- If no value was reached, the input UTxOs are insufficient to provide -- the necessary collaterals, and thus an error is raised - Nothing -> throw $ MCEBalancingError $ NoSuitableCollateral fee percentage totalCollateral + Nothing -> throw $ CEBalancingError $ NoSuitableCollateral fee percentage totalCollateral -- If a value was reached, we return it alongside the return collaterals Just (oRefs, returnOutput) -> return $ Just (Set.fromList oRefs, returnOutput) reachValue :: forall effs. - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + ( Members + '[ Query, + Params, + Error P.Ledger.ToCardanoError + ] + effs + ) => -- | The Utxos available to reach the value Utxos -> -- | The target value to reach @@ -273,10 +312,10 @@ reachValue :: -- the surplus output, which is either built from scratch or from the provided -- surplus output, if any. Sem effs (Maybe ([Api.TxOutRef], Maybe TxSkelOut)) -reachValue utxos target fuel outputOrUser = do +reachValue (Map.toList -> utxos) target fuel outputOrUser = do -- We retrieve the current protocol version, which is going to be used to -- compute the size of the inputs and outputs added by this function - Cardano.ProtVer majorVersion _ <- Microlens.view Conway.ppProtocolVersionL . Emulator.emulatorPParams <$> getParams + Cardano.ProtVer majorVersion _ <- Microlens.view Conway.ppProtocolVersionL <$> getParams -- We annotate @outputOrUser@ with the size of the existing output, if any outputOrUser' <- case outputOrUser of Left output -> Left . (output,) <$> outputSize majorVersion output @@ -391,30 +430,45 @@ reachValue utxos target fuel outputOrUser = do -- | Estimates the required fee for a given skeleton with a given initial fee -- and collaterals estimateTxSkelFee :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => + ( Members + '[ Query, + Params, + Error ChainError, + Error P.Ledger.ToCardanoError, + Fail + ] + effs + ) => TxSkel -> Fee -> Maybe Collaterals -> - Sem effs (Fee, Body) + Sem effs (Fee, Body, ExUnitsFailures) estimateTxSkelFee skel fee mCollaterals = do -- We retrieve the necessary data to generate the transaction body - params <- Emulator.pEmulatorPParams <$> getParams + params <- getParams -- We build the index known to the skeleton index <- txSkelToIndex skel mCollaterals -- We build the transaction body - txBody <- txSkelToTxBody skel fee mCollaterals + (txBody, exUnitsFailures) <- txSkelToTxBody skel fee mCollaterals -- We retrieve the amount of signatories let nbOfSignatories = fromIntegral $ length $ txSkelSignatories skel -- We compute the estimated fee let Cardano.Coin newFee = Cardano.calculateMinTxFee Cardano.ShelleyBasedEraConway params index txBody nbOfSignatories -- We return both the new fee and generated body - return (newFee, txBody) + return (newFee, txBody, exUnitsFailures) -- | This creates a balanced skeleton from a given skeleton and fee. In other -- words, this ensures that the following equation holds: input value + minted -- value + withdrawn value = output value + burned value + fee + deposits computeBalancedTxSkel :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + ( Members + '[ Query, + Params, + Error ChainError, + Error P.Ledger.ToCardanoError + ] + effs + ) => Peer -> Utxos -> TxSkel -> @@ -468,10 +522,10 @@ computeBalancedTxSkel balancingUser balancingUtxos txSkel@TxSkel {..} (Script.lo (additionalInsTxOutRefs, newTxSkelOuts) <- case solution of -- There is no solution with the provided parameters Nothing -> do - let totalValue = mconcat $ view txSkelOutValueL . snd <$> balancingUtxos + let totalValue = foldOf (traversed % txSkelOutValueL) balancingUtxos difference = snd $ Api.split $ missingLeft <> PlutusTx.negate totalValue throw $ - MCEBalancingError $ + CEBalancingError $ if difference == mempty then NotEnoughFundForExtraMinAda balancingUser else NotEnoughFund balancingUser difference @@ -498,13 +552,18 @@ computeBalancedTxSkel balancingUser balancingUtxos txSkel@TxSkel {..} (Script.lo -- See https://github.com/IntersectMBO/cardano-ledger/blob/master/docs/adr/2024-08-14_009-refscripts-fee-change.md -- for more information getMinAndMaxFee :: - (Members '[MockChainRead] effs) => + ( Members + '[ Query, + Params + ] + effs + ) => Integer -> Sem effs (Fee, Fee) getMinAndMaxFee nbOfScripts = do -- We retrieve the necessary parameters to compute the maximum possible fee -- for a transaction. There are quite a few of them. - params <- Emulator.pEmulatorPParams <$> getParams + params <- getParams let maxTxSize = toInteger $ Microlens.view Conway.ppMaxTxSizeL params Cardano.Coin txFeePerByte = Microlens.view Conway.ppMinFeeAL params Cardano.Coin txFeeFixed = Microlens.view Conway.ppMinFeeBL params diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Anchor.hs b/src/Cooked/Automation/GenerateTx/Anchor.hs similarity index 94% rename from src/Cooked/MockChain/Automation/GenerateTx/Anchor.hs rename to src/Cooked/Automation/GenerateTx/Anchor.hs index c1ba9b049..9fcbbc946 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Anchor.hs +++ b/src/Cooked/Automation/GenerateTx/Anchor.hs @@ -1,5 +1,5 @@ -- | Transforming 'TxSkelAnchor' into its Cardano counterpart -module Cooked.MockChain.Automation.GenerateTx.Anchor (toCardanoAnchor) where +module Cooked.Automation.GenerateTx.Anchor (toCardanoAnchor) where import Cardano.Ledger.BaseTypes qualified as C.Ledger import Cardano.Ledger.Conway.Core qualified as Conway diff --git a/src/Cooked/Automation/GenerateTx/Body.hs b/src/Cooked/Automation/GenerateTx/Body.hs new file mode 100644 index 000000000..5e0f4ff84 --- /dev/null +++ b/src/Cooked/Automation/GenerateTx/Body.hs @@ -0,0 +1,186 @@ +-- | This modules exposes entry points to convert a 'TxSkel' into a fully +-- fledged transaction body +module Cooked.Automation.GenerateTx.Body + ( txSkelToTxBody, + txBodyContentToTxBody, + txSkelToTxBodyContent, + txSkelToIndex, + txSignatoriesAndBodyToCardanoTx, + ) +where + +import Cardano.Api qualified as Cardano +import Cardano.Ledger.Alonzo.Plutus.Evaluate qualified as Alonzo +import Control.Monad +import Cooked.Automation.GenerateTx.Certificate +import Cooked.Automation.GenerateTx.Collateral +import Cooked.Automation.GenerateTx.Input +import Cooked.Automation.GenerateTx.Mint +import Cooked.Automation.GenerateTx.Output +import Cooked.Automation.GenerateTx.Proposal +import Cooked.Automation.GenerateTx.ReferenceInputs +import Cooked.Automation.GenerateTx.Withdrawals +import Cooked.Automation.GenerateTx.Witness +import Cooked.Effect.Params +import Cooked.Effect.Query +import Cooked.Runtime.Error +import Cooked.Skeleton +import Cooked.Utilities.Aliases +import Data.Bifunctor (first) +import Data.Map qualified as Map +import Data.Set qualified as Set +import Ledger.Address qualified as P.Ledger +import Ledger.Tx.CardanoAPI qualified as P.Ledger +import Optics.Core +import Plutus.Script.Utils.Address qualified as Script +import Polysemy +import Polysemy.Error +import Polysemy.Fail +import Witherable + +-- | Generates a body content from a skeleton +txSkelToTxBodyContent :: + ( Members + '[ Query, + Params, + Error ChainError, + Error P.Ledger.ToCardanoError, + Fail + ] + effs + ) => + TxSkel -> + Fee -> + Maybe Collaterals -> + Sem effs BodyContent +txSkelToTxBodyContent skel@TxSkel {..} fee mCollaterals = do + txIns <- mapM toTxInAndWitness $ Map.toList txSkelInputs + txInsReference <- toInsReference skel + (txInsCollateral, txTotalCollateral, txReturnCollateral) <- toCollateralTriplet mCollaterals + txOuts <- mapM toCardanoTxOut txSkelOutputs + (txValidityLowerBound, txValidityUpperBound) <- fromEither $ P.Ledger.toCardanoValidityRange txSkelValidityRange + txMintValue <- toMintValue txSkelMints + txExtraKeyWits <- + if null txSkelSignatories + then return Cardano.TxExtraKeyWitnessesNone + else + Cardano.TxExtraKeyWitnesses Cardano.AlonzoEraOnwardsConway + <$> fromEither + (mapM (P.Ledger.toCardanoPaymentKeyHash . P.Ledger.PaymentPubKeyHash . Script.toPubKeyHash) txSkelSignatories) + txProtocolParams <- Cardano.BuildTxWith . Just . Cardano.LedgerProtocolParameters <$> getParams + txProposalProcedures <- Just . Cardano.Featured Cardano.ConwayEraOnwardsConway <$> toProposalProcedures txSkelProposals + txWithdrawals <- toWithdrawals txSkelWithdrawals + txCertificates <- toCertificates txSkelCertificates + let txFee = Cardano.TxFeeExplicit Cardano.ShelleyBasedEraConway $ Cardano.Coin fee + -- This is filled later on, after computing the execution units + txScriptValidity = Cardano.TxScriptValidityNone + txMetadata = Cardano.TxMetadataNone + txAuxScripts = Cardano.TxAuxScriptsNone + txUpdateProposal = Cardano.TxUpdateProposalNone + txVotingProcedures = Nothing + txCurrentTreasuryValue = Nothing + txTreasuryDonation = Nothing + return Cardano.TxBodyContent {..} + +-- | Generates a transaction body from a body content +txBodyContentToTxBody :: + (Member (Error P.Ledger.ToCardanoError) effs) => + BodyContent -> + Sem effs Body +txBodyContentToTxBody = + fromEither + . first (P.Ledger.TxBodyError . Cardano.displayError) + . Cardano.createTransactionBody Cardano.shelleyBasedEra + +-- | Generates an index with utxos known to a 'TxSkel' +txSkelToIndex :: + ( Members + '[ Query, + Params, + Error P.Ledger.ToCardanoError + ] + effs + ) => + TxSkel -> + Maybe Collaterals -> + Sem effs (Cardano.UTxO Cardano.ConwayEra) +txSkelToIndex txSkel mCollaterals = do + -- We build the index of UTxOs which are known to this skeleton. This includes + -- collateral inputs, inputs and reference inputs. + let collateralIns = maybe Set.empty fst mCollaterals + -- We retrieve all the outputs known to the skeleton + (knownTxORefs, knownTxOuts) <- + utxosFromRefs (txSkelKnownTxOutRefs txSkel <> collateralIns) + >>= retrieveUtxos + >>= retrieve (unzip . Map.toList) + -- We then compute their Cardano counterparts + txOutL <- forM knownTxOuts toCardanoTxOut + -- We build the index and handle the possible error + txInL <- fromEither $ forM knownTxORefs P.Ledger.toCardanoTxIn + -- We reshape the built index to the right format and return it + return $ Cardano.UTxO $ Map.fromList $ zip txInL $ Cardano.toCtxUTxOTxOut <$> txOutL + +-- | Generates a transaction body from a 'TxSkel' and associated fee and +-- collateral information. This transaction body accounts for the actual +-- execution units of each of the scripts involved in the skeleton. During the +-- computation of these execution units, some validation errors can occur, in +-- which case the body will not account for them, but the error maps will be +-- returned. +txSkelToTxBody :: + ( Members + '[ Query, + Params, + Error P.Ledger.ToCardanoError, + Error ChainError, + Fail + ] + effs + ) => + TxSkel -> + Fee -> + Maybe Collaterals -> + Sem effs (Body, ExUnitsFailures) +txSkelToTxBody txSkel fee mCollaterals = do + -- We create a first body content and body, without execution units + txBodyContent' <- txSkelToTxBodyContent txSkel fee mCollaterals + txBody' <- txBodyContentToTxBody txBodyContent' + -- We create a full transaction from the body + let (Cardano.ShelleyTx _ tx) = txSignatoriesAndBodyToCardanoTx (txSkelSignatories txSkel) txBody' + -- We build the index of known utxos + index <- txSkelToIndex txSkel mCollaterals + -- We retrieve the parameters + params <- getParams + -- We retrieve the @epochInfo@ from the era history + epochInfo <- Cardano.unLedgerEpochInfo . Cardano.toLedgerEpochInfo <$> getEraHistory + -- We retrieve the system start + systemStart <- getSystemStart + -- We compute the execution units associated with the transaction + let exUnitsReport = Alonzo.evalTxExUnits params tx (P.Ledger.fromPlutusIndex index) epochInfo systemStart + -- We transform the keys to Cardano script index + let cExUnitsReport = Map.mapKeysMonotonic (Cardano.toScriptIndex Cardano.AlonzoEraOnwardsConway) exUnitsReport + -- We extract the succesful cases from the map + let executionUnitsMap = mapMaybe (preview (_Right % to Cardano.fromAlonzoExUnits)) cExUnitsReport + -- We also extract the failures + let failuresMap = mapMaybe (preview _Left) cExUnitsReport + -- We attempt to insert the execution units in the body + let (txBodyContent, scriptValid) = + Cardano.substituteExecutionUnits executionUnitsMap txBodyContent' + & either + -- If this fails, this can only be a + -- @TxBodyErrorScriptWitnessIndexMissingFromExecUnitsMap@ which means that + -- some scripts failed (@failureMap@ is not empty) in which case we return + -- the original body, and mark the scripts as invalid. + (const (txBodyContent', Cardano.ScriptInvalid)) + -- We now have a body content with proper execution units and can create + -- the final body from it, while marking the scripts as valid. + (,Cardano.ScriptValid) + -- We generate the final tx body from the body content and the script validity + finalTxBody <- txBodyContentToTxBody txBodyContent {Cardano.txScriptValidity = Cardano.TxScriptValidity Cardano.AlonzoEraOnwardsConway scriptValid} + return (finalTxBody, failuresMap) + +-- | Generates a Cardano transaction and signs it +txSignatoriesAndBodyToCardanoTx :: + [TxSkelSignatory] -> + Body -> + Transaction +txSignatoriesAndBodyToCardanoTx signatories txBody = Cardano.Tx txBody $ mapMaybe (toKeyWitness txBody) signatories diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Certificate.hs b/src/Cooked/Automation/GenerateTx/Certificate.hs similarity index 85% rename from src/Cooked/MockChain/Automation/GenerateTx/Certificate.hs rename to src/Cooked/Automation/GenerateTx/Certificate.hs index 7cfb0707c..fdf00bf57 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Certificate.hs +++ b/src/Cooked/Automation/GenerateTx/Certificate.hs @@ -1,17 +1,17 @@ -- | This module provide primitives to transform certificates from our skeleton -- to certificate in Cardano transaction bodies. -module Cooked.MockChain.Automation.GenerateTx.Certificate (toCertificates) where +module Cooked.Automation.GenerateTx.Certificate (toCertificates) where import Cardano.Api qualified as Cardano import Cardano.Ledger.Conway.TxCert qualified as Conway import Cardano.Ledger.DRep qualified as C.Ledger import Cardano.Ledger.PoolParams qualified as C.Ledger import Cardano.Ledger.Shelley.TxCert qualified as Shelley -import Cardano.Node.Emulator.Internal.Node qualified as Emulator -import Cooked.MockChain.Automation.GenerateTx.Credential -import Cooked.MockChain.Automation.GenerateTx.Witness -import Cooked.MockChain.Effect.Read -import Cooked.MockChain.Runtime.Error +import Cooked.Automation.GenerateTx.Credential +import Cooked.Automation.GenerateTx.Witness +import Cooked.Effect.Params +import Cooked.Effect.Query +import Cooked.Runtime.Error import Cooked.Skeleton.Certificate import Cooked.Skeleton.User import Data.Default @@ -25,7 +25,7 @@ import Polysemy.Error import Polysemy.Fail toDRep :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + (Members '[Query, Params, Error P.Ledger.ToCardanoError] effs) => Api.DRep -> Sem effs C.Ledger.DRep toDRep Api.DRepAlwaysAbstain = return C.Ledger.DRepAlwaysAbstain @@ -33,7 +33,7 @@ toDRep Api.DRepAlwaysNoConfidence = return C.Ledger.DRepAlwaysNoConfidence toDRep (Api.DRep (Api.DRepCredential cred)) = C.Ledger.DRepCredential <$> toDRepCredential cred toDelegatee :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + (Members '[Query, Params, Error P.Ledger.ToCardanoError] effs) => Api.Delegatee -> Sem effs Conway.Delegatee toDelegatee (Api.DelegStake pkh) = Conway.DelegStake <$> toStakePoolKeyHash pkh @@ -41,7 +41,7 @@ toDelegatee (Api.DelegVote dRep) = Conway.DelegVote <$> toDRep dRep toDelegatee (Api.DelegStakeVote pkh dRep) = liftA2 Conway.DelegStakeVote (toStakePoolKeyHash pkh) (toDRep dRep) toCertificate :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + (Members '[Query, Params, Error ChainError, Error P.Ledger.ToCardanoError] effs) => TxSkelCertificate -> Sem effs (Cardano.Certificate Cardano.ConwayEra) toCertificate txSkelCert = @@ -77,11 +77,11 @@ toCertificate txSkelCert = Shelley.RetirePool (toStakePoolKeyHash poolHash) ( do - eeh <- Emulator.emulatorEraHistory <$> getParams + eeh <- getEraHistory case Cardano.slotToEpoch (fromIntegral slot) eeh of -- TODO: we could have a dedicated error for this case if the -- can occur at several places in the codebase - Left err -> throw $ MCEFailure $ "Too far away in the future: " <> show err + Left err -> throw $ CEFailure $ "Too far away in the future: " <> show err Right (epoch, _, _) -> return epoch ) TxSkelCertificate (Script.toCredential -> coldCred) (CommitteeRegisterHot hotCred) -> @@ -90,7 +90,7 @@ toCertificate txSkelCert = Conway.ConwayTxCertGov . (`Conway.ConwayResignCommitteeColdKey` SNothing) <$> toColdCredential cred toCertificateWitness :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + (Members '[Query, Params, Error ChainError, Error P.Ledger.ToCardanoError] effs) => TxSkelCertificate -> Sem effs (Maybe (Cardano.ScriptWitness Cardano.WitCtxStake Cardano.ConwayEra)) toCertificateWitness = @@ -104,7 +104,7 @@ toCertificateWitness = -- | Builds a 'Cardano.TxCertificates' from a list of 'TxSkelCertificate' toCertificates :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => + (Members '[Query, Params, Error ChainError, Error P.Ledger.ToCardanoError, Fail] effs) => [TxSkelCertificate] -> Sem effs (Cardano.TxCertificates Cardano.BuildTx Cardano.ConwayEra) toCertificates = diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Collateral.hs b/src/Cooked/Automation/GenerateTx/Collateral.hs similarity index 84% rename from src/Cooked/MockChain/Automation/GenerateTx/Collateral.hs rename to src/Cooked/Automation/GenerateTx/Collateral.hs index 98ef617e9..1d4c081aa 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Collateral.hs +++ b/src/Cooked/Automation/GenerateTx/Collateral.hs @@ -1,17 +1,17 @@ -- | This module exposes the generation of transaction collaterals, which -- consist of a collateral amount, collateral inputs and return collateral -module Cooked.MockChain.Automation.GenerateTx.Collateral +module Cooked.Automation.GenerateTx.Collateral ( toCollateralTriplet, ) where import Cardano.Api qualified as Cardano -import Cooked.MockChain.Automation.GenerateTx.Output -import Cooked.MockChain.Common -import Cooked.MockChain.Effect.Read +import Cooked.Automation.GenerateTx.Output +import Cooked.Effect.Params +import Cooked.Effect.Query import Cooked.Skeleton.Output import Cooked.Skeleton.Value -import Data.Map qualified as Map +import Cooked.Utilities.Aliases import Data.Set qualified as Set import Ledger.Tx.CardanoAPI qualified as P.Ledger import Optics.Core @@ -31,7 +31,7 @@ import Polysemy.Error -- These quantity should satisfy the equation (in terms of their values): -- collateral inputs = total collateral + return collateral toCollateralTriplet :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + (Members '[Query, Params, Error P.Ledger.ToCardanoError] effs) => Maybe Collaterals -> Sem effs @@ -47,7 +47,11 @@ toCollateralTriplet (Just (Set.toList -> collateralInsList, mReturnCollateral)) [] -> return Cardano.TxInsCollateralNone l -> fromEither $ Cardano.TxInsCollateral Cardano.AlonzoEraOnwardsConway <$> mapM P.Ledger.toCardanoTxIn l -- We collect the amount of lovelace in the collateral inputs - Api.Lovelace collateralInsLovelace <- foldOf (folded % txSkelOutValueL % valueLovelaceL) . Map.elems <$> lookupUtxos collateralInsList + Api.Lovelace collateralInsLovelace <- + utxosFromRefs collateralInsList + >>= extractAFold (txSkelOutValueL % valueLovelaceL) + >>= retrieveExtractedHeads + >>= retrieve (foldOf folded) -- We collect the amount of lovelace in the return collateral output let Api.Lovelace returnCollateralLovelace = maybe 0 (view (txSkelOutValueL % valueLovelaceL)) mReturnCollateral -- The total collateral is the difference between the two diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Credential.hs b/src/Cooked/Automation/GenerateTx/Credential.hs similarity index 98% rename from src/Cooked/MockChain/Automation/GenerateTx/Credential.hs rename to src/Cooked/Automation/GenerateTx/Credential.hs index 0621c17d6..9e04f7f7f 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Credential.hs +++ b/src/Cooked/Automation/GenerateTx/Credential.hs @@ -1,5 +1,5 @@ -- | This module exposes the generation of various kinds of credentials -module Cooked.MockChain.Automation.GenerateTx.Credential +module Cooked.Automation.GenerateTx.Credential ( toRewardAccount, toCardanoCredential, toStakeCredential, diff --git a/src/Cooked/Automation/GenerateTx/Input.hs b/src/Cooked/Automation/GenerateTx/Input.hs new file mode 100644 index 000000000..a3633d8d1 --- /dev/null +++ b/src/Cooked/Automation/GenerateTx/Input.hs @@ -0,0 +1,50 @@ +-- | This module exposes the generation of transaction inputs +module Cooked.Automation.GenerateTx.Input (toTxInAndWitness) where + +import Cardano.Api qualified as Cardano +import Cooked.Automation.GenerateTx.Witness +import Cooked.Effect.Query +import Cooked.Runtime.Error +import Cooked.Skeleton +import Ledger.Tx.CardanoAPI qualified as P.Ledger +import Optics.Core +import Plutus.Script.Utils.Scripts qualified as Script +import PlutusLedgerApi.V3 qualified as Api +import Polysemy +import Polysemy.Error + +-- | Converts a 'TxSkel' input, which consists of a 'Api.TxOutRef' and a +-- 'TxSkelRedeemer', into a 'Cardano.TxIn', together with the appropriate witness. +toTxInAndWitness :: + (Members '[Query, Error ChainError, Error P.Ledger.ToCardanoError] effs) => + (Api.TxOutRef, TxSkelRedeemer) -> + Sem + effs + ( Cardano.TxIn, + Cardano.BuildTxWith Cardano.BuildTx (Cardano.Witness Cardano.WitCtxTxIn Cardano.ConwayEra) + ) +toTxInAndWitness (txOutRef, txSkelRedeemer) = do + TxSkelOut {txSkelOutOwner, txSkelOutDatum} <- txSkelOutByRef txOutRef + let toScriptDatum = case txSkelOutDatum of + NoTxSkelOutDatum -> return $ Cardano.ScriptDatumForTxIn Nothing + SomeTxSkelOutDatum _ Inline -> return Cardano.InlineScriptDatum + SomeTxSkelOutDatum dat _ -> return $ Cardano.ScriptDatumForTxIn $ Just $ P.Ledger.toCardanoScriptData $ Api.toBuiltinData dat + SomeTxSkelOutDatumHash hash -> throw $ CESpendingHashOnlyDatum txOutRef hash + witness <- case txSkelOutOwner of + UserPubKey _ -> return $ Cardano.KeyWitness Cardano.KeyWitnessForSpending + UserScript script -> do + scriptDatum <- toScriptDatum + Cardano.ScriptWitness Cardano.ScriptWitnessForSpending <$> toScriptWitness script txSkelRedeemer scriptDatum + UserScriptHash sHash -> do + scriptDatum <- toScriptDatum + -- The full script is not available in the owner, so it must be recovered + -- from the reference script of the redeemer's reference input. + mVScript <- case txSkelRedeemerReferenceInput txSkelRedeemer of + Nothing -> return Nothing + Just refOutRef -> preview txSkelOutReferenceScriptAT <$> txSkelOutByRef refOutRef + case mVScript of + Just vScript + | Script.toScriptHash vScript == sHash -> + Cardano.ScriptWitness Cardano.ScriptWitnessForSpending <$> toScriptWitness vScript txSkelRedeemer scriptDatum + _ -> throw $ CESpendingHashOnlyScript txOutRef sHash + (,Cardano.BuildTxWith witness) <$> fromEither (P.Ledger.toCardanoTxIn txOutRef) diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Mint.hs b/src/Cooked/Automation/GenerateTx/Mint.hs similarity index 83% rename from src/Cooked/MockChain/Automation/GenerateTx/Mint.hs rename to src/Cooked/Automation/GenerateTx/Mint.hs index 16ef83498..6a9d9f141 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Mint.hs +++ b/src/Cooked/Automation/GenerateTx/Mint.hs @@ -1,11 +1,11 @@ -- | This module exposes the generation of a transaction minted value -module Cooked.MockChain.Automation.GenerateTx.Mint (toMintValue) where +module Cooked.Automation.GenerateTx.Mint (toMintValue) where import Cardano.Api qualified as Cardano import Control.Monad -import Cooked.MockChain.Automation.GenerateTx.Witness -import Cooked.MockChain.Effect.Read -import Cooked.MockChain.Runtime.Error +import Cooked.Automation.GenerateTx.Witness +import Cooked.Effect.Query +import Cooked.Runtime.Error import Cooked.Skeleton.Mint import Cooked.Skeleton.User import Data.Map qualified as Map @@ -21,7 +21,7 @@ import Polysemy.Error -- | Converts a 'TxSkelMints' into a 'Cardano.TxMintValue' toMintValue :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + (Members '[Query, Error ChainError, Error P.Ledger.ToCardanoError] effs) => TxSkelMints -> Sem effs (Cardano.TxMintValue Cardano.BuildTx Cardano.ConwayEra) toMintValue txSkelMints | txSkelMints == mempty = return Cardano.TxMintNone diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Output.hs b/src/Cooked/Automation/GenerateTx/Output.hs similarity index 82% rename from src/Cooked/MockChain/Automation/GenerateTx/Output.hs rename to src/Cooked/Automation/GenerateTx/Output.hs index 0b833e3d8..1465faf2f 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Output.hs +++ b/src/Cooked/Automation/GenerateTx/Output.hs @@ -1,9 +1,9 @@ -- | This modules exposes the generation of transaction outputs -module Cooked.MockChain.Automation.GenerateTx.Output (toCardanoTxOut) where +module Cooked.Automation.GenerateTx.Output (toCardanoTxOut) where import Cardano.Api qualified as Cardano -import Cardano.Node.Emulator.Internal.Node.Params qualified as Emulator -import Cooked.MockChain.Effect.Read +import Cooked.Effect.Params +import Cooked.Effect.Query import Cooked.Skeleton.Datum import Cooked.Skeleton.Output import Ledger.Tx.CardanoAPI qualified as P.Ledger @@ -15,7 +15,7 @@ import Polysemy.Error -- | Converts a 'TxSkelOut' to the corresponding 'Cardano.TxOut' toCardanoTxOut :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + (Members '[Query, Params, Error P.Ledger.ToCardanoError] effs) => TxSkelOut -> Sem effs (Cardano.TxOut Cardano.CtxTx Cardano.ConwayEra) toCardanoTxOut output = do @@ -23,7 +23,7 @@ toCardanoTxOut output = do oValue = view txSkelOutValueL output oDatum = view txSkelOutDatumL output oRefScript = view txSkelOutMReferenceScriptL output - networkId <- Emulator.pNetworkId <$> getParams + networkId <- getNetworkId address <- fromEither $ P.Ledger.toCardanoAddressInEra networkId oAddress (P.Ledger.toCardanoTxOutValue -> value) <- fromEither $ P.Ledger.toCardanoValue oValue datum <- case oDatum of @@ -41,4 +41,7 @@ toCardanoTxOut output = do Cardano.TxOutDatumInline Cardano.BabbageEraOnwardsConway $ P.Ledger.toCardanoScriptData $ Api.toBuiltinData datum + SomeTxSkelOutDatumHash hash -> + Cardano.TxOutDatumHash Cardano.AlonzoEraOnwardsConway + <$> fromEither (P.Ledger.toCardanoScriptDataHash hash) return $ Cardano.TxOut address value datum $ P.Ledger.toCardanoReferenceScript oRefScript diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Proposal.hs b/src/Cooked/Automation/GenerateTx/Proposal.hs similarity index 87% rename from src/Cooked/MockChain/Automation/GenerateTx/Proposal.hs rename to src/Cooked/Automation/GenerateTx/Proposal.hs index 8a92b45df..b2be4aa94 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Proposal.hs +++ b/src/Cooked/Automation/GenerateTx/Proposal.hs @@ -1,5 +1,5 @@ -- | This module exposes the generation of proposal procedures -module Cooked.MockChain.Automation.GenerateTx.Proposal (toProposalProcedures) where +module Cooked.Automation.GenerateTx.Proposal (toProposalProcedures) where import Cardano.Api qualified as Cardano import Cardano.Api.Ledger qualified as Cardano @@ -9,11 +9,12 @@ import Cardano.Ledger.Conway.Governance qualified as Conway import Cardano.Ledger.Conway.PParams qualified as Conway import Cardano.Node.Emulator.Internal.Node qualified as Emulator import Control.Monad -import Cooked.MockChain.Automation.GenerateTx.Anchor -import Cooked.MockChain.Automation.GenerateTx.Credential -import Cooked.MockChain.Automation.GenerateTx.Witness -import Cooked.MockChain.Effect.Read -import Cooked.MockChain.Runtime.Error +import Cooked.Automation.GenerateTx.Anchor +import Cooked.Automation.GenerateTx.Credential +import Cooked.Automation.GenerateTx.Witness +import Cooked.Effect.Params +import Cooked.Effect.Query +import Cooked.Runtime.Error import Cooked.Skeleton.Proposal import Cooked.Skeleton.User import Data.Coerce @@ -33,7 +34,7 @@ import Polysemy.Error -- over a Cardano parameter update toPParamsUpdate :: forall effs. - (Member (Error MockChainError) effs) => + (Member (Error ChainError) effs) => ParamChange -> Conway.PParamsUpdate Emulator.EmulatorEra -> Sem effs (Conway.PParamsUpdate Emulator.EmulatorEra) @@ -61,7 +62,7 @@ toPParamsUpdate pChange ppu = TreasuryCut q -> setL Conway.ppuTauL $ toBR q MinPoolCost n -> setL Conway.ppuMinPoolCostL $ fromIntegral n CoinsPerUTxOByte n -> setL Conway.ppuCoinsPerUTxOByteL $ Conway.CoinPerByte $ fromIntegral n - CostModels _pv1 _pv2 _pv3 -> throw $ MCEUnsupportedFeature "CostModels" + CostModels _pv1 _pv2 _pv3 -> throw $ CEUnsupportedFeature "CostModels" Prices q r -> setL Conway.ppuPricesL $ Cardano.Prices (toBR q) (toBR r) MaxTxExUnits n m -> setL Conway.ppuMaxTxExUnitsL $ Cardano.ExUnits (fromIntegral n) (fromIntegral m) MaxBlockExUnits n m -> setL Conway.ppuMaxBlockExUnitsL $ Cardano.ExUnits (fromIntegral n) (fromIntegral m) @@ -84,14 +85,14 @@ toPParamsUpdate pChange ppu = -- | Translates a given skeleton proposal into a governance action toGovAction :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + (Members '[Query, Params, Error ChainError, Error P.Ledger.ToCardanoError] effs) => GovernanceAction a -> StrictMaybe Conway.ScriptHash -> Sem effs (Conway.GovAction Emulator.EmulatorEra) toGovAction NoConfidence _ = return $ Conway.NoConfidence SNothing -toGovAction UpdateCommittee {} _ = throw $ MCEUnsupportedFeature "UpdateCommittee" -toGovAction NewConstitution {} _ = throw $ MCEUnsupportedFeature "TxGovActionNewConstitution" -toGovAction HardForkInitiation {} _ = throw $ MCEUnsupportedFeature "TxGovActionHardForkInitiation" +toGovAction UpdateCommittee {} _ = throw $ CEUnsupportedFeature "UpdateCommittee" +toGovAction NewConstitution {} _ = throw $ CEUnsupportedFeature "TxGovActionNewConstitution" +toGovAction HardForkInitiation {} _ = throw $ CEUnsupportedFeature "TxGovActionHardForkInitiation" toGovAction (ParameterChange changes) sHash = do ppu <- foldM (flip toPParamsUpdate) (Conway.PParamsUpdate Cardano.emptyPParamsStrictMaybe) changes return $ Conway.ParameterChange SNothing ppu sHash @@ -100,7 +101,7 @@ toGovAction (TreasuryWithdrawals (Map.toList -> withdrawals)) sHash = -- | Translates a list of skeleton proposals into a proposal procedures toProposalProcedures :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + (Members '[Query, Params, Error ChainError, Error P.Ledger.ToCardanoError] effs) => [TxSkelProposal] -> Sem effs (Cardano.TxProposalProcedures Cardano.BuildTx Cardano.ConwayEra) toProposalProcedures props | null props = return Cardano.TxProposalProceduresNone diff --git a/src/Cooked/MockChain/Automation/GenerateTx/ReferenceInputs.hs b/src/Cooked/Automation/GenerateTx/ReferenceInputs.hs similarity index 90% rename from src/Cooked/MockChain/Automation/GenerateTx/ReferenceInputs.hs rename to src/Cooked/Automation/GenerateTx/ReferenceInputs.hs index 825f41294..c979aab1c 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/ReferenceInputs.hs +++ b/src/Cooked/Automation/GenerateTx/ReferenceInputs.hs @@ -1,8 +1,8 @@ -- | This module allows the generation of Cardano reference inputs -module Cooked.MockChain.Automation.GenerateTx.ReferenceInputs (toInsReference) where +module Cooked.Automation.GenerateTx.ReferenceInputs (toInsReference) where import Cardano.Api qualified as Cardano -import Cooked.MockChain.Effect.Read +import Cooked.Effect.Query import Cooked.Skeleton import Data.Map qualified as Map import Data.Set qualified as Set @@ -17,7 +17,7 @@ import Polysemy.Error -- redeemers of the transaction, which can be gathered with -- 'txSkelReferenceInputsInRedeemers'. toInsReference :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + (Members '[Query, Error P.Ledger.ToCardanoError] effs) => TxSkel -> Sem effs (Cardano.TxInsReference Cardano.BuildTx Cardano.ConwayEra) toInsReference skel = do diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Withdrawals.hs b/src/Cooked/Automation/GenerateTx/Withdrawals.hs similarity index 80% rename from src/Cooked/MockChain/Automation/GenerateTx/Withdrawals.hs rename to src/Cooked/Automation/GenerateTx/Withdrawals.hs index 0727d021a..bf5ec0a37 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Withdrawals.hs +++ b/src/Cooked/Automation/GenerateTx/Withdrawals.hs @@ -1,12 +1,12 @@ -- | This modules exposes the generation of withdrawals -module Cooked.MockChain.Automation.GenerateTx.Withdrawals (toWithdrawals) where +module Cooked.Automation.GenerateTx.Withdrawals (toWithdrawals) where import Cardano.Api qualified as Cardano -import Cardano.Node.Emulator.Internal.Node.Params qualified as Emulator import Control.Monad -import Cooked.MockChain.Automation.GenerateTx.Witness -import Cooked.MockChain.Effect.Read -import Cooked.MockChain.Runtime.Error +import Cooked.Automation.GenerateTx.Witness +import Cooked.Effect.Params +import Cooked.Effect.Query +import Cooked.Runtime.Error import Cooked.Skeleton.User import Cooked.Skeleton.Withdrawal import Data.Coerce @@ -20,12 +20,12 @@ import Polysemy.Error -- | Takes a 'TxSkelWithdrawals' and transforms it into a 'Cardano.TxWithdrawals' toWithdrawals :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + (Members '[Query, Params, Error ChainError, Error P.Ledger.ToCardanoError] effs) => TxSkelWithdrawals -> Sem effs (Cardano.TxWithdrawals Cardano.BuildTx Cardano.ConwayEra) toWithdrawals withdrawals | withdrawals == mempty = return Cardano.TxWithdrawalsNone toWithdrawals (view txSkelWithdrawalsListI -> withdrawals) = do - networkId <- Emulator.pNetworkId <$> getParams + networkId <- getNetworkId cardanoWithdrawals <- forM withdrawals $ \(Withdrawal user amount) -> do let coinAmount = maybe (Cardano.Coin 0) coerce amount (sCred, witness) <- case user of diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Witness.hs b/src/Cooked/Automation/GenerateTx/Witness.hs similarity index 88% rename from src/Cooked/MockChain/Automation/GenerateTx/Witness.hs rename to src/Cooked/Automation/GenerateTx/Witness.hs index fc60ec8a3..154330357 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Witness.hs +++ b/src/Cooked/Automation/GenerateTx/Witness.hs @@ -1,13 +1,13 @@ -- | This module exposes the generation of key and script witnesses -module Cooked.MockChain.Automation.GenerateTx.Witness +module Cooked.Automation.GenerateTx.Witness ( toScriptWitness, toKeyWitness, ) where import Cardano.Api qualified as Cardano -import Cooked.MockChain.Effect.Read -import Cooked.MockChain.Runtime.Error +import Cooked.Effect.Query +import Cooked.Runtime.Error import Cooked.Skeleton import Ledger.Address qualified as P.Ledger import Ledger.Tx.CardanoAPI qualified as P.Ledger @@ -20,7 +20,7 @@ import Polysemy.Error -- | Translates a script and a reference script utxo into either a plutus script -- or a reference input containing the right script toPlutusScriptOrReferenceInput :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + (Members '[Query, Error ChainError, Error P.Ledger.ToCardanoError] effs) => VScript -> Maybe Api.TxOutRef -> Sem effs (Cardano.PlutusScriptOrReferenceInput lang) @@ -33,7 +33,7 @@ toPlutusScriptOrReferenceInput (Script.toScriptHash -> scriptHash) (Just scriptO | scriptHash == scriptHash' -> do s <- fromEither $ P.Ledger.toCardanoTxIn scriptOutRef return $ Cardano.PReferenceScript s - _ -> throw $ MCEWrongReferenceScriptError scriptOutRef scriptHash mScriptHash + _ -> throw $ CEWrongReferenceScriptError scriptOutRef scriptHash mScriptHash -- | Translates a script with its associated redeemer and datum to a script -- witness. Note on the usage of 'P.Ledger.zeroExecutionUnits': at this stage of @@ -41,7 +41,7 @@ toPlutusScriptOrReferenceInput (Script.toScriptHash -> scriptHash) (Just scriptO -- script. They will be filled out later on once the full body has been -- generated. So, for now, we temporarily leave them to 0. toScriptWitness :: - ( Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs, + ( Members '[Query, Error ChainError, Error P.Ledger.ToCardanoError] effs, ToVScript a ) => a -> diff --git a/src/Cooked/BlockChain.hs b/src/Cooked/BlockChain.hs new file mode 100644 index 000000000..572dcb709 --- /dev/null +++ b/src/Cooked/BlockChain.hs @@ -0,0 +1,8 @@ +-- | This module centralizes the node-backend (BlockChain) running code. It is +-- an umbrella re-exporting all the BlockChain submodules, which only provide +-- instances. +module Cooked.BlockChain (module X) where + +import Cooked.BlockChain.Config as X +import Cooked.BlockChain.Instances as X +import Cooked.BlockChain.Run as X diff --git a/src/Cooked/BlockChain/Config.hs b/src/Cooked/BlockChain/Config.hs new file mode 100644 index 000000000..a430491c8 --- /dev/null +++ b/src/Cooked/BlockChain/Config.hs @@ -0,0 +1,55 @@ +-- | This module exposes the configuration elements required to execute a block +-- run. This includes the initial parameters and a way of processing the results +-- of a run. +module Cooked.BlockChain.Config + ( -- * Initial blockchain configuration + BlockChainConf (..), + blockChainConfTemplate, + + -- * Blockchain return type + RawBlockChainReturn, + FunOnBlockChainResult, + displayBlockChainResult, + ) +where + +import Cardano.Api qualified as Cardano +import Cooked.Pretty +import Cooked.Runtime +import Data.Default +import Prettyprinter ((<+>)) +import Prettyprinter qualified as PP +import Prettyprinter.Render.Text qualified as PP + +-- | Raw return type of running a blockchain +type RawBlockChainReturn a = + (PrettyCookedOpts, (ChainIndex, Either ChainError a)) + +-- | The type of function handling the raw blockchain return within an IO +-- context. +type FunOnBlockChainResult a b = RawBlockChainReturn a -> IO b + +-- | A simple function handling the result of a blockchain run by displaying on +-- IO the returned value and resulting blockchain state. +displayBlockChainResult :: (Show a) => FunOnBlockChainResult a () +displayBlockChainResult (opts, (chainIndexToUtxoState -> UtxoState available consumed, res)) = do + PP.putDoc $ case res of + Left err -> "🔴 Error:" <+> prettyCookedOpt opts err + Right a -> "🟢 Success with returned value:" <+> PP.viaShow a + PP.putDoc $ "🗑️" <+> prettyCookedOpt opts consumed + PP.putDoc $ "💰" <+> prettyCookedOpt opts available + +-- | Configuration from which to run a blockchain +data BlockChainConf a b where + BlockChainConf :: + { bccConnectInfo :: Cardano.LocalNodeConnectInfo, + bccInitialChainIndex :: ChainIndex, + bccPrettyOpts :: PrettyCookedOpts, + bccFunOnResult :: RawBlockChainReturn a -> IO b + } -> + BlockChainConf a b + +-- | A basic template for a 'BlockChainConf'. It takes a connection info as a +-- parameter, and displays the result of the run directly in IO. +blockChainConfTemplate :: (Show a) => Cardano.LocalNodeConnectInfo -> BlockChainConf a () +blockChainConfTemplate connectInfo = BlockChainConf connectInfo def def displayBlockChainResult diff --git a/src/Cooked/BlockChain/Instances.hs b/src/Cooked/BlockChain/Instances.hs new file mode 100644 index 000000000..d278d4cd0 --- /dev/null +++ b/src/Cooked/BlockChain/Instances.hs @@ -0,0 +1,134 @@ +{-# OPTIONS_GHC -Wno-orphans #-} + +-- | This module exposes the concrete instances to run a blockchain against a +-- real node backend, mirroring 'Cooked.MockChain.Instances' which targets the +-- emulated chain. +module Cooked.BlockChain.Instances + ( -- * Direct, simple blockchain instance + DirectBlockChainEffs, + DirectBlockChain, + + -- * Blockchain instance with all effects + FullBlockChainEffs, + FullBlockChain, + ) +where + +import Cardano.Api qualified as Cardano +import Cooked.BlockChain.Run +import Cooked.Effect +import Cooked.Pretty.Options +import Cooked.Runtime +import Ledger qualified as P.Ledger +import Polysemy +import Polysemy.Error +import Polysemy.Fail +import Polysemy.Reader +import Polysemy.State + +-- | The most simple, straightforward stack of effects allowing to express +-- blockchain runs. This should be the preferred way of writing such runs. +type DirectBlockChainEffs = + '[ Validate, + Query, + Time, + Misc, + Fail + ] + +-- | A blockchain computation built on top of the 'DirectBlockChainEffs' stack +-- of effects. +type DirectBlockChain a = Sem DirectBlockChainEffs a + +instance RunnableBlockChain DirectBlockChainEffs where + runBlockChain pcOpts index nodeConnectInfo = + runFinal + . embedToFinal + . failToEmbed + . runState pcOpts + . runState index + . runError + . mapError CEToCardanoError + . mapError CETooFarAway + . mapError CEAcquiringFailure + . mapError CEEraMismatch + . mapError CENodeToClientVersionError + . runReader nodeConnectInfo + . runBlockChainMisc + . runBlockChainLog + . runBlockChainParams + . runBlockChainTime + . runBlockChainQuery + . runBlockChainSubmit + . runChainValidate + . insertAt @17 + @'[ Embed IO, + Final IO + ] + . insertAt @7 + @'[ Reader Cardano.LocalNodeConnectInfo, + Error Cardano.UnsupportedNtcVersionError, + Error Cardano.EraMismatch, + Error Cardano.AcquiringFailure, + Error Cardano.PastHorizonException, + Error P.Ledger.ToCardanoError, + Error ChainError, + State ChainIndex, + State PrettyCookedOpts + ] + . insertAt @4 + @'[ Params, + Log + ] + . insertAt @1 + @'[ Submit + ] + +-- | The full stack of effects required to run a blockchain, including +-- sub-effects usually invisible to the user. +type FullBlockChainEffs = + '[ Validate, + Submit, + Query, + Time, + Params, + Log, + Misc, + Reader Cardano.LocalNodeConnectInfo, + Error Cardano.UnsupportedNtcVersionError, + Error Cardano.EraMismatch, + Error Cardano.AcquiringFailure, + Error Cardano.PastHorizonException, + Error P.Ledger.ToCardanoError, + Error ChainError, + State ChainIndex, + State PrettyCookedOpts, + Fail, + Embed IO, + Final IO + ] + +-- | A blockchain computation built on top of the `FullBlockChainEffs` stack of effects +type FullBlockChain a = Sem FullBlockChainEffs a + +instance RunnableBlockChain FullBlockChainEffs where + runBlockChain pcOpts index nodeConnectInfo = + runFinal + . embedToFinal + . failToEmbed + . runState pcOpts + . runState index + . runError + . mapError CEToCardanoError + . mapError CETooFarAway + . mapError CEAcquiringFailure + . mapError CEEraMismatch + . mapError CENodeToClientVersionError + . runReader nodeConnectInfo + . runBlockChainMisc + . runBlockChainLog + . runBlockChainParams + . runBlockChainTime + . runBlockChainQuery + . runBlockChainSubmit + . runChainValidate diff --git a/src/Cooked/BlockChain/Run.hs b/src/Cooked/BlockChain/Run.hs new file mode 100644 index 000000000..ed8dc3513 --- /dev/null +++ b/src/Cooked/BlockChain/Run.hs @@ -0,0 +1,41 @@ +-- | This module exposes the infrastructure to execute blockchain runs against a +-- real node backend, mirroring 'Cooked.MockChain.Runnable' which targets the +-- emulated chain. +module Cooked.BlockChain.Run + ( -- * Running blockchains + RunnableBlockChain (..), + runBlockChainFromConf, + runBlockChainFromConfTemplate, + ) +where + +import Cardano.Api qualified as Cardano +import Cooked.BlockChain.Config +import Cooked.Pretty.Options +import Cooked.Runtime.State +import Polysemy + +-- | The class of effects that represent a blockchain run +class RunnableBlockChain effs where + -- | Runs a blockchain computation + runBlockChain :: PrettyCookedOpts -> ChainIndex -> Cardano.LocalNodeConnectInfo -> Sem effs a -> IO (RawBlockChainReturn a) + +-- | Runs a 'RunnableBlockChain' from an initial 'BlockChainConf' +runBlockChainFromConf :: + (RunnableBlockChain effs) => + BlockChainConf a b -> + Sem effs a -> + IO b +runBlockChainFromConf (BlockChainConf connectInfo chainIndex prettyOpts fun) comp = + runBlockChain prettyOpts chainIndex connectInfo comp >>= fun + +-- | Runs a 'RunnableBlockChain' from an initial default 'BlockChainConf' +runBlockChainFromConfTemplate :: + ( RunnableBlockChain effs, + Show a + ) => + Cardano.LocalNodeConnectInfo -> + Sem effs a -> + IO () +runBlockChainFromConfTemplate nodeInfo = + runBlockChainFromConf (blockChainConfTemplate nodeInfo) diff --git a/src/Cooked/Effect.hs b/src/Cooked/Effect.hs new file mode 100644 index 000000000..cc771b560 --- /dev/null +++ b/src/Cooked/Effect.hs @@ -0,0 +1,14 @@ +-- | This module centralizes the @polysemy@ effects that define the +-- capabilities of a chain (reading, writing, logging, submission, time, +-- validation and miscellaneous primitives). It is an umbrella re-exporting all +-- the effect submodules. +module Cooked.Effect (module X) where + +import Cooked.Effect.Log as X +import Cooked.Effect.Misc as X +import Cooked.Effect.Override as X +import Cooked.Effect.Params as X +import Cooked.Effect.Query as X +import Cooked.Effect.Submission as X +import Cooked.Effect.Time as X +import Cooked.Effect.Validation as X diff --git a/src/Cooked/Effect/Log.hs b/src/Cooked/Effect/Log.hs new file mode 100644 index 000000000..4365201dc --- /dev/null +++ b/src/Cooked/Effect/Log.hs @@ -0,0 +1,62 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- | This module exposes primitives required to log internal pieces of +-- information during a mockchain run. This includes, in particular, all the +-- adjustment automatically done by \cooked-validators\ during the transaction +-- processing phase. This effect is typically not available to users, and should +-- solely be used to track internal events. To trace additional elements from a +-- user's perspective, use `Cooked.Effect.Misc.note` instead. +module Cooked.Effect.Log + ( -- * Logging effect + Log, + runMockChainLog, + runBlockChainLog, + + -- * Logging primitive + logEvent, + ) +where + +import Cooked.Pretty.Class +import Cooked.Pretty.MockChain () +import Cooked.Pretty.Options +import Cooked.Pretty.Skeleton +import Cooked.Runtime.Journal +import Cooked.Runtime.State +import Polysemy +import Polysemy.State +import Polysemy.Writer + +-- | An effect to allow logging of mockchain events +data Log :: Effect where + LogEvent :: ChainLogEntry -> Log m () + +makeSem_ ''Log + +-- | Logs an internal event occurring while processing a transaction skeleton +logEvent :: (Member Log effs) => ChainLogEntry -> Sem effs () + +-- | Interpreting a `Log` in terms of a writer of +-- @[MockChainLogEntry]@ +runMockChainLog :: + (Member (Writer ChainJournal) effs) => + Sem (Log : effs) a -> + Sem effs a +runMockChainLog = interpret $ \(LogEvent event) -> tell $ fromLogEntry event + +-- | Interpreting a 'Log' by directly producing a trace on the standard output +-- for each log entry. +runBlockChainLog :: + ( Members + '[ Embed IO, + State PrettyCookedOpts, + State ChainIndex + ] + effs + ) => + Sem (Log : effs) a -> + Sem effs a +runBlockChainLog = interpret $ \(LogEvent event) -> do + opts <- get + index <- gets chainIndexOutputs + embed $ printCookedOpt opts $ Contextualized index event diff --git a/src/Cooked/Effect/Misc.hs b/src/Cooked/Effect/Misc.hs new file mode 100644 index 000000000..ea54f520a --- /dev/null +++ b/src/Cooked/Effect/Misc.hs @@ -0,0 +1,152 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- | This module defines primitives that offer quality of life features when +-- operating a mockchain without interacting with the mockchain state itself. +module Cooked.Effect.Misc + ( -- * Misc effect + Misc (..), + runMockChainMisc, + runBlockChainMisc, + + -- * Storing aliases for hashable elements + define, + defineM, + + -- * Taking notes in the notebook + note, + noteP, + noteL, + noteW, + noteS, + + -- * Asserting properties + assert, + assert', + assertP, + assertL, + assertW, + assertS, + ) +where + +import Cooked.Pretty.Class +import Cooked.Pretty.Hashable +import Cooked.Pretty.Options +import Cooked.Runtime.Journal +import Data.Map qualified as Map +import Polysemy +import Polysemy.Fail +import Polysemy.State +import Polysemy.Writer +import Prettyprinter ((<+>)) +import Prettyprinter qualified as PP +import Prettyprinter.Render.Text qualified as PP + +-- | An effect that corresponds to extra QOL capabilities of the MockChain +data Misc :: Effect where + Define :: (ToHash a) => String -> a -> Misc m a + Note :: (PrettyCookedOpts -> DocCooked) -> Misc m () + Assert :: (PrettyCookedOpts -> DocCooked) -> Bool -> Misc m () + +makeSem_ ''Misc + +-- | Stores an alias matching a hashable data for pretty printing purpose +define :: forall effs a. (Member Misc effs, ToHash a) => String -> a -> Sem effs a + +-- | Like `define`, but binds the result of a monadic computation instead +defineM :: (Member Misc effs, ToHash a) => String -> Sem effs a -> Sem effs a +defineM name = (define name =<<) + +-- | Takes note of an element represented as its rendering function to trace at +-- the end of the run +note :: forall effs. (Member Misc effs) => (PrettyCookedOpts -> DocCooked) -> Sem effs () + +-- | Takes note of a pretty-printable element to trace at the end of the run +noteP :: forall effs s. (Member Misc effs, PrettyCooked s) => s -> Sem effs () +noteP doc = note (`prettyCookedOpt` doc) + +-- | Takes note of a pretty-printable element as list with a title, to trace at +-- the end of the run +noteL :: forall effs l. (Member Misc effs, PrettyCookedList l) => String -> l -> Sem effs () +noteL title docs = note $ \opts -> prettyItemize opts (prettyCooked title) "-" docs + +-- | Takes note of a showable element to trace at the end of the run +noteW :: forall effs s. (Member Misc effs, Show s) => s -> Sem effs () +noteW = note . const . PP.viaShow + +-- | Takes note of a String to trace at the end of the run +noteS :: forall effs. (Member Misc effs) => String -> Sem effs () +noteS = noteP + +-- | Ensures a specific property holds, rendering the provided message with the +-- ambient pretty-printing options otherwise +assert :: forall effs. (Member Misc effs) => (PrettyCookedOpts -> DocCooked) -> Bool -> Sem effs () + +-- | Like `assert`, but with a pretty-printable message +assertP :: forall effs s. (Member Misc effs, PrettyCooked s) => s -> Bool -> Sem effs () +assertP doc = assert (`prettyCookedOpt` doc) + +-- | Like `assert`, but with a pretty-printable message displayed as a list with +-- a title +assertL :: forall effs l. (Member Misc effs, PrettyCookedList l) => String -> l -> Bool -> Sem effs () +assertL title docs = assert $ \opts -> prettyItemize opts (prettyCooked title) "-" docs + +-- | Like `assert`, but with a showable message +assertW :: forall effs s. (Member Misc effs, Show s) => s -> Bool -> Sem effs () +assertW = assert . const . PP.viaShow + +-- | Like `assert`, but with a `String` message +assertS :: forall effs. (Member Misc effs) => String -> Bool -> Sem effs () +assertS = assertP + +-- | Like `assert`, but with a default error message +assert' :: forall effs. (Member Misc effs) => Bool -> Sem effs () +assert' = assertS "Assertion" + +-- | Interprets a `Misc` in terms of a writer in @j@ where @j@ can be +-- built from either of the three possible parameters of the 3 misc actions. The +-- 3 actions only update the state, which is only used at the end of the run. +runMockChainMisc :: + forall effs a. + (Member (Writer ChainJournal) effs) => + Sem (Misc : effs) a -> + Sem effs a +runMockChainMisc = interpret $ \case + (Define name hashable) -> tell (fromAlias name $ toHash hashable) >> return hashable + (Note s) -> tell $ fromNote s + (Assert s b) -> tell $ fromAssert s b + +-- | Interprets a `Misc` in the context of a deployed node, running in a +-- stack featuring @IO@ (via `Embed`). Contrary to `runMockChainMisc`, which +-- gathers everything in a journal to be inspected at the end of the run, this +-- interpreter reacts immediately: +-- +-- * `Define` registers an alias in the ambient `PrettyCookedOpts`, so that every +-- subsequent rendering (later notes and assertions) benefits from it. +-- +-- * `Note` is rendered using the current `PrettyCookedOpts` and printed to the +-- standard output right away, then the run proceeds. +-- +-- * `Assert` prints the associated message. When the asserted property holds, +-- the run proceeds; otherwise the whole computation is stopped. +runBlockChainMisc :: + forall effs a. + ( Members + '[ Embed IO, + State PrettyCookedOpts, + Fail + ] + effs + ) => + Sem (Misc : effs) a -> + Sem effs a +runBlockChainMisc = interpret $ \case + Define name hashable -> do + modify $ addHashNames $ Map.singleton (toHash hashable) name + return hashable + Note s -> gets s >>= embed . PP.putDoc . (<> PP.line) + Assert s b -> do + doc <- gets s + if b + then embed $ PP.putDoc $ "✔" <+> doc <> PP.line + else fail $ renderString id $ "✘" <+> doc diff --git a/src/Cooked/Effect/Override.hs b/src/Cooked/Effect/Override.hs new file mode 100644 index 000000000..17bba1685 --- /dev/null +++ b/src/Cooked/Effect/Override.hs @@ -0,0 +1,117 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- | This module exposes primitives to manually (and artificially) update the +-- current state of the blockchain. +module Cooked.Effect.Override + ( -- * The `Override` effect + Override (..), + runMockChainOverride, + + -- * Other operations + setParams, + setConstitutionScript, + forceOutputs, + forceOutputs_, + ) +where + +import Cardano.Api qualified as Cardano +import Cardano.Api.Ledger qualified as Cardano +import Cardano.Node.Emulator.Internal.Node qualified as Emulator +import Control.Lens qualified as Lens +import Control.Monad +import Cooked.Automation.AutoFilling.MinAda +import Cooked.Automation.GenerateTx.Body +import Cooked.Automation.GenerateTx.Output +import Cooked.Effect.Log +import Cooked.Effect.Params +import Cooked.Effect.Query +import Cooked.Runtime.Error +import Cooked.Runtime.State +import Cooked.Skeleton +import Cooked.Utilities.Aliases +import Data.Map.Strict qualified as Map +import Ledger.Index qualified as P.Ledger +import Ledger.Orphans () +import Ledger.Tx qualified as P.Ledger +import Ledger.Tx.CardanoAPI qualified as P.Ledger +import Optics.Core +import Plutus.Script.Utils.Scripts qualified as Script +import Polysemy +import Polysemy.Error +import Polysemy.State + +-- | An effect that offers all the primitives that are performing modifications +-- on the blockchain state. +data Override :: Effect where + SetParams :: Emulator.Params -> Override m () + SetConstitutionScript :: (ToVScript s) => s -> Override m () + ForceOutputs :: [TxSkelOut] -> Override m Utxos + +makeSem_ ''Override + +-- | Updates the current parameters +setParams :: (Member Override effs) => Emulator.Params -> Sem effs () + +-- | Sets the current script to act as the official constitution script +setConstitutionScript :: (Member Override effs, ToVScript s) => s -> Sem effs () + +-- | Forces the generation of utxos corresponding to certain +-- `TxSkelOut`. Returns the created UTxOs, which might differ from the original +-- list if some min ADA adjustment occurred. +forceOutputs :: (Member Override effs) => [TxSkelOut] -> Sem effs Utxos + +-- | Same as `forceOutputs`, but discards the returned outputs +forceOutputs_ :: (Member Override effs) => [TxSkelOut] -> Sem effs () +forceOutputs_ = void . forceOutputs + +-- | Interprets the `Override` effect +runMockChainOverride :: + forall effs a. + ( Members + '[ State EmulatorState, + State ChainIndex, + Error P.Ledger.ToCardanoError, + Error ChainError, + Log, + Query, + Params + ] + effs + ) => + Sem (Override : effs) a -> + Sem effs a +runMockChainOverride = interpret $ \case + SetParams params -> do + modify $ set emulatorStateParamsL params + modify $ over emulatorStateLedgerStateL $ Emulator.updateStateParams params + SetConstitutionScript (toVScript -> cScript) -> do + modify' $ chainIndexConstitutionL ?~ cScript + modify' $ + over emulatorStateLedgerStateL $ + Lens.set + Emulator.elsConstitutionScriptL + (Cardano.SJust $ Cardano.toShelleyScriptHash $ Script.toCardanoScriptHash cScript) + ForceOutputs outputs -> do + -- We adjust the outputs for the minimal required ADA if needed + outputsMinAda <- mapM toTxSkelOutWithMinAda outputs + -- We transform these outputs to Cardano outputs + outputs' <- mapM toCardanoTxOut outputsMinAda + -- We create our transaction body, composed of the forced outputs + cardanoTx <- + P.Ledger.CardanoEmulatorEraTx . (`Cardano.Tx` []) + <$> txBodyContentToTxBody (P.Ledger.emptyTxBodyContent {Cardano.txOuts = outputs'}) + -- We need to adjust our internal state to account for the forced + -- transaction. We begin by computing the new outputs. + let outputsList = zip (P.Ledger.fromCardanoTxIn . snd <$> P.Ledger.getCardanoTxOutRefs cardanoTx) outputsMinAda + -- We update the index, which effectively receives the new utxos + modify' $ + over emulatorStateLedgerStateL $ + Lens.over Emulator.elsUtxoL $ + P.Ledger.fromPlutusIndex + . P.Ledger.insert cardanoTx + . P.Ledger.toPlutusIndex + -- We update our internal map by adding the new outputs + modify' $ addOutputs outputsList + -- Embedly, we return the created utxos + return $ Map.fromList outputsList diff --git a/src/Cooked/Effect/Params.hs b/src/Cooked/Effect/Params.hs new file mode 100644 index 000000000..5615b0212 --- /dev/null +++ b/src/Cooked/Effect/Params.hs @@ -0,0 +1,212 @@ +-- | This module exposes internal, configuration-level primitives to query the +-- fixed configuration of the chain, such as its protocol parameters, network +-- id, era history and system start. These primitives are not meant to be used +-- directly when writing traces: they are an implementation detail backing the +-- user-facing 'Cooked.Effect.Query.Query' effect, +-- and they are deliberately not meant to be used directly through the "Cooked" +-- umbrella module. +module Cooked.Effect.Params + ( -- * The 'Params' effect + Params, + + -- * 'Params' interpreters + runMockChainParams, + runBlockChainParams, + + -- * Queries related to protocol parameters + getParams, + getNetworkId, + govActionDeposit, + dRepDeposit, + stakeAddressDeposit, + stakePoolDeposit, + + -- * Queries related to time configuration + getEraHistory, + getSystemStart, + + -- * Queries related to `Cooked.Skeleton.TxSkel` deposits + txSkelDepositedValueInCertificates, + txSkelDepositedValueInProposals, + ) +where + +import Cardano.Api qualified as Cardano +import Cardano.Ledger.Conway qualified as Conway +import Cardano.Ledger.Conway.Core qualified as Conway +import Cardano.Ledger.Core qualified as C.Ledger +import Cardano.Ledger.Shelley.API qualified as Shelley +import Cardano.Node.Emulator.Internal.Node qualified as Emulator +import Cardano.Slotting.Time qualified as Time +import Control.Lens qualified as Lens +import Cooked.Runtime.State +import Cooked.Skeleton +import Data.Functor +import Optics.Core +import PlutusLedgerApi.V3 qualified as Api +import Polysemy +import Polysemy.Error +import Polysemy.Reader +import Polysemy.State + +-- | An effect that offers primitives to query the fixed configuration of the +-- chain (protocol parameters, network id, era history and system start). As its +-- name suggests, this effect is read-only and does not alter the state in any +-- way. It is internal to the library and backs the user-facing +-- 'Cooked.Effect.Query.Query' effect. +data Params :: Effect where + GetParams :: Params m (C.Ledger.PParams Conway.ConwayEra) + GetNetworkId :: Params m Cardano.NetworkId + GetEraHistory :: Params m Cardano.EraHistory + GetSystemStart :: Params m Time.SystemStart + +makeSem_ ''Params + +-- | Returns the emulator parameters, including protocol parameters +getParams :: + (Member Params effs) => + Sem effs (C.Ledger.PParams Conway.ConwayEra) + +-- | Returns the network id of the current chain +getNetworkId :: + (Member Params effs) => + Sem effs Cardano.NetworkId + +-- | Returns the era history of the chain, which notably allows converting slots +-- into epochs (see 'Cardano.slotToEpoch'). +getEraHistory :: + (Member Params effs) => + Sem effs Cardano.EraHistory + +-- | Returns the system start time of the chain, that is the UTC time at which +-- the first slot begins. +getSystemStart :: + (Member Params effs) => + Sem effs Time.SystemStart + +-- | Retrieves the required governance action deposit amount +govActionDeposit :: + (Member Params effs) => + Sem effs Api.Lovelace +govActionDeposit = + getParams + <&> Api.Lovelace + . Cardano.unCoin + . Lens.view Conway.ppGovActionDepositL + +-- | Retrieves the required drep deposit amount +dRepDeposit :: + (Member Params effs) => + Sem effs Api.Lovelace +dRepDeposit = + getParams + <&> Api.Lovelace + . Cardano.unCoin + . Lens.view Conway.ppDRepDepositL + +-- | Retrieves the required stake address deposit amount +stakeAddressDeposit :: + (Member Params effs) => + Sem effs Api.Lovelace +stakeAddressDeposit = + getParams + <&> Api.Lovelace + . Cardano.unCoin + . Lens.view Conway.ppKeyDepositL + +-- | Retrieves the required stake pool deposit amount +stakePoolDeposit :: + (Member Params effs) => + Sem effs Api.Lovelace +stakePoolDeposit = + getParams + <&> Api.Lovelace + . Cardano.unCoin + . Lens.view Conway.ppPoolDepositL + +-- | Retrieves the total amount of lovelace deposited in certificates in this +-- skeleton. Note that unregistering a staking address or a dRep lead to a +-- negative deposit (a withdrawal, in fact) which means this function can return +-- a negative amount of lovelace, which is intended. The deposited amounts are +-- dictated by the current protocol parameters, and computed as such. +txSkelDepositedValueInCertificates :: + (Member Params effs) => + TxSkel -> + Sem effs Api.Lovelace +txSkelDepositedValueInCertificates txSkel = do + sDep <- stakeAddressDeposit + dDep <- dRepDeposit + pDep <- stakePoolDeposit + return $ + foldOf + ( txSkelCertificatesL + % traversed + % to + ( \case + TxSkelCertificate _ StakingRegister {} -> sDep + TxSkelCertificate _ StakingRegisterDelegate {} -> sDep + TxSkelCertificate _ StakingUnRegister {} -> -sDep + TxSkelCertificate _ DRepRegister {} -> dDep + TxSkelCertificate _ DRepUnRegister {} -> -dDep + TxSkelCertificate _ PoolRegister {} -> pDep + -- There is no special case for 'PoolRetire' because the deposit + -- is given back to the reward account. + _ -> Api.Lovelace 0 + ) + ) + txSkel + +-- | Retrieves the total amount of lovelace deposited in proposals in this +-- skeleton (equal to `govActionDeposit` times the number of proposals) +txSkelDepositedValueInProposals :: + (Member Params effs) => + TxSkel -> + Sem effs Api.Lovelace +txSkelDepositedValueInProposals TxSkel {txSkelProposals} = + govActionDeposit + <&> Api.Lovelace + . (toInteger (length txSkelProposals) *) + . Api.getLovelace + +-- | The interpretation for the configuration effect with a stored +-- 'EmulatorState' +runMockChainParams :: + (Member (State EmulatorState) effs) => + Sem (Params : effs) a -> + Sem effs a +runMockChainParams = interpret $ \case + GetParams -> gets $ Emulator.pEmulatorPParams . emulatorStateParams + GetNetworkId -> gets $ Emulator.pNetworkId . emulatorStateParams + GetEraHistory -> gets $ Emulator.emulatorEraHistory . emulatorStateParams + GetSystemStart -> gets $ Shelley.systemStart . Emulator.emulatorGlobals . emulatorStateParams + +-- | Interpret the `Params` effect by talking to a deployed node +-- through a `Cardano.LocalNodeConnectInfo` (socket path and network id) provided +-- via a `Reader`, running in a stack featuring @IO@ (via `Embed`). +runBlockChainParams :: + ( Members + '[ Embed IO, + Error Cardano.UnsupportedNtcVersionError, + Error Cardano.EraMismatch, + Error Cardano.AcquiringFailure, + Reader Cardano.LocalNodeConnectInfo + ] + effs + ) => + Sem (Params : effs) a -> + Sem effs a +runBlockChainParams = interpret $ \case + GetParams -> queryAndHandleErrors $ Cardano.queryProtocolParameters Cardano.ShelleyBasedEraConway + GetNetworkId -> asks Cardano.localNodeNetworkId + GetEraHistory -> queryAndHandleError Cardano.queryEraHistory + GetSystemStart -> queryAndHandleError Cardano.querySystemStart + where + -- Fetches the local node info, embeds a query in IO and handles errors + query q = do + conn <- ask + response <- embed $ Cardano.executeLocalStateQueryExpr conn Cardano.VolatileTip q + fromEither response + -- Handles one more layer of errors from the response of a query + queryAndHandleError q = query q >>= fromEither + -- Handles a second layer of error from the response of a query + queryAndHandleErrors q = queryAndHandleError q >>= fromEither diff --git a/src/Cooked/Effect/Query.hs b/src/Cooked/Effect/Query.hs new file mode 100644 index 000000000..d8c9ed783 --- /dev/null +++ b/src/Cooked/Effect/Query.hs @@ -0,0 +1,523 @@ +-- | This module exposes the user-facing primitives to query the current state +-- of the blockchain, such as the available UTxOs, and the current constitution +-- or rewards. It also provides the 'UtxoSearch' framework, a convenient way to +-- look through UTxOs, filter them, and extract pieces of information from them. +-- Time-related queries live in the separate +-- 'Cooked.Effect.Time.Time' effect. The lower-level +-- configuration primitives (protocol parameters, network id, era history, system +-- start) live in the internal +-- 'Cooked.Effect.Params.Params' effect, which this +-- effect relies on during its own interpretation. +module Cooked.Effect.Query + ( -- * Utxo searches types + RefinedOutputsList, + UtxoSearchResult, + utxosSearchResultUtxosI, + + -- * Retrieving pieces of @UtxoSearchResult@ + retrieve, + retrieveUtxos, + retrieveRefinedOutputs, + retrieveExtracts, + retrieveTxOutRefs, + retrieveExtractedHeads, + + -- * Extracting new information from UTxOs + extract, + extractPure, + extractAFold, + extractTotal, + extractPureTotal, + extractGetter, + + -- * Filtering some UTxOs out + ensure, + ensurePure, + ensureAFoldIs, + ensureAFoldIsn't, + + -- * The 'Query' effect and interpreters + Query, + runMockChainQuery, + runBlockChainQuery, + + -- * Queries related to `Cooked.Skeleton.TxSkel` + txSkelAllScripts, + txSkelInputScripts, + txSkelInputValue, + + -- * Queries related to fetching UTxOs + allUtxos, + utxosAt, + txSkelOutByRef, + utxosFromCardanoTx, + utxosFromRefs, + previewByRef, + viewByRef, + + -- * Query fetching the current reward amount + getCurrentReward, + + -- * Query fetching the current full constitution script + getConstitutionScript, + ) +where + +import Cardano.Api qualified as Cardano +import Cardano.Api.Ledger qualified as Cardano hiding (TxIn) +import Cardano.Node.Emulator.Internal.Node qualified as Emulator +import Control.Monad +import Cooked.Automation.GenerateTx.Credential +import Cooked.Effect.Params +import Cooked.Runtime.Error +import Cooked.Runtime.State +import Cooked.Skeleton +import Cooked.Utilities.Aliases +import Cooked.Utilities.Families hiding (Member) +import Data.Coerce (coerce) +import Data.Map (Map) +import Data.Map qualified as Map +import Data.Map.Optics (toMapOf) +import Data.Maybe +import Data.Maybe.Strict +import Data.Set (Set) +import Data.Set qualified as Set +import Ledger.Address qualified as P.Ledger +import Ledger.Tx qualified as P.Ledger +import Ledger.Tx.CardanoAPI qualified as P.Ledger +import Optics.Core +import Optics.Core.Extras +import Plutus.Script.Utils.Address qualified as Script +import Plutus.Script.Utils.Scripts qualified as Script +import PlutusLedgerApi.V3 qualified as Api +import Polysemy +import Polysemy.Error +import Polysemy.Reader +import Polysemy.State +import Witherable (filterA, witherM) + +-- | An heterogeneous list starting with a 'TxSkelOut' +type RefinedOutputsList els = HList (TxSkelOut ': els) + +-- | Raw result of a `UtxoSearch`. We store the `Api.TxOutRef` of the output, +-- alongside an heterogeneous list starting with the output in question, +-- followed by any element that was extracted during the search. +type UtxoSearchResult els = Map Api.TxOutRef (RefinedOutputsList els) + +-- | An isomorphisms between `Utxos` and search results with no extra element. +utxosSearchResultUtxosI :: Iso' (UtxoSearchResult '[]) Utxos +utxosSearchResultUtxosI = iso (fmap hHead) (fmap hSingleton) + +-- | A `UtxoSearch` is a computation that returns a list of UTxOs alongside +-- their `TxSkelOut` counterpart and a list of other elements retrieved from the +-- output. The idea is to begin with a simple search and refine the search with +-- filters while appending new elements to the list. +type UtxoSearch effs els = Sem effs (UtxoSearchResult els) + +-- | Retrieves part of a 'UtxoSearchResult'. We define it on a more general +-- type, to allow for extracting values from basically anything, thus avoiding +-- annoying fmaps prepending sequences of utxo search operators bound with @>>=@ +retrieve :: + (els -> a) -> + (els -> Sem effs a) +retrieve f = return . f + +-- | Retrieves the `TxSkelOut`s from a `UtxoSearchResult` +retrieveUtxos :: + UtxoSearchResult els -> + Sem effs Utxos +retrieveUtxos = retrieve $ fmap hHead + +-- | Retrieves the `TxSkelOut`s from a `UtxoSearchResult` alongside the +-- extracted elements +retrieveRefinedOutputs :: + UtxoSearchResult els -> + Sem effs [RefinedOutputsList els] +retrieveRefinedOutputs = retrieve Map.elems + +-- | Retrieves the extracted elements from a `UtxoSearchResult` +retrieveExtracts :: + UtxoSearchResult els -> + Sem effs [HList els] +retrieveExtracts = retrieve $ Map.elems . fmap hTail + +-- | Retrieves the `Api.TxOutRef`s from a `UtxoSearchResult` +retrieveTxOutRefs :: + UtxoSearchResult els -> + Sem effs (Set Api.TxOutRef) +retrieveTxOutRefs = retrieve Map.keysSet + +-- | Retrieves the first extracted elements from a 'UtxoSearchResult' +retrieveExtractedHeads :: + UtxoSearchResult (a ': els) -> + Sem effs [a] +retrieveExtractedHeads = retrieve $ Map.elems . fmap (hHead . hTail) + +-- | Extracts a new element from the currently selected outputs, filtering out +-- in the process utxos for which this element is not available +extract :: + (TxSkelOut -> Sem effs (Maybe b)) -> + UtxoSearchResult els -> + UtxoSearch effs (b ': els) +extract extractFun = + witherM + ( \(HCons txSkelOut es) -> + fmap (HCons txSkelOut . (`HCons` es)) <$> extractFun txSkelOut + ) + +-- | Same as `extract`, but with a pure extraction function +extractPure :: + (TxSkelOut -> Maybe b) -> + UtxoSearchResult els -> + UtxoSearch effs (b ': els) +extractPure = extract . (return .) + +-- | Same as `extractPure`, using an affine fold to extract the element +extractAFold :: + (Is k An_AffineFold) => + Optic' k is TxSkelOut b -> + UtxoSearchResult els -> + UtxoSearch effs (b ': els) +extractAFold = extractPure . preview + +-- | Same as `extract`, but with a total extraction function +extractTotal :: + (TxSkelOut -> Sem effs b) -> + UtxoSearchResult els -> + UtxoSearch effs (b ': els) +extractTotal = extract . (fmap Just .) + +-- | Same as `extract`, but with a pure and total extraction function +extractPureTotal :: + (TxSkelOut -> b) -> + UtxoSearchResult els -> + UtxoSearch effs (b ': els) +extractPureTotal = extractTotal . (return .) + +-- | Same as `extractPureTotal`, using a getter to extract the element +extractGetter :: + (Is k A_Getter) => + Optic' k is TxSkelOut b -> + UtxoSearchResult els -> + UtxoSearch effs (b ': els) +extractGetter = extractPureTotal . view + +-- | Ensures the outputs resulting from the search satisfy the given predicate +ensure :: + (TxSkelOut -> Sem effs Bool) -> + UtxoSearchResult els -> + UtxoSearch effs els +ensure filterF = filterA (filterF . hHead) + +-- | Same as `ensure`, but with a pure predicate +ensurePure :: + (TxSkelOut -> Bool) -> + UtxoSearchResult els -> + UtxoSearch effs els +ensurePure = ensure . (return .) + +-- | Ensures the outputs resulting from the search contain the focus of the +-- given affine fold +ensureAFoldIs :: + (Is k An_AffineFold) => + Optic' k is TxSkelOut b -> + UtxoSearchResult els -> + UtxoSearch effs els +ensureAFoldIs = ensurePure . is + +-- | Ensures the outputs resulting from the search do not contain the focus of +-- the given affine fold +ensureAFoldIsn't :: + (Is k An_AffineFold) => + Optic' k is TxSkelOut b -> + UtxoSearchResult els -> + UtxoSearch effs els +ensureAFoldIsn't = ensurePure . isn't + +-- | An effect that offers primitives to query the current state of the +-- mockchain. As its name suggests, this effect is read-only and does not alter +-- the state in any way. This is the user-facing read effect; its interpreters +-- rely on the internal +-- 'Cooked.Effect.Params.Params' effect to resolve the +-- fixed chain configuration. +data Query :: Effect where + TxSkelOutByRef :: Api.TxOutRef -> Query m TxSkelOut + AllUtxos :: Query m (UtxoSearchResult '[]) + UtxosAt :: (Script.ToAddress a) => a -> Query m (UtxoSearchResult '[]) + GetConstitutionScript :: Query m (Maybe VScript) + GetCurrentReward :: (Script.ToCredential c) => c -> Query m (Maybe Api.Lovelace) + +makeSem_ ''Query + +-- | Returns all scripts involved in this 'TxSkel' +txSkelAllScripts :: + (Member Query effs) => + TxSkel -> + Sem effs [VScript] +txSkelAllScripts txSkel = do + txSkelSpendingScripts <- txSkelInputScripts txSkel + return $ + toListOf (txSkelRedeemedScriptsT % userVScriptL) txSkel + <> txSkelSpendingScripts + +-- | Returns all scripts which guard transaction inputs +txSkelInputScripts :: + (Member Query effs) => + TxSkel -> + Sem effs [VScript] +txSkelInputScripts = + fmap catMaybes + . mapM (previewByRef (txSkelOutOwnerL % userVScriptAT)) + . Map.keys + . txSkelInputs + +-- | look up the UTxOs the transaction consumes, and sum their values. +txSkelInputValue :: + (Member Query effs) => + TxSkel -> + Sem effs Api.Value +txSkelInputValue = + fmap mconcat + . mapM (viewByRef txSkelOutValueL) + . Map.keys + . txSkelInputs + +-- | Returns a list of all currently known outputs +allUtxos :: + (Member Query effs) => + Sem effs (UtxoSearchResult '[]) + +-- | Returns a list of all UTxOs at a certain address. +utxosAt :: + ( Member Query effs, + Script.ToAddress cred + ) => + cred -> + Sem effs (UtxoSearchResult '[]) + +-- | Returns an output given a reference to it +txSkelOutByRef :: + (Member Query effs) => + Api.TxOutRef -> + Sem effs TxSkelOut + +-- | Retrieves the ordered list of outputs of the given "CardanoTx". +-- +-- This is useful when writing endpoints and/or traces to fetch utxos of +-- interest right from the start and avoid querying the chain for them +-- afterwards using 'allUtxos' or similar functions. +utxosFromCardanoTx :: + (Member Query effs) => + P.Ledger.CardanoTx -> + Sem effs (UtxoSearchResult '[]) +utxosFromCardanoTx = + utxosFromRefs + . fmap (P.Ledger.fromCardanoTxIn . snd) + . P.Ledger.getCardanoTxOutRefs + +-- | Go through all of the 'Api.TxOutRef's in the list and look them up in the +-- state of the blockchain, throwing an error if one of them cannot be resolved. +utxosFromRefs :: + ( Foldable f, + Member Query effs + ) => + f Api.TxOutRef -> + Sem effs (UtxoSearchResult '[]) +utxosFromRefs = + foldM + (\m oRef -> flip (Map.insert oRef) m . hSingleton <$> txSkelOutByRef oRef) + Map.empty + +-- | Retrieves an output and views a specific element out of it +viewByRef :: + ( Member Query effs, + Is g A_Getter + ) => + Optic' g is TxSkelOut c -> + Api.TxOutRef -> + Sem effs c +viewByRef optic = (view optic <$>) . txSkelOutByRef + +-- | Retrieves an output and previews a specific element out of it +previewByRef :: + ( Member Query effs, + Is af An_AffineFold + ) => + Optic' af is TxSkelOut c -> + Api.TxOutRef -> + Sem effs (Maybe c) +previewByRef optic = (preview optic <$>) . txSkelOutByRef + +-- | Gets the current official constitution script +getConstitutionScript :: + (Member Query effs) => + Sem effs (Maybe VScript) + +-- | Gets the current reward associated with a credential +getCurrentReward :: + ( Member Query effs, + Script.ToCredential c + ) => + c -> + Sem effs (Maybe Api.Lovelace) + +-- | The interpretation for read-only effect with a stored 'EmulatorState' and +-- 'ChainIndex' +runMockChainQuery :: + forall effs a. + ( Members + '[ State EmulatorState, + State ChainIndex, + Error P.Ledger.ToCardanoError, + Error ChainError + ] + effs + ) => + Sem (Query : effs) a -> + Sem effs a +runMockChainQuery = interpret $ \case + TxSkelOutByRef oRef -> do + res <- gets $ Map.lookup oRef . chainIndexOutputs + case res of + Just (txSkelOut, True) -> return txSkelOut + _ -> throw $ CEUnknownOutRef oRef + AllUtxos -> fetchUtxos $ const True + UtxosAt (Script.toAddress -> addr) -> fetchUtxos $ (== addr) . Script.toAddress + GetConstitutionScript -> gets $ view chainIndexConstitutionL + GetCurrentReward (Script.toCredential -> cred) -> do + stakeCredential <- toStakeCredential cred + gets $ + preview $ + emulatorStateLedgerStateL + % to (Emulator.getReward stakeCredential) + % _Just + % to coerce + where + fetchUtxos decide = + gets $ + toMapOf $ + chainIndexOutputsL + % itraversed + % filtered snd + % filtered (decide . fst) + % to (hSingleton . fst) + +-- | Interpret the `Query` effect by talking to a deployed node +-- through a `Cardano.LocalNodeConnectInfo` (socket path and network id) +-- provided via a `Reader`, running in a stack featuring @IO@ (via `Embed`). The +-- fixed chain configuration is resolved through the internal +-- 'Cooked.Effect.Params.Params' effect. +runBlockChainQuery :: + forall effs a. + ( Members + '[ Embed IO, + Params, + Error Cardano.UnsupportedNtcVersionError, + Error Cardano.EraMismatch, + Error Cardano.AcquiringFailure, + Error P.Ledger.ToCardanoError, + Error ChainError, + Reader Cardano.LocalNodeConnectInfo, + State ChainIndex + ] + effs + ) => + Sem (Query : effs) a -> + Sem effs a +runBlockChainQuery = interpret $ \case + AllUtxos -> queryUtxosAndHandleErrors Cardano.QueryUTxOWhole + UtxosAt (Script.toAddress -> addr) -> do + networkId <- getNetworkId + (Cardano.AddressInEra _ cAddr) <- fromEither $ P.Ledger.toCardanoAddressInEra networkId addr + queryUtxosAndHandleErrors $ Cardano.QueryUTxOByAddress $ Set.singleton $ Cardano.toAddressAny cAddr + TxSkelOutByRef oRef -> do + txIn <- fromEither $ P.Ledger.toCardanoTxIn oRef + utxo <- queryUtxosAndHandleErrors $ Cardano.QueryUTxOByTxIn $ Set.singleton txIn + maybe (throw $ CEUnknownOutRef oRef) (return . hHead) $ Map.lookup oRef utxo + GetConstitutionScript -> do + -- We retrieve the official optional script hash of the current constitution + Cardano.Constitution _ mScriptHash <- + queryAndHandleErrors $ Cardano.queryConstitution Cardano.ConwayEraOnwardsConway + -- We retrieve the optional constitution already stored in the chain index + mStoredConstitution <- gets chainIndexConstitution + -- We inspect the current option constitution script hash + case mScriptHash of + -- There is no official constitution (should not happen). We just set our + -- own constitution to @Nothing@ accordingly. + SNothing -> do + modify' $ set chainIndexConstitutionL Nothing + return Nothing + -- There is an official constitution, and it matches the stored one, which + -- we directly return. + SJust (Cardano.ScriptHash -> scriptHash) + | Just storedConstitution <- mStoredConstitution, + Script.toScriptHash scriptHash == Script.toScriptHash storedConstitution -> + return $ Just storedConstitution + -- There is an official constitution, and it does not match the stored one + -- (it has changed, or it's the first time it's been queried). We fetch + -- the actual constitution from a reference script at its own address, + -- where it should live, according to a governance convention. We store + -- the script we find there after verifying its hash, and return it. + SJust (Cardano.ScriptHash -> scriptHash) -> do + networkId <- getNetworkId + utxo <- + queryUtxosAndHandleErrors $ + Cardano.QueryUTxOByAddress $ + Set.singleton $ + Cardano.AddressShelley $ + Cardano.makeShelleyAddress + networkId + (Cardano.PaymentCredentialByScript scriptHash) + Cardano.NoStakeAddress + let newConstitution = + listToMaybe $ + [ script + | (_, preview txSkelOutReferenceScriptAT . hHead -> Just script) <- Map.toList utxo, + Script.toScriptHash script == Script.toScriptHash scriptHash + ] + modify' $ set chainIndexConstitutionL newConstitution + return newConstitution + GetCurrentReward (Script.toCredential -> cred) -> do + networkId <- getNetworkId + stakeCred <- toStakeCredential cred + (rewards, _) <- + queryAndHandleErrors $ + Cardano.queryStakeAddresses + Cardano.ShelleyBasedEraConway + (Set.singleton (Cardano.fromShelleyStakeCredential stakeCred)) + networkId + return $ Api.Lovelace . Cardano.unCoin <$> Map.lookup (Cardano.StakeAddress (Cardano.toShelleyNetwork networkId) stakeCred) rewards + where + -- Fetches the local node info, embeds a query in IO and handles errors + query q = do + conn <- ask + response <- embed $ Cardano.executeLocalStateQueryExpr conn Cardano.VolatileTip q + fromEither response + -- Handles one more layer of errors from the response of a query + queryAndHandleError q = query q >>= fromEither + -- Handles a second layer of error from the response of a query + queryAndHandleErrors q = queryAndHandleError q >>= fromEither + -- Queries the Utxos present on-chain, handling the errors, and returns the + -- query result in terms of @Utxos@, updated with the known chain index. + queryUtxosAndHandleErrors utxoFilter = do + utxo <- queryAndHandleErrors $ Cardano.queryUtxo Cardano.ShelleyBasedEraConway utxoFilter + knownUtxos <- gets chainIndexOutputs + return $ + Map.mapWithKey + (\oRef txSkelOut -> hSingleton $ maybe txSkelOut fst $ Map.lookup oRef knownUtxos) + (Map.mapKeysMonotonic P.Ledger.fromCardanoTxIn $ convertUtxo <$> Cardano.unUTxO utxo) + convertUtxo :: Cardano.TxOut Cardano.CtxUTxO Cardano.ConwayEra -> TxSkelOut + convertUtxo (Cardano.TxOut (P.Ledger.toPlutusAddress -> (Api.Address cred stCred)) val dat refScript) = + TxSkelOut + (review userCredentialI cred) + stCred + ( dat & \case + Cardano.TxOutDatumNone -> NoTxSkelOutDatum + Cardano.TxOutDatumHash _ hash -> + SomeTxSkelOutDatumHash $ Api.DatumHash $ Api.toBuiltin $ Cardano.serialiseToRawBytes hash + Cardano.TxOutDatumInline _ datum -> + SomeTxSkelOutDatum (P.Ledger.fromCardanoScriptData datum) Inline + ) + (P.Ledger.fromCardanoValue $ P.Ledger.fromCardanoTxOutValue val) + False + (P.Ledger.fromCardanoReferenceScript refScript) diff --git a/src/Cooked/Effect/Submission.hs b/src/Cooked/Effect/Submission.hs new file mode 100644 index 000000000..6c8100c7b --- /dev/null +++ b/src/Cooked/Effect/Submission.hs @@ -0,0 +1,99 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- | This module exposes the 'Submit' effect, which is responsible for +-- submitting a Cardano transaction for validation. +module Cooked.Effect.Submission + ( -- * The 'Submit' effect + Submit (..), + submitTransaction, + + -- * Interpretation functions + runMockChainSubmit, + runBlockChainSubmit, + ) +where + +import Cardano.Api qualified as Cardano +import Cardano.Ledger.Shelley.API.Mempool qualified as Shelley +import Cardano.Node.Emulator.Internal.Node qualified as Emulator +import Cooked.Effect.Params +import Cooked.Runtime.State +import Cooked.Utilities.Aliases +import Data.Foldable.Extra +import Ledger.Orphans () +import Optics.Core +import Polysemy +import Polysemy.Error +import Polysemy.Fail +import Polysemy.Reader +import Polysemy.State + +-- | An effect allow to submit a transaction for validation +data Submit :: Effect where + SubmitTransaction :: Transaction -> Submit m SubmissionFailures + +makeSem_ ''Submit + +-- | Submits a transaction for validation, returning a (possibly empty) list of +-- submission failures. +submitTransaction :: + (Member Submit effs) => + Transaction -> + Sem effs SubmissionFailures + +-- | Interprets the `Submit` effect on an emulator +runMockChainSubmit :: + forall effs a. + (Member (State EmulatorState) effs) => + Sem (Submit : effs) a -> + Sem effs a +runMockChainSubmit = interpret $ \case + SubmitTransaction cardanoTx -> do + -- To run transaction validation we need a minimal ledger state + eLedgerState <- gets emulatorStateLedgerState + -- And the emulator params + params <- gets emulatorStateParams + -- We run the transaction validation through the emulator + let (newELedgerState, submissionFailures) = case Emulator.validateAndApplyTx params eLedgerState cardanoTx of + Left (Shelley.ApplyTxError errs) -> (newELedgerState, toList errs) + Right (newELedgerState', _) -> (newELedgerState', []) + -- We update the index with the utxos consumed and produced by the tx + modify' $ set emulatorStateLedgerStateL newELedgerState + -- We return the validation result + return submissionFailures + +-- | Interprets the `Submit` effect by submitting the generated +-- transaction to a deployed node through a `Cardano.LocalNodeConnectInfo` +-- (socket path and network id) provided via a `Reader`, running in a stack +-- featuring @IO@ (via `Embed`). +runBlockChainSubmit :: + forall effs a. + ( Members + '[ Embed IO, + Error Cardano.EraMismatch, + Params, + Reader Cardano.LocalNodeConnectInfo, + Fail + ] + effs + ) => + Sem (Submit : effs) a -> + Sem effs a +runBlockChainSubmit = interpret $ \case + SubmitTransaction cardanoTx -> do + -- We retrieve the local node connection info. + conn <- ask + -- We submit the transaction to the node + result <- embed $ Cardano.submitTxToNodeLocal conn $ Cardano.TxInMode Cardano.ShelleyBasedEraConway cardanoTx + -- We disect the result the node sends us + case result of + Cardano.SubmitFail + ( Cardano.TxValidationErrorInCardanoMode + (Cardano.ShelleyTxValidationError Cardano.ShelleyBasedEraConway (Shelley.ApplyTxError err)) + ) -> return $ toList err + -- Somehow, the error does not correspond to the proper era, should be unreachable + Cardano.SubmitFail (Cardano.TxValidationErrorInCardanoMode _) -> fail "TxValidationErrorInCardanoMode: Unreachable case" + -- There is an era mismatch between the ledger era and the transaction era + Cardano.SubmitFail (Cardano.TxValidationEraMismatch eraMismatch) -> throw eraMismatch + -- The submission was successful (no phase 1 error) + Cardano.SubmitSuccess -> return [] diff --git a/src/Cooked/Effect/Time.hs b/src/Cooked/Effect/Time.hs new file mode 100644 index 000000000..f097dd934 --- /dev/null +++ b/src/Cooked/Effect/Time.hs @@ -0,0 +1,238 @@ +-- | This module exposes the user-facing primitives to query and manipulate the +-- current time of the blockchain, expressed in terms of slots and POSIX time. +-- It regroups the time-related read primitives (the current slot, the +-- conversions between slots and POSIX time ranges) as well as the primitives to +-- wait for a given slot or time. The lower-level configuration primitives (era +-- history and system start) live in the internal +-- 'Cooked.Effect.Params.Params' effect, which the node +-- interpreter of this effect relies on. +module Cooked.Effect.Time + ( -- * The 'Time' effect + Time, + + -- * 'Time' interpreters + runMockChainTime, + runBlockChainTime, + + -- * Queries related to the current time + currentSlot, + currentMSRange, + getEnclosingSlot, + slotRangeBefore, + slotRangeAfter, + slotToMSRange, + + -- * Modifications of the current time + waitNSlots, + awaitSlot, + awaitEnclosingSlot, + waitNMSFromSlotLowerBound, + waitNMSFromSlotUpperBound, + ) +where + +import Cardano.Api qualified as Cardano +import Cardano.Node.Emulator.Internal.Node qualified as Emulator +import Cardano.Slotting.Time qualified as Time +import Control.Concurrent (threadDelay) +import Control.Lens qualified as Lens +import Control.Monad +import Cooked.Effect.Params +import Cooked.Runtime.State +import Data.Time.Clock +import Data.Time.Clock.POSIX +import Ledger.Slot qualified as P.Ledger +import Optics.Core +import PlutusLedgerApi.V3 qualified as Api +import Polysemy +import Polysemy.Error +import Polysemy.Fail +import Polysemy.Reader +import Polysemy.State + +-- | An effect that offers primitives to query, convert, and wait on the current +-- time of the mockchain. The read-only primitives ('currentSlot', +-- 'slotToMSRange', 'getEnclosingSlot') do not alter the state, while the waiting +-- primitive ('waitNSlots') advances the current slot. +data Time :: Effect where + CurrentSlot :: Time m P.Ledger.Slot + SlotToMSRange :: P.Ledger.Slot -> Time m (Api.POSIXTime, Api.POSIXTime) + GetEnclosingSlot :: Api.POSIXTime -> Time m P.Ledger.Slot + WaitNSlots :: Integer -> Time m P.Ledger.Slot + +makeSem_ ''Time + +-- | Returns the current slot +currentSlot :: + (Member Time effs) => + Sem effs P.Ledger.Slot + +-- | Returns the closed ms interval corresponding to the slot with the given +-- number. +slotToMSRange :: + (Members '[Time, Fail] effs) => + P.Ledger.Slot -> + Sem effs (Api.POSIXTime, Api.POSIXTime) + +-- | Returns the closed ms interval corresponding to the current slot +currentMSRange :: + (Members '[Time, Fail] effs) => + Sem effs (Api.POSIXTime, Api.POSIXTime) +currentMSRange = slotToMSRange =<< currentSlot + +-- | Return the slot that contains the given time. See 'slotToMSRange' for +-- some satisfied equational properties. +getEnclosingSlot :: + (Member Time effs) => + Api.POSIXTime -> + Sem effs P.Ledger.Slot + +-- | The infinite range of slots ending before or at the given time +slotRangeBefore :: + (Members '[Time, Fail] effs) => + Api.POSIXTime -> + Sem effs P.Ledger.SlotRange +slotRangeBefore t = do + n <- getEnclosingSlot t + (_, b) <- slotToMSRange n + -- If the given time @t@ happens to be the last ms of its slot, we can include + -- the whole slot. Otherwise, the only way to be sure that the returned slot + -- range contains no time after @t@ is to go to the preceding slot. + return $ Api.to $ if t == b then n else n - 1 + +-- | The infinite range of slots starting after or at the given time +slotRangeAfter :: + (Members '[Time, Fail] effs) => + Api.POSIXTime -> + Sem effs P.Ledger.SlotRange +slotRangeAfter t = do + n <- getEnclosingSlot t + (a, _) <- slotToMSRange n + return $ Api.from $ if t == a then n else n + 1 + +-- | Waits a certain number of slots and returns the new slot +waitNSlots :: + (Member Time effs) => + Integer -> + Sem effs P.Ledger.Slot + +-- | Wait for a certain slot, or throws an error if the slot is already past +awaitSlot :: (Member Time effs) => P.Ledger.Slot -> Sem effs P.Ledger.Slot +awaitSlot (P.Ledger.Slot targetSlot) = do + P.Ledger.Slot now <- currentSlot + waitNSlots (targetSlot - now) + +-- | Waits until the current slot becomes greater or equal to the slot +-- containing the given POSIX time. Note that that it might not wait for +-- anything if the current slot is large enough. +awaitEnclosingSlot :: (Member Time effs) => Api.POSIXTime -> Sem effs P.Ledger.Slot +awaitEnclosingSlot time = getEnclosingSlot time >>= awaitSlot + +-- | Wait a given number of ms from the lower bound of the current slot and +-- returns the current slot after waiting. +waitNMSFromSlotLowerBound :: (Members '[Time, Fail] effs) => Integer -> Sem effs P.Ledger.Slot +waitNMSFromSlotLowerBound duration = currentMSRange >>= awaitEnclosingSlot . (+ fromIntegral duration) . fst + +-- | Wait a given number of ms from the upper bound of the current slot and +-- returns the current slot after waiting. +waitNMSFromSlotUpperBound :: (Members '[Time, Fail] effs) => Integer -> Sem effs P.Ledger.Slot +waitNMSFromSlotUpperBound duration = currentMSRange >>= awaitEnclosingSlot . (+ fromIntegral duration) . snd + +-- | The interpretation for the time effect with a stored 'EmulatorState' +runMockChainTime :: + forall effs a. + ( Members + '[ State EmulatorState, + Fail + ] + effs + ) => + Sem (Time : effs) a -> + Sem effs a +runMockChainTime = interpret $ \case + CurrentSlot -> gets $ view $ emulatorStateLedgerStateL % to Emulator.getSlot + SlotToMSRange slot -> do + slotConfig <- gets $ Emulator.pSlotConfig . emulatorStateParams + case Emulator.slotToPOSIXTimeRange slotConfig slot of + Api.Interval + (Api.LowerBound (Api.Finite l) leftclosed) + (Api.UpperBound (Api.Finite r) rightclosed) -> + return + ( if leftclosed then l else l + 1, + if rightclosed then r else r - 1 + ) + _ -> fail "Unexpected unbounded slot: please report a bug at https://github.com/tweag/cooked-validators/issues" + GetEnclosingSlot t -> gets $ (`Emulator.posixTimeToEnclosingSlot` t) . Emulator.pSlotConfig . emulatorStateParams + WaitNSlots n -> do + cs <- gets $ Emulator.getSlot . emulatorStateLedgerState + -- Waiting for a non-positive number of slots does not change the current + -- slot, and we simply return it unchanged. + if n <= 0 + then return cs + else do + let newSlot = cs + fromIntegral n + modify' $ over emulatorStateLedgerStateL $ Lens.set Emulator.elsSlotL $ fromIntegral newSlot + return newSlot + +-- | Interpret the `Time` effect by talking to a deployed node through a +-- `Cardano.LocalNodeConnectInfo` (socket path and network id) provided via a +-- `Reader`, running in a stack featuring @IO@ (via `Embed`). Waiting is +-- performed by suspending the thread for the appropriate amount of time. The +-- fixed chain configuration is resolved through the internal +-- 'Cooked.Effect.Params.Params' effect. +runBlockChainTime :: + forall effs a. + ( Members + '[ Embed IO, + Params, + Error Cardano.PastHorizonException, + Reader Cardano.LocalNodeConnectInfo + ] + effs + ) => + Sem (Time : effs) a -> + Sem effs a +runBlockChainTime = interpret $ \case + CurrentSlot -> getNodeSlot + SlotToMSRange slot -> slotToMS slot + GetEnclosingSlot t -> do + eraHistory <- getEraHistory + systemStart <- getSystemStart + let relTime = Time.toRelativeTime systemStart $ posixTimeToUTC t + fromSlotNo <$> fromEither (Cardano.getSlotForRelativeTime relTime eraHistory) + WaitNSlots n -> do + P.Ledger.Slot cs <- getNodeSlot + let target = P.Ledger.Slot $ cs + n + -- We compute the POSIX time at which the target slot begins and suspend the + -- thread until we reach it, if it lies in the future. + (Api.POSIXTime targetMS, _) <- slotToMS target + nowUTC <- embed getCurrentTime + let diff = diffUTCTime (posixTimeToUTC (Api.POSIXTime targetMS)) nowUTC + when (diff > 0) $ embed $ threadDelay $ round $ diff * 1000000 + return $ P.Ledger.Slot $ max cs (cs + n) + where + -- Retrieves the current slot from the local node chain tip + getNodeSlot :: Sem effs P.Ledger.Slot + getNodeSlot = do + conn <- ask + chainTip <- embed $ Cardano.getLocalChainTip conn + return $ case chainTip of + Cardano.ChainTipAtGenesis -> P.Ledger.Slot 0 + (Cardano.ChainTip slotNo _ _) -> fromSlotNo slotNo + -- Converts a slot into the closed POSIX ms interval it spans + slotToMS :: P.Ledger.Slot -> Sem effs (Api.POSIXTime, Api.POSIXTime) + slotToMS slot = do + eraHistory <- getEraHistory + systemStart <- getSystemStart + (relStart, slotLen) <- fromEither $ Cardano.getProgress (toSlotNo slot) eraHistory + let startUTC = Time.fromRelativeTime systemStart relStart + endUTC = Time.getSlotLength slotLen `addUTCTime` startUTC + return (utcToPOSIXTime startUTC, utcToPOSIXTime endUTC - 1) + -- Converts a Plutus slot to a Cardano slot + toSlotNo = Cardano.SlotNo . fromInteger . P.Ledger.getSlot + -- Converts a Cardano slot to a Plutus slot + fromSlotNo (Cardano.SlotNo w) = P.Ledger.Slot (toInteger w) + -- Converts a POSIX time to a UTC time + posixTimeToUTC = posixSecondsToUTCTime . fromRational . (/ 1000) . toRational . Api.getPOSIXTime + -- Converts a UTC time to a POSIX time + utcToPOSIXTime = Api.POSIXTime . round . (1000 *) . utcTimeToPOSIXSeconds diff --git a/src/Cooked/Effect/Validation.hs b/src/Cooked/Effect/Validation.hs new file mode 100644 index 000000000..c81394762 --- /dev/null +++ b/src/Cooked/Effect/Validation.hs @@ -0,0 +1,167 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- | This module exposes the `Validate` effect, which is responsible +-- for turning a `Cooked.Skeleton.TxSkel` into an actual transaction and +-- submitting it to the emulated ledger. This includes running the whole +-- adjustment pipeline (auto-filling, balancing and transaction generation) and +-- updating the mockchain state based on the validation outcome. +module Cooked.Effect.Validation + ( -- * The `Validate` effect + Validate (..), + validateTxSkel, + validateTxSkel', + validateTxSkelL, + validateTxSkel_, + + -- * Interpreting the effect + runChainValidate, + ) +where + +import Cardano.Api qualified as Cardano +import Control.Monad +import Cooked.Automation +import Cooked.Effect.Log +import Cooked.Effect.Params +import Cooked.Effect.Query +import Cooked.Effect.Submission +import Cooked.Runtime.Error +import Cooked.Runtime.Journal +import Cooked.Runtime.State +import Cooked.Skeleton +import Cooked.Utilities.Aliases +import Data.Foldable.Extra +import Data.Map.Strict qualified as Map +import Data.Set qualified as Set +import Ledger.Orphans () +import Ledger.Tx qualified as P.Ledger +import Ledger.Tx.CardanoAPI qualified as P.Ledger +import Optics.Core +import PlutusLedgerApi.V3 qualified as Api +import Polysemy +import Polysemy.Error +import Polysemy.Fail +import Polysemy.State + +-- | An effect that offers the ability to submit a 'TxSkel' throughout the +-- modification and validation pipeline. Technically, this effect is not needed +-- from a semantical perspective, as all of this could already be expressed in +-- 'Submit', however, we want this effect to exist on its own to be +-- eligible to be modified by tweaks. +data Validate :: Effect where + ValidateTxSkel :: TxSkel -> Validate m (ExtendedTxSkel, SubmissionFailures, Transaction, Utxos) + +makeSem_ ''Validate + +-- | Generates, balances and validates a transaction from a skeleton. Returns +-- the extended skeleton, generated transaction and the new produced outputs. +validateTxSkel :: + (Member Validate effs) => + TxSkel -> + Sem effs (ExtendedTxSkel, SubmissionFailures, Transaction, Utxos) + +-- | Same as `validateTxSkel`, but only returns the generated UTxOs +validateTxSkel' :: + (Member Validate effs) => + TxSkel -> + Sem effs Utxos +validateTxSkel' = fmap (view _4) . validateTxSkel + +-- | Same as `validateTxSkel'`, but only returns the list of produced +-- 'Api.TxOutRef' +validateTxSkelL :: + (Member Validate effs) => + TxSkel -> + Sem effs [Api.TxOutRef] +validateTxSkelL = fmap (toList . Map.keysSet) . validateTxSkel' + +-- | Same as `validateTxSkel`, but discards the returned transaction +validateTxSkel_ :: + (Member Validate effs) => + TxSkel -> + Sem effs () +validateTxSkel_ = void . validateTxSkel + +-- | Interpretes the 'Validate' effects in terms of other effects, in +-- particular 'Submit'. +runChainValidate :: + ( Members + '[ Log, + Query, + Params, + Submit, + Error P.Ledger.ToCardanoError, + Error ChainError, + State ChainIndex, + Fail + ] + effs + ) => + Sem (Validate : effs) a -> + Sem effs a +runChainValidate = interpret $ \case + ValidateTxSkel txSkel -> do + -- We fetch the skeleton options + let TxSkelOpts {..} = txSkelOpts txSkel + -- We log the submission of the new skeleton + logEvent $ CLogSubmittedTxSkel txSkel + -- We run the automation pipeline on the original skeleton + eSkel@(ExtendedTxSkel finalTxSkel fee mCollaterals txBody exUnitsFailures) <- runAutomationPipeline txSkel + -- We log the adjusted skeleton + logEvent $ CLogAdjustedTxSkel finalTxSkel fee mCollaterals + -- We handle the execution units failures when applicable + when (notNull exUnitsFailures) $ + if txSkelOptHaltOnExUnitsFailures + -- If requested, we treat them as fatal, ending the run + then throw $ CEExUnitsFailures exUnitsFailures + -- Otherwise, we just log them + else logEvent $ CELogExUnitsFailures exUnitsFailures + -- We build the Cardano transaction, and apply on it the modification in the + -- skeleton option + let cardanoTx = txSkelOptModTx $ txSignatoriesAndBodyToCardanoTx (view txSkelSignatoriesL finalTxSkel) txBody + -- We wrap it for plutus-ledger usage + let pCardanoTx = P.Ledger.CardanoTx cardanoTx Cardano.ShelleyBasedEraConway + -- We compute the id of the new transaction + let txId = P.Ledger.fromCardanoTxId $ P.Ledger.getCardanoTxId pCardanoTx + -- We submit the transaction for validation + submissionFailures <- submitTransaction cardanoTx + -- We handle the submission failures when applicable + when (notNull submissionFailures) $ + if txSkelOptHaltOnSubmissionFailures + -- If requested, we treat them as fatal, ending the run + then throw $ CESubmissionFailures submissionFailures + -- Otherwise, we just log them + else logEvent $ CELogSubmissionFailures submissionFailures + -- We compute the set of consumed outputs and new outputs, based on the + -- validity of the transaction, producing some validity logs in the process. + (consumedInputs, newOutputs) <- + if + -- the transaction is valid, the index is modified based on the regular + -- inputs and outputs of the transaction. + | null submissionFailures && null exUnitsFailures -> do + let inputs = Map.keysSet $ txSkelInputs finalTxSkel + outputs = fromCardanoIndex (P.Ledger.getCardanoTxProducedOutputs pCardanoTx) $ txSkelOutputs finalTxSkel + logEvent $ CLogNewTx txId $ Valid (length inputs) (Map.size outputs) + return (inputs, outputs) + -- the transaction fails in phase 1, the index remains unchanged. + | notNull submissionFailures -> do + logEvent $ CLogNewTx txId InvalidPhase1 + return (Set.empty, Map.empty) + -- the transaction fails in phase 2, but no collaterals were + -- provided. This is an unreachable case. + | Nothing <- mCollaterals -> + fail + "Unreachable case when processing validation result, please report a bug at https://github.com/tweag/cooked-validators/issues" + -- the transaction fails in phase 2, and collaterals are provided, the + -- index is modified based on the collateral inputs and outputs of the + -- transaction. + | Just (colIns, retCol) <- mCollaterals -> do + let outputs = fromCardanoIndex (P.Ledger.getCardanoTxProducedReturnCollateral pCardanoTx) $ toList retCol + logEvent $ CLogNewTx txId $ InvalidPhase2 (length colIns) (Map.size outputs) + return (colIns, outputs) + -- We update the index with the consumed and produced outputs + modify' $ removeOutputs consumedInputs + modify' $ addOutputs $ Map.toList newOutputs + return (eSkel, submissionFailures, cardanoTx, newOutputs) + where + fromCardanoIndex index = Map.fromList . zip (P.Ledger.fromCardanoTxIn . fst <$> Map.toList index) diff --git a/src/Cooked/MockChain.hs b/src/Cooked/MockChain.hs index 7dff3ba5e..f86dcb11f 100644 --- a/src/Cooked/MockChain.hs +++ b/src/Cooked/MockChain.hs @@ -1,17 +1,11 @@ --- | This module centralizes everything related to our mockchain, while hiding --- elements related to logs and inner state. +-- | This module centralizes the emulated-chain (MockChain) running code. It is +-- an umbrella re-exporting all the MockChain submodules (instances, running, +-- tweaking and testing). module Cooked.MockChain (module X) where -import Cooked.MockChain.Automation.Balancing as X -import Cooked.MockChain.Common as X -import Cooked.MockChain.Effect.Misc as X -import Cooked.MockChain.Effect.Read as X -import Cooked.MockChain.Effect.Write as X -import Cooked.MockChain.Run.Instances as X -import Cooked.MockChain.Run.Runnable as X -import Cooked.MockChain.Run.Tweak as X -import Cooked.MockChain.Runtime.Error as X -import Cooked.MockChain.Runtime.Journal as X -import Cooked.MockChain.Runtime.State as X +import Cooked.MockChain.Config as X +import Cooked.MockChain.Instances as X +import Cooked.MockChain.Ltl as X +import Cooked.MockChain.Run as X import Cooked.MockChain.Testing as X -import Cooked.MockChain.UtxoSearch as X +import Cooked.MockChain.Tweak as X diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Body.hs b/src/Cooked/MockChain/Automation/GenerateTx/Body.hs deleted file mode 100644 index 1019f7c8b..000000000 --- a/src/Cooked/MockChain/Automation/GenerateTx/Body.hs +++ /dev/null @@ -1,157 +0,0 @@ --- | This modules exposes entry points to convert a 'TxSkel' into a fully --- fledged transaction body -module Cooked.MockChain.Automation.GenerateTx.Body - ( txSkelToTxBody, - txBodyContentToTxBody, - txSkelToTxBodyContent, - txSkelToIndex, - txSignatoriesAndBodyToCardanoTx, - txSkelToCardanoTx, - ) -where - -import Cardano.Api qualified as Cardano -import Cardano.Node.Emulator.Internal.Node qualified as Emulator -import Control.Monad -import Cooked.MockChain.Automation.GenerateTx.Certificate -import Cooked.MockChain.Automation.GenerateTx.Collateral -import Cooked.MockChain.Automation.GenerateTx.Input -import Cooked.MockChain.Automation.GenerateTx.Mint -import Cooked.MockChain.Automation.GenerateTx.Output -import Cooked.MockChain.Automation.GenerateTx.Proposal -import Cooked.MockChain.Automation.GenerateTx.ReferenceInputs -import Cooked.MockChain.Automation.GenerateTx.Withdrawals -import Cooked.MockChain.Automation.GenerateTx.Witness -import Cooked.MockChain.Common -import Cooked.MockChain.Effect.Read -import Cooked.MockChain.Runtime.Error -import Cooked.Skeleton -import Data.Map qualified as Map -import Data.Maybe -import Data.Set qualified as Set -import Ledger.Address qualified as P.Ledger -import Ledger.Tx.CardanoAPI qualified as P.Ledger -import Plutus.Script.Utils.Address qualified as Script -import Polysemy -import Polysemy.Error -import Polysemy.Fail - --- | Generates a body content from a skeleton -txSkelToTxBodyContent :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => - TxSkel -> - Fee -> - Maybe Collaterals -> - Sem effs (Cardano.TxBodyContent Cardano.BuildTx Cardano.ConwayEra) -txSkelToTxBodyContent skel@TxSkel {..} fee mCollaterals = do - txIns <- mapM toTxInAndWitness $ Map.toList txSkelInputs - txInsReference <- toInsReference skel - (txInsCollateral, txTotalCollateral, txReturnCollateral) <- toCollateralTriplet mCollaterals - txOuts <- mapM toCardanoTxOut txSkelOutputs - (txValidityLowerBound, txValidityUpperBound) <- fromEither $ P.Ledger.toCardanoValidityRange txSkelValidityRange - txMintValue <- toMintValue txSkelMints - txExtraKeyWits <- - if null txSkelSignatories - then return Cardano.TxExtraKeyWitnessesNone - else - Cardano.TxExtraKeyWitnesses Cardano.AlonzoEraOnwardsConway - <$> fromEither - (mapM (P.Ledger.toCardanoPaymentKeyHash . P.Ledger.PaymentPubKeyHash . Script.toPubKeyHash) txSkelSignatories) - txProtocolParams <- Cardano.BuildTxWith . Just . Emulator.ledgerProtocolParameters <$> getParams - txProposalProcedures <- Just . Cardano.Featured Cardano.ConwayEraOnwardsConway <$> toProposalProcedures txSkelProposals - txWithdrawals <- toWithdrawals txSkelWithdrawals - txCertificates <- toCertificates txSkelCertificates - let txFee = Cardano.TxFeeExplicit Cardano.ShelleyBasedEraConway $ Cardano.Coin fee - txMetadata = Cardano.TxMetadataNone - txAuxScripts = Cardano.TxAuxScriptsNone - txUpdateProposal = Cardano.TxUpdateProposalNone - txScriptValidity = Cardano.TxScriptValidityNone - txVotingProcedures = Nothing - txCurrentTreasuryValue = Nothing - txTreasuryDonation = Nothing - return Cardano.TxBodyContent {..} - --- | Generates a transaction body from a body content -txBodyContentToTxBody :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => - Cardano.TxBodyContent Cardano.BuildTx Cardano.ConwayEra -> - Sem effs (Cardano.TxBody Cardano.ConwayEra) -txBodyContentToTxBody txBodyContent = do - params <- getParams - -- We create the associated Shelley TxBody - fromEither $ Emulator.createTransactionBody params $ P.Ledger.CardanoBuildTx txBodyContent - --- | Generates an index with utxos known to a 'TxSkel' -txSkelToIndex :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => - TxSkel -> - Maybe Collaterals -> - Sem effs (Cardano.UTxO Cardano.ConwayEra) -txSkelToIndex txSkel mCollaterals = do - -- We build the index of UTxOs which are known to this skeleton. This includes - -- collateral inputs, inputs and reference inputs. - let collateralIns = maybe [] (Set.toList . fst) mCollaterals - -- We retrieve all the outputs known to the skeleton - (knownTxORefs, knownTxOuts) <- unzip . Map.toList <$> lookupUtxos (Set.toList (txSkelKnownTxOutRefs txSkel) <> collateralIns) - -- We then compute their Cardano counterparts - txOutL <- forM knownTxOuts toCardanoTxOut - -- We build the index and handle the possible error - txInL <- fromEither $ forM knownTxORefs P.Ledger.toCardanoTxIn - return $ Cardano.UTxO $ Map.fromList $ zip txInL $ Cardano.toCtxUTxOTxOut <$> txOutL - --- | Generates a transaction body from a 'TxSkel' and associated fee and --- collateral information. This transaction body accounts for the actual --- execution units of each of the scripts involved in the skeleton. -txSkelToTxBody :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => - TxSkel -> - Fee -> - Maybe Collaterals -> - Sem effs (Cardano.TxBody Cardano.ConwayEra) -txSkelToTxBody txSkel fee mCollaterals = do - -- We create a first body content and body, without execution units - txBodyContent' <- txSkelToTxBodyContent txSkel fee mCollaterals - txBody' <- txBodyContentToTxBody txBodyContent' - -- We create a full transaction from the body - let tx' = txSignatoriesAndBodyToCardanoTx (txSkelSignatories txSkel) txBody' - -- We retrieve the index and parameters to feed to @getTxExUnitsWithLogs@ - index <- txSkelToIndex txSkel mCollaterals - params <- getParams - -- We retrieve the execution units associated with the transaction - case Emulator.getTxExUnitsWithLogs params (P.Ledger.fromPlutusIndex index) tx' of - -- Computing the execution units can result in all kinds of phase 2 - -- validation failures, except for the ones related to the execution units - -- themselves. Unless required in the options, we throw the validation - -- failure right away when applicable. - Left err | not $ txSkelOptDeferPhase2FailuresDuringBalancing $ txSkelOpts txSkel -> throw $ uncurry MCEValidationError err - -- The other option is to ignore those and return the unchanged body with - -- the existing execution units, postponing the handling of the failures. - Left _ -> return txBody' - -- When no error arises, we get an execution unit for each script usage. We - -- first have to transform this Ledger map to a cardano API map. - Right (Map.mapKeysMonotonic (Cardano.toScriptIndex Cardano.AlonzoEraOnwardsConway) . fmap (Cardano.fromAlonzoExUnits . snd) -> exUnits) -> - -- We can then assign the right execution units to the body content - case Cardano.substituteExecutionUnits exUnits txBodyContent' of - -- This can only be a @TxBodyErrorScriptWitnessIndexMissingFromExecUnitsMap@ - Left err -> throw $ MCEFailure $ "Error while assigning execution units: " <> show err - -- We now have a body content with proper execution units and can create - -- the final body from it - Right txBodyContent -> txBodyContentToTxBody txBodyContent - --- | Generates a Cardano transaction and signs it -txSignatoriesAndBodyToCardanoTx :: - [TxSkelSignatory] -> - Cardano.TxBody Cardano.ConwayEra -> - Cardano.Tx Cardano.ConwayEra -txSignatoriesAndBodyToCardanoTx signatories txBody = Cardano.Tx txBody $ mapMaybe (toKeyWitness txBody) signatories - --- | Generates a full Cardano transaction from a skeleton, fees and collaterals -txSkelToCardanoTx :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => - TxSkel -> - Fee -> - Maybe Collaterals -> - Sem effs (Cardano.Tx Cardano.ConwayEra) -txSkelToCardanoTx txSkel fee = - fmap (txSignatoriesAndBodyToCardanoTx (txSkelSignatories txSkel)) - . txSkelToTxBody txSkel fee diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Input.hs b/src/Cooked/MockChain/Automation/GenerateTx/Input.hs deleted file mode 100644 index 7d2c6091e..000000000 --- a/src/Cooked/MockChain/Automation/GenerateTx/Input.hs +++ /dev/null @@ -1,35 +0,0 @@ --- | This module exposes the generation of transaction inputs -module Cooked.MockChain.Automation.GenerateTx.Input (toTxInAndWitness) where - -import Cardano.Api qualified as Cardano -import Cooked.MockChain.Automation.GenerateTx.Witness -import Cooked.MockChain.Effect.Read -import Cooked.MockChain.Runtime.Error -import Cooked.Skeleton -import Ledger.Tx.CardanoAPI qualified as P.Ledger -import PlutusLedgerApi.V3 qualified as Api -import Polysemy -import Polysemy.Error - --- | Converts a 'TxSkel' input, which consists of a 'Api.TxOutRef' and a --- 'TxSkelRedeemer', into a 'Cardano.TxIn', together with the appropriate witness. -toTxInAndWitness :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => - (Api.TxOutRef, TxSkelRedeemer) -> - Sem - effs - ( Cardano.TxIn, - Cardano.BuildTxWith Cardano.BuildTx (Cardano.Witness Cardano.WitCtxTxIn Cardano.ConwayEra) - ) -toTxInAndWitness (txOutRef, txSkelRedeemer) = do - TxSkelOut {txSkelOutOwner, txSkelOutDatum} <- txSkelOutByRef txOutRef - witness <- case txSkelOutOwner of - UserPubKey _ -> return $ Cardano.KeyWitness Cardano.KeyWitnessForSpending - UserScript script -> - fmap (Cardano.ScriptWitness Cardano.ScriptWitnessForSpending) $ - toScriptWitness script txSkelRedeemer $ - case txSkelOutDatum of - NoTxSkelOutDatum -> Cardano.ScriptDatumForTxIn Nothing - SomeTxSkelOutDatum _ Inline -> Cardano.InlineScriptDatum - SomeTxSkelOutDatum dat _ -> Cardano.ScriptDatumForTxIn $ Just $ P.Ledger.toCardanoScriptData $ Api.toBuiltinData dat - (,Cardano.BuildTxWith witness) <$> fromEither (P.Ledger.toCardanoTxIn txOutRef) diff --git a/src/Cooked/MockChain/Common.hs b/src/Cooked/MockChain/Common.hs deleted file mode 100644 index e8429773a..000000000 --- a/src/Cooked/MockChain/Common.hs +++ /dev/null @@ -1,32 +0,0 @@ --- | This module exposes some type aliases common to our MockChain library -module Cooked.MockChain.Common - ( -- * Type aliases - Fee, - CollateralIns, - Collaterals, - Utxo, - Utxos, - ) -where - -import Cooked.Skeleton.Output -import Data.Set (Set) -import PlutusLedgerApi.V3 qualified as Api - --- * Type aliases - --- | An alias for Integers used as fees -type Fee = Integer - --- | An alias for sets of utxos used as collateral inputs -type CollateralIns = Set Api.TxOutRef - --- | An alias for optional pairs of collateral inputs and optional return --- collateral output -type Collaterals = (CollateralIns, Maybe TxSkelOut) - --- | An alias for an output and its reference -type Utxo = (Api.TxOutRef, TxSkelOut) - --- | An alias for lists of `Utxo` -type Utxos = [Utxo] diff --git a/src/Cooked/MockChain/Run/Runnable.hs b/src/Cooked/MockChain/Config.hs similarity index 53% rename from src/Cooked/MockChain/Run/Runnable.hs rename to src/Cooked/MockChain/Config.hs index 6f7667abf..8640c8b6a 100644 --- a/src/Cooked/MockChain/Run/Runnable.hs +++ b/src/Cooked/MockChain/Config.hs @@ -1,45 +1,34 @@ --- | This module exposes the infrastructure to execute mockchain runs. In --- particular: --- --- - The notion of initial distribution (a list of payments) --- --- - The return types of the runs (raw and refined) --- --- - The initial configuration with which to execute a run --- --- - The notion of `RunnableMockChain` to actually execute computations -module Cooked.MockChain.Run.Runnable - ( InitialDistribution, +-- | This module exposes the configuration elements required to execute a +-- mockchain run. This includes the initial parameters (with an initial +-- distribution of funds), and various ways of processing the results of a run. +module Cooked.MockChain.Config + ( -- * Initial distributions + InitialDistribution, initialDistributionTemplate, distributionFromList, + + -- * Initial mockchain configurations + MockChainConf (..), + mockChainConfTemplate, + + -- * Mockchain run return type RawMockChainReturn, MockChainReturn (..), FunOnMockChainResult, unRawMockChainReturn, - MockChainConf (..), - mockChainConfTemplate, - RunnableMockChain (..), - runMockChainFromConf, - runMockChainFromInitDist, - runMockChainFromInitDistTemplate, - runMockChainDef, ) where -import Cooked.MockChain.Effect.Write -import Cooked.MockChain.Runtime.Error -import Cooked.MockChain.Runtime.Journal -import Cooked.MockChain.Runtime.State +import Cooked.Runtime.Error +import Cooked.Runtime.Journal +import Cooked.Runtime.State import Cooked.Skeleton.Output -import Cooked.Wallet +import Cooked.Utilities.Wallet import Data.Default import Data.List (foldl') import Data.Map (Map) import Plutus.Script.Utils.Value qualified as Script import PlutusLedgerApi.V3 qualified as Api -import Polysemy - --- * Initial distribution of funds -- | Describes the initial distribution of UTxOs per user. -- @@ -72,20 +61,20 @@ distributionFromList = foldl' (\x (user, values) -> x <> map (receives user . Va -- | Raw return type of running a mockchain type RawMockChainReturn a = - (MockChainJournal, (MockChainState, Either MockChainError a)) + (ChainJournal, (ChainIndex, (EmulatorState, Either ChainError a))) -- | The returned type when running a mockchain. This is both a reorganizing and -- filtering of the natural returned type `RawMockChainReturn`. data MockChainReturn a where MockChainReturn :: { -- | The value returned by the computation, or an error - mcrValue :: Either MockChainError a, + mcrValue :: Either ChainError a, -- | The outputs at the end of the run mcrOutputs :: Map Api.TxOutRef (TxSkelOut, Bool), -- | The 'UtxoState' at the end of the run mcrUtxoState :: UtxoState, -- | The final journal emitted during the run - mcrJournal :: MockChainJournal + mcrJournal :: ChainJournal } -> MockChainReturn a deriving (Functor) @@ -96,14 +85,16 @@ type FunOnMockChainResult a b = RawMockChainReturn a -> b -- | Building a `MockChainReturn` from a `RawMockChainReturn` unRawMockChainReturn :: FunOnMockChainResult a (MockChainReturn a) -unRawMockChainReturn (journal, (st, val)) = - MockChainReturn val (mcstOutputs st) (mcstToUtxoState st) journal +unRawMockChainReturn (journal, (chainIndex, (_emulatorState, val))) = + MockChainReturn val (chainIndexOutputs chainIndex) (chainIndexToUtxoState chainIndex) journal -- | Configuration from which to run a mockchain data MockChainConf a b where MockChainConf :: - { -- | The initial state from which to run the mockchain - mccInitialState :: MockChainState, + { -- | The initial emulator state from which to run the mockchain + mccInitialEmulatorState :: EmulatorState, + -- | The initial chain index from which to run the mockchain + mccInitialChainIndex :: ChainIndex, -- | The initial payments to issue in the run mccInitialDistribution :: InitialDistribution, -- | The function to apply on the results of the run @@ -111,55 +102,7 @@ data MockChainConf a b where } -> MockChainConf a b --- | The default `MockChainConf`, which uses the default initial state and +-- | The default `MockChainConf`, which uses the default initial states and -- initial distribution, and returns a refined `MockChainReturn` mockChainConfTemplate :: MockChainConf a (MockChainReturn a) -mockChainConfTemplate = MockChainConf def def unRawMockChainReturn - --- | The class of effects that represent a mockchain run -class RunnableMockChain effs where - -- | Runs a computation from an initial `MockChainState`, while returning a - -- list of `RawMockChainReturn` - runMockChain :: MockChainState -> Sem effs a -> [RawMockChainReturn a] - --- | Runs a `RunnableMockChain` from an initial `MockChainConf` -runMockChainFromConf :: - ( RunnableMockChain effs, - Member MockChainWrite effs - ) => - MockChainConf a b -> - Sem effs a -> - [b] -runMockChainFromConf (MockChainConf initState initDist funOnResult) currentRun = - fmap funOnResult $ - runMockChain initState $ - forceOutputs initDist >> currentRun - --- | Runs a `RunnableMockChain` from an initial distribution -runMockChainFromInitDist :: - ( RunnableMockChain effs, - Member MockChainWrite effs - ) => - InitialDistribution -> - Sem effs a -> - [MockChainReturn a] -runMockChainFromInitDist initDist = - runMockChainFromConf $ mockChainConfTemplate {mccInitialDistribution = initDist} - --- | Same as `runMockChainFromInitDist` using the `initialDistributionTemplate` -runMockChainFromInitDistTemplate :: - ( RunnableMockChain effs, - Member MockChainWrite effs - ) => - Sem effs a -> - [MockChainReturn a] -runMockChainFromInitDistTemplate = runMockChainFromInitDist initialDistributionTemplate - --- | Runs a `RunnableMockChain` from a default configuration -runMockChainDef :: - ( RunnableMockChain effs, - Member MockChainWrite effs - ) => - Sem effs a -> - [MockChainReturn a] -runMockChainDef = runMockChainFromConf mockChainConfTemplate +mockChainConfTemplate = MockChainConf def def def unRawMockChainReturn diff --git a/src/Cooked/MockChain/Effect/Log.hs b/src/Cooked/MockChain/Effect/Log.hs deleted file mode 100644 index 4c03d113d..000000000 --- a/src/Cooked/MockChain/Effect/Log.hs +++ /dev/null @@ -1,73 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - --- | This module exposes primitives required to log internal pieces of --- information during a mockchain run. This includes, in particular, all the --- adjustment automatically done by \cooked-validators\ during the transaction --- processing phase. This effect is typically not available to users, and should --- solely be used to track internal events. To trace additional elements from a --- user's perspective, use `Cooked.MockChain.Effect.Misc.note` instead. -module Cooked.MockChain.Effect.Log - ( -- * Logging events - MockChainLogEntry (..), - - -- * Logging effect - MockChainLog, - runMockChainLog, - - -- * Logging primitive - logEvent, - ) -where - -import Cooked.MockChain.Common -import Cooked.Skeleton -import Plutus.Script.Utils.Scripts qualified as Script -import PlutusLedgerApi.V3 qualified as Api -import Polysemy -import Polysemy.Writer - --- | Events logged when processing transaction skeletons -data MockChainLogEntry - = -- | Logging a Skeleton as it is submitted by the user. - MCLogSubmittedTxSkel TxSkel - | -- | Logging a Skeleton as it has been adjusted by the balancing mechanism, - -- alongside fee, and possible collateral utxos and return collateral user. - MCLogAdjustedTxSkel TxSkel Fee (Maybe Collaterals) - | -- | Logging the successful validation of a new transaction, with its id and - -- number of produced outputs. - MCLogNewTx Api.TxId Integer - | -- | Logging the fact that utxos provided by the user for balancing have to be - -- discarded for a specific reason. - MCLogDiscardedUtxos Integer String - | -- | Logging the fact that utxos provided as collaterals will not be used - -- because the transaction does not involve scripts. There are 2 cases, - -- depending on whether the user has provided an explicit user or a set of - -- utxos to be used as collaterals. - MCLogUnusedCollaterals (Either Peer CollateralIns) - | -- | Logging the automatic addition of a reference script - MCLogAddedReferenceScript TxSkelRedeemer Api.TxOutRef Script.ScriptHash - | -- | Logging the automatic addition of a withdrawal amount - MCLogAutoFilledWithdrawalAmount Api.Credential Api.Lovelace - | -- | Logging the automatic addition of the constitution script - MCLogAutoFilledConstitution Api.ScriptHash - | -- | Logging the automatic adjustment of a min ada amount - MCLogAdjustedTxSkelOut TxSkelOut Api.Lovelace - deriving (Show) - --- | An effect to allow logging of mockchain events -data MockChainLog :: Effect where - LogEvent :: MockChainLogEntry -> MockChainLog m () - -makeSem_ ''MockChainLog - --- | Interpreting a `MockChainLog` in terms of a writer of --- @[MockChainLogEntry]@ -runMockChainLog :: - (Member (Writer j) effs) => - (MockChainLogEntry -> j) -> - Sem (MockChainLog : effs) a -> - Sem effs a -runMockChainLog inject = interpret $ \(LogEvent event) -> tell $ inject event - --- | Logs an internal event occurring while processing a transaction skeleton -logEvent :: (Member MockChainLog effs) => MockChainLogEntry -> Sem effs () diff --git a/src/Cooked/MockChain/Effect/Misc.hs b/src/Cooked/MockChain/Effect/Misc.hs deleted file mode 100644 index fe2407c93..000000000 --- a/src/Cooked/MockChain/Effect/Misc.hs +++ /dev/null @@ -1,91 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - --- | This module defines primitives that offer quality of life features when --- operating a mockchain without interacting with the mockchain state itself. -module Cooked.MockChain.Effect.Misc - ( -- * Misc effect - MockChainMisc (..), - runMockChainMisc, - - -- * Storing aliases for hashable elements - define, - defineM, - - -- * Taking notes in the notebook - note, - noteP, - noteL, - noteW, - noteS, - - -- * Asserting properties - assert, - assert', - ) -where - -import Cooked.Pretty.Class -import Cooked.Pretty.Hashable -import Cooked.Pretty.Options -import PlutusLedgerApi.V3 qualified as Api -import Polysemy -import Polysemy.Writer -import Prettyprinter qualified as PP - --- | An effect that corresponds to extra QOL capabilities of the MockChain -data MockChainMisc :: Effect where - Define :: (ToHash a) => String -> a -> MockChainMisc m a - Note :: (PrettyCookedOpts -> DocCooked) -> MockChainMisc m () - Assert :: String -> Bool -> MockChainMisc m () - -makeSem_ ''MockChainMisc - --- | Interpreting a `MockChainMisc` in terms of a writer of @Map --- BuiltinByteString String@ -runMockChainMisc :: - forall effs a j. - (Member (Writer j) effs) => - (String -> Api.BuiltinByteString -> j) -> - ((PrettyCookedOpts -> DocCooked) -> j) -> - (String -> Bool -> j) -> - Sem (MockChainMisc : effs) a -> - Sem effs a -runMockChainMisc injectAlias injectNote injectPred = interpret $ \case - (Define name hashable) -> tell (injectAlias name $ toHash hashable) >> return hashable - (Note s) -> tell $ injectNote s - (Assert s b) -> tell $ injectPred s b - --- | Stores an alias matching a hashable data for pretty printing purpose -define :: forall effs a. (Member MockChainMisc effs, ToHash a) => String -> a -> Sem effs a - --- | Like `define`, but binds the result of a monadic computation instead -defineM :: (Member MockChainMisc effs, ToHash a) => String -> Sem effs a -> Sem effs a -defineM name = (define name =<<) - --- | Takes note of an element represented as its rendering function to trace at --- the end of the run -note :: forall effs. (Member MockChainMisc effs) => (PrettyCookedOpts -> DocCooked) -> Sem effs () - --- | Takes note of a pretty-printable element to trace at the end of the run -noteP :: forall effs s. (Member MockChainMisc effs, PrettyCooked s) => s -> Sem effs () -noteP doc = note (`prettyCookedOpt` doc) - --- | Takes note of a pretty-printable element as list with a title, to trace at --- the end of the run -noteL :: forall effs l. (Member MockChainMisc effs, PrettyCookedList l) => String -> l -> Sem effs () -noteL title docs = note $ \opts -> prettyItemize opts (prettyCooked title) "-" docs - --- | Takes note of a showable element to trace at the end of the run -noteW :: forall effs s. (Member MockChainMisc effs, Show s) => s -> Sem effs () -noteW = note . const . PP.viaShow - --- | Takes note of a String to trace at the end of the run -noteS :: forall effs. (Member MockChainMisc effs) => String -> Sem effs () -noteS = noteP - --- | Ensures a specific property holds, sending the provided error message otherwise -assert :: forall effs. (Member MockChainMisc effs) => String -> Bool -> Sem effs () - --- | Ensures a specific property holds, with a default error message otherwise -assert' :: forall effs. (Member MockChainMisc effs) => Bool -> Sem effs () -assert' = assert "Assertion" diff --git a/src/Cooked/MockChain/Effect/Read.hs b/src/Cooked/MockChain/Effect/Read.hs deleted file mode 100644 index d73d3a6d2..000000000 --- a/src/Cooked/MockChain/Effect/Read.hs +++ /dev/null @@ -1,404 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - --- | This module exposes primitives to query the current state of the --- blockchain. -module Cooked.MockChain.Effect.Read - ( -- * The `MockChainRead` effect - MockChainRead, - runMockChainRead, - - -- * Queries related to protocol parameters - getParams, - govActionDeposit, - dRepDeposit, - stakeAddressDeposit, - stakePoolDeposit, - - -- * Queries related to `Cooked.Skeleton.TxSkel` - txSkelDepositedValueInCertificates, - txSkelDepositedValueInProposals, - txSkelAllScripts, - txSkelInputScripts, - txSkelInputValue, - - -- * Queries related to timing - currentSlot, - currentMSRange, - getEnclosingSlot, - slotRangeBefore, - slotRangeAfter, - slotToMSRange, - - -- * Queries related to fetching UTxOs - allUtxos, - utxosAt, - txSkelOutByRef, - utxosFromCardanoTx, - lookupUtxos, - previewByRef, - viewByRef, - - -- * Other queries - getConstitutionScript, - getCurrentReward, - ) -where - -import Cardano.Api qualified as Cardano -import Cardano.Ledger.Conway.Core qualified as Conway -import Cardano.Node.Emulator.Internal.Node qualified as Emulator -import Control.Lens qualified as Lens -import Control.Monad -import Cooked.MockChain.Automation.GenerateTx.Credential (toStakeCredential) -import Cooked.MockChain.Common -import Cooked.MockChain.Runtime.Error -import Cooked.MockChain.Runtime.State -import Cooked.Skeleton -import Data.Coerce (coerce) -import Data.Map (Map) -import Data.Map qualified as Map -import Data.Maybe -import Ledger.Slot qualified as P.Ledger -import Ledger.Tx qualified as P.Ledger -import Ledger.Tx.CardanoAPI qualified as P.Ledger -import Optics.Core -import Plutus.Script.Utils.Address qualified as Script -import PlutusLedgerApi.V3 qualified as Api -import Polysemy -import Polysemy.Error -import Polysemy.Fail -import Polysemy.State - --- | An effect that offers primitives to query the current state of the --- mockchain. As its name suggests, this effect is read-only and does not alter --- the state in any way. -data MockChainRead :: Effect where - GetParams :: MockChainRead m Emulator.Params - TxSkelOutByRef :: Api.TxOutRef -> MockChainRead m TxSkelOut - CurrentSlot :: MockChainRead m P.Ledger.Slot - AllUtxos :: MockChainRead m Utxos - UtxosAt :: (Script.ToCredential a) => a -> MockChainRead m Utxos - GetConstitutionScript :: MockChainRead m (Maybe VScript) - GetCurrentReward :: (Script.ToCredential c) => c -> MockChainRead m (Maybe Api.Lovelace) - -makeSem_ ''MockChainRead - --- | The interpretation for read-only effect in the blockchain state -runMockChainRead :: - forall effs a. - ( Members - '[ State MockChainState, - Error P.Ledger.ToCardanoError, - Error MockChainError - ] - effs - ) => - Sem (MockChainRead : effs) a -> - Sem effs a -runMockChainRead = interpret $ \case - GetParams -> gets mcstParams - TxSkelOutByRef oRef -> do - res <- gets $ Map.lookup oRef . mcstOutputs - case res of - Just (txSkelOut, True) -> return txSkelOut - _ -> throw $ MCEUnknownOutRef oRef - AllUtxos -> fetchUtxos $ const True - UtxosAt (Script.toCredential -> cred) -> fetchUtxos $ (== cred) . Script.toCredential - CurrentSlot -> gets $ view $ mcstLedgerStateL % to Emulator.getSlot - GetConstitutionScript -> gets $ view mcstConstitutionL - GetCurrentReward (Script.toCredential -> cred) -> do - stakeCredential <- toStakeCredential cred - gets $ - preview $ - mcstLedgerStateL - % to (Emulator.getReward stakeCredential) - % _Just - % to coerce - where - fetchUtxos decide = - gets $ - toListOf $ - mcstOutputsL - % to Map.toList - % traversed - % filtered (snd . snd) - % filtered (decide . fst . snd) - % to (fmap fst) - --- | Returns the emulator parameters, including protocol parameters -getParams :: - (Member MockChainRead effs) => - Sem effs Emulator.Params - --- | Retrieves the required governance action deposit amount -govActionDeposit :: - (Member MockChainRead effs) => - Sem effs Api.Lovelace -govActionDeposit = - getParams - <&> Api.Lovelace - . Cardano.unCoin - . Lens.view Conway.ppGovActionDepositL - . Emulator.emulatorPParams - --- | Retrieves the required drep deposit amount -dRepDeposit :: - (Member MockChainRead effs) => - Sem effs Api.Lovelace -dRepDeposit = - getParams - <&> Api.Lovelace - . Cardano.unCoin - . Lens.view Conway.ppDRepDepositL - . Emulator.emulatorPParams - --- | Retrieves the required stake address deposit amount -stakeAddressDeposit :: - (Member MockChainRead effs) => - Sem effs Api.Lovelace -stakeAddressDeposit = - getParams - <&> Api.Lovelace - . Cardano.unCoin - . Lens.view Conway.ppKeyDepositL - . Emulator.emulatorPParams - --- | Retrieves the required stake pool deposit amount -stakePoolDeposit :: - (Member MockChainRead effs) => - Sem effs Api.Lovelace -stakePoolDeposit = - getParams - <&> Api.Lovelace - . Cardano.unCoin - . Lens.view Conway.ppPoolDepositL - . Emulator.emulatorPParams - --- | Retrieves the total amount of lovelace deposited in certificates in this --- skeleton. Note that unregistering a staking address or a dRep lead to a --- negative deposit (a withdrawal, in fact) which means this function can return --- a negative amount of lovelace, which is intended. The deposited amounts are --- dictated by the current protocol parameters, and computed as such. -txSkelDepositedValueInCertificates :: - (Member MockChainRead effs) => - TxSkel -> - Sem effs Api.Lovelace -txSkelDepositedValueInCertificates txSkel = do - sDep <- stakeAddressDeposit - dDep <- dRepDeposit - pDep <- stakePoolDeposit - return $ - foldOf - ( txSkelCertificatesL - % traversed - % to - ( \case - TxSkelCertificate _ StakingRegister {} -> sDep - TxSkelCertificate _ StakingRegisterDelegate {} -> sDep - TxSkelCertificate _ StakingUnRegister {} -> -sDep - TxSkelCertificate _ DRepRegister {} -> dDep - TxSkelCertificate _ DRepUnRegister {} -> -dDep - TxSkelCertificate _ PoolRegister {} -> pDep - -- There is no special case for 'PoolRetire' because the deposit - -- is given back to the reward account. - _ -> Api.Lovelace 0 - ) - ) - txSkel - --- | Retrieves the total amount of lovelace deposited in proposals in this --- skeleton (equal to `govActionDeposit` times the number of proposals) -txSkelDepositedValueInProposals :: - (Member MockChainRead effs) => - TxSkel -> - Sem effs Api.Lovelace -txSkelDepositedValueInProposals TxSkel {txSkelProposals} = - govActionDeposit - <&> Api.Lovelace - . (toInteger (length txSkelProposals) *) - . Api.getLovelace - --- | Returns all scripts involved in this 'TxSkel' -txSkelAllScripts :: - (Member MockChainRead effs) => - TxSkel -> - Sem effs [VScript] -txSkelAllScripts txSkel = do - txSkelSpendingScripts <- txSkelInputScripts txSkel - return $ - toListOf (txSkelRedeemedScriptsT % userVScriptL) txSkel - <> txSkelSpendingScripts - --- | Returns all scripts which guard transaction inputs -txSkelInputScripts :: - (Member MockChainRead effs) => - TxSkel -> - Sem effs [VScript] -txSkelInputScripts = - fmap catMaybes - . mapM (previewByRef (txSkelOutOwnerL % userVScriptAT)) - . Map.keys - . txSkelInputs - --- | look up the UTxOs the transaction consumes, and sum their values. -txSkelInputValue :: - (Member MockChainRead effs) => - TxSkel -> - Sem effs Api.Value -txSkelInputValue = - fmap mconcat - . mapM (viewByRef txSkelOutValueL) - . Map.keys - . txSkelInputs - --- | Returns the current slot -currentSlot :: - (Member MockChainRead effs) => - Sem effs P.Ledger.Slot - --- | Returns the closed ms interval corresponding to the current slot -currentMSRange :: - (Members '[MockChainRead, Fail] effs) => - Sem effs (Api.POSIXTime, Api.POSIXTime) -currentMSRange = slotToMSRange =<< currentSlot - --- | Return the slot that contains the given time. See 'slotToMSRange' for --- some satisfied equational properties. -getEnclosingSlot :: - (Member MockChainRead effs) => - Api.POSIXTime -> - Sem effs P.Ledger.Slot -getEnclosingSlot t = - getParams - <&> (`Emulator.posixTimeToEnclosingSlot` t) - . Emulator.pSlotConfig - --- | The infinite range of slots ending before or at the given time -slotRangeBefore :: - (Members '[MockChainRead, Fail] effs) => - Api.POSIXTime -> - Sem effs P.Ledger.SlotRange -slotRangeBefore t = do - n <- getEnclosingSlot t - (_, b) <- slotToMSRange n - -- If the given time @t@ happens to be the last ms of its slot, we can include - -- the whole slot. Otherwise, the only way to be sure that the returned slot - -- range contains no time after @t@ is to go to the preceding slot. - return $ Api.to $ if t == b then n else n - 1 - --- | The infinite range of slots starting after or at the given time -slotRangeAfter :: - (Members '[MockChainRead, Fail] effs) => - Api.POSIXTime -> - Sem effs P.Ledger.SlotRange -slotRangeAfter t = do - n <- getEnclosingSlot t - (a, _) <- slotToMSRange n - return $ Api.from $ if t == a then n else n + 1 - --- | Returns the closed ms interval corresponding to the slot with the given --- number. It holds that --- --- > slotToMSRange (getEnclosingSlot t) == (a, b) ==> a <= t <= b --- --- and --- --- > slotToMSRange n == (a, b) ==> getEnclosingSlot a == n && getEnclosingSlot b == n --- --- and --- --- > slotToMSRange n == (a, b) ==> getEnclosingSlot (a-1) == n-1 && getEnclosingSlot (b+1) == n+1 -slotToMSRange :: - ( Members '[MockChainRead, Fail] effs, - Integral i - ) => - i -> - Sem effs (Api.POSIXTime, Api.POSIXTime) -slotToMSRange (fromIntegral -> slot) = do - slotConfig <- Emulator.pSlotConfig <$> getParams - case Emulator.slotToPOSIXTimeRange slotConfig slot of - Api.Interval - (Api.LowerBound (Api.Finite l) leftclosed) - (Api.UpperBound (Api.Finite r) rightclosed) -> - return - ( if leftclosed then l else l + 1, - if rightclosed then r else r - 1 - ) - _ -> fail "Unexpected unbounded slot: please report a bug at https://github.com/tweag/cooked-validators/issues" - --- | Returns a list of all currently known outputs -allUtxos :: - (Member MockChainRead effs) => - Sem effs Utxos - --- | Returns a list of all UTxOs at a certain address. -utxosAt :: - ( Member MockChainRead effs, - Script.ToCredential cred - ) => - cred -> - Sem effs Utxos - --- | Returns an output given a reference to it -txSkelOutByRef :: - (Member MockChainRead effs) => - Api.TxOutRef -> - Sem effs TxSkelOut - --- | Retrieves the ordered list of outputs of the given "CardanoTx". --- --- This is useful when writing endpoints and/or traces to fetch utxos of --- interest right from the start and avoid querying the chain for them --- afterwards using 'allUtxos' or similar functions. -utxosFromCardanoTx :: - (Member MockChainRead effs) => - P.Ledger.CardanoTx -> - Sem effs [(Api.TxOutRef, TxSkelOut)] -utxosFromCardanoTx = - mapM (\txOutRef -> (txOutRef,) <$> txSkelOutByRef txOutRef) - . fmap (P.Ledger.fromCardanoTxIn . snd) - . P.Ledger.getCardanoTxOutRefs - --- | Go through all of the 'Api.TxOutRef's in the list and look them up in the --- state of the blockchain, throwing an error if one of them cannot be resolved. -lookupUtxos :: - (Member MockChainRead effs) => - [Api.TxOutRef] -> - Sem effs (Map Api.TxOutRef TxSkelOut) -lookupUtxos = - foldM - (\m oRef -> flip (Map.insert oRef) m <$> txSkelOutByRef oRef) - Map.empty - --- | Retrieves an output and views a specific element out of it -viewByRef :: - ( Member MockChainRead effs, - Is g A_Getter - ) => - Optic' g is TxSkelOut c -> - Api.TxOutRef -> - Sem effs c -viewByRef optic = (view optic <$>) . txSkelOutByRef - --- | Retrieves an output and previews a specific element out of it -previewByRef :: - ( Member MockChainRead effs, - Is af An_AffineFold - ) => - Optic' af is TxSkelOut c -> - Api.TxOutRef -> - Sem effs (Maybe c) -previewByRef optic = (preview optic <$>) . txSkelOutByRef - --- | Gets the current official constitution script -getConstitutionScript :: - (Member MockChainRead effs) => - Sem effs (Maybe VScript) - --- | Gets the current reward associated with a credential -getCurrentReward :: - ( Member MockChainRead effs, - Script.ToCredential c - ) => - c -> - Sem effs (Maybe Api.Lovelace) diff --git a/src/Cooked/MockChain/Effect/Write.hs b/src/Cooked/MockChain/Effect/Write.hs deleted file mode 100644 index 0cbdd3c7d..000000000 --- a/src/Cooked/MockChain/Effect/Write.hs +++ /dev/null @@ -1,294 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - --- | This module exposes primitives to update the current state of the --- blockchain, including by sending transactions for validation. -module Cooked.MockChain.Effect.Write - ( -- * The `MockChainWrite` effect - MockChainWrite (..), - runMockChainWrite, - - -- * Modifications of the current time - waitNSlots, - awaitSlot, - awaitEnclosingSlot, - waitNMSFromSlotLowerBound, - waitNMSFromSlotUpperBound, - - -- * Sending `Cooked.Skeleton.TxSkel`s for validation - validateTxSkel, - validateTxSkel', - validateTxSkel_, - - -- * Other operations - setParams, - setConstitutionScript, - forceOutputs, - forceOutputs_, - ) -where - -import Cardano.Api qualified as Cardano -import Cardano.Api.Ledger qualified as Cardano -import Cardano.Node.Emulator.Internal.Node qualified as Emulator -import Control.Lens qualified as Lens -import Control.Monad -import Cooked.MockChain.Automation.AutoFilling.Constitution -import Cooked.MockChain.Automation.AutoFilling.MinAda -import Cooked.MockChain.Automation.AutoFilling.ReferenceScripts -import Cooked.MockChain.Automation.AutoFilling.Withdrawals -import Cooked.MockChain.Automation.Balancing -import Cooked.MockChain.Automation.GenerateTx.Body -import Cooked.MockChain.Automation.GenerateTx.Output -import Cooked.MockChain.Common -import Cooked.MockChain.Effect.Log -import Cooked.MockChain.Effect.Read -import Cooked.MockChain.Runtime.Error -import Cooked.MockChain.Runtime.State -import Cooked.Skeleton -import Cooked.Tweak.Common -import Cooked.Tweak.Query -import Data.Map.Strict qualified as Map -import Ledger.Index qualified as P.Ledger -import Ledger.Orphans () -import Ledger.Slot qualified as P.Ledger -import Ledger.Tx qualified as P.Ledger -import Ledger.Tx.CardanoAPI qualified as P.Ledger -import Optics.Core -import Plutus.Script.Utils.Scripts qualified as Script -import PlutusLedgerApi.V3 qualified as Api -import Polysemy -import Polysemy.Error -import Polysemy.Fail -import Polysemy.State - --- | An effect that offers all the primitives that are performing modifications --- on the blockchain state. -data MockChainWrite :: Effect where - WaitNSlots :: Integer -> MockChainWrite m P.Ledger.Slot - SetParams :: Emulator.Params -> MockChainWrite m () - ValidateTxSkel :: TxSkel -> MockChainWrite m (P.Ledger.CardanoTx, Utxos) - SetConstitutionScript :: (ToVScript s) => s -> MockChainWrite m () - ForceOutputs :: [TxSkelOut] -> MockChainWrite m Utxos - -makeSem_ ''MockChainWrite - --- | Interprets the `MockChainWrite` effect -runMockChainWrite :: - forall effs a. - ( Members - '[ State MockChainState, - Error P.Ledger.ToCardanoError, - Error MockChainError, - MockChainLog, - MockChainRead, - Fail - ] - effs - ) => - Sem (MockChainWrite : effs) a -> - Sem effs a -runMockChainWrite = interpret $ \case - SetParams params -> do - modify $ set mcstParamsL params - modify $ over mcstLedgerStateL $ Emulator.updateStateParams params - WaitNSlots n -> do - cs <- gets (Emulator.getSlot . mcstLedgerState) - if - | n == 0 -> return cs - | n > 0 -> do - let newSlot = cs + fromIntegral n - modify' (over mcstLedgerStateL $ Lens.set Emulator.elsSlotL $ fromIntegral newSlot) - return newSlot - | otherwise -> throw $ MCEPastSlot cs (cs + fromIntegral n) - SetConstitutionScript (toVScript -> cScript) -> do - modify' (mcstConstitutionL ?~ cScript) - modify' $ - over mcstLedgerStateL $ - Lens.set Emulator.elsConstitutionScriptL $ - (Cardano.SJust . Cardano.toShelleyScriptHash . Script.toCardanoScriptHash) - cScript - ForceOutputs outputs -> do - -- We retrieve the protocol parameters - params <- getParams - -- The emulator takes for granted transactions with a single pseudo input, - -- which we build to force transaction validation - let input = - ( Cardano.genesisUTxOPseudoTxIn (Emulator.pNetworkId params) $ - Cardano.GenesisUTxOKeyHash $ - Cardano.KeyHash "23d51e91ae5adc7ae801e9de4cd54175fb7464ec2680b25686bbb194", - Cardano.BuildTxWith $ Cardano.KeyWitness Cardano.KeyWitnessForSpending - ) - -- We adjust the outputs for the minimal required ADA if needed - outputsMinAda <- mapM toTxSkelOutWithMinAda outputs - -- We transform these outputs to Cardano outputs - outputs' <- mapM toCardanoTxOut outputsMinAda - -- We create our transaction body, which only consists of the dummy input - -- and the outputs to force, and make a transaction out of it. - cardanoTx <- - P.Ledger.CardanoEmulatorEraTx . txSignatoriesAndBodyToCardanoTx [] - <$> fromEither - ( Emulator.createTransactionBody params $ - P.Ledger.CardanoBuildTx - ( P.Ledger.emptyTxBodyContent - { Cardano.txOuts = outputs', - Cardano.txIns = [input] - } - ) - ) - -- We need to adjust our internal state to account for the forced - -- transaction. We begin by computing the new map of outputs. - let outputsMap = - Map.fromList $ - zipWith - (\x y -> (x, (y, True))) - (P.Ledger.fromCardanoTxIn . snd <$> P.Ledger.getCardanoTxOutRefs cardanoTx) - outputsMinAda - -- We update the index, which effectively receives the new utxos - modify' - ( over mcstLedgerStateL $ - Lens.over - Emulator.elsUtxoL - ( P.Ledger.fromPlutusIndex - . P.Ledger.insert cardanoTx - . P.Ledger.toPlutusIndex - ) - ) - -- We update our internal map by adding the new outputs - modify' (over mcstOutputsL (<> outputsMap)) - -- Finally, we return the created utxos - return $ Map.toList (fst <$> outputsMap) - ValidateTxSkel skel -> fmap snd $ runTweak skel $ do - -- We retrieve the current skeleton options - TxSkelOpts {..} <- viewTweak txSkelOptsL - -- We log the submission of the new skeleton - viewTweak simple >>= logEvent . MCLogSubmittedTxSkel - -- We retrieve the current parameters - oldParams <- getParams - -- We compute the optionally modified parameters - let newParams = txSkelOptModParams oldParams - -- We change the parameters for the duration of the validation process - modify $ set mcstParamsL newParams - modify $ over mcstLedgerStateL $ Emulator.updateStateParams newParams - -- We ensure that the outputs have the required minimal amount of ada, when - -- requested in the skeleton options - autoFillMinAda - -- We retrieve the official constitution script and attach it to each - -- proposal that requires it, if it's not empty - autoFillConstitution - -- We add reference scripts in the various redeemers of the skeleton, when - -- they can be found in the index and are allowed to be auto filled - autoFillReferenceScripts - -- We attach the reward amount to withdrawals when applicable - autoFillWithdrawalAmounts - -- We balance the skeleton when requested in the skeleton option, and get - -- the associated fee, collateral inputs and return collateral user - ExtendedTxSkel finalTxSkel fee mCollaterals body <- viewTweak simple >>= balanceTxSkel - -- We log the adjusted skeleton - logEvent $ MCLogAdjustedTxSkel finalTxSkel fee mCollaterals - -- We generate the transaction asscoiated with the skeleton, and apply on it - -- the modifications from the skeleton options - signatories <- viewTweak txSkelSignatoriesL - let cardanoTx = P.Ledger.CardanoEmulatorEraTx $ txSkelOptModTx $ txSignatoriesAndBodyToCardanoTx signatories body - -- To run transaction validation we need a minimal ledger state - eLedgerState <- gets mcstLedgerState - -- We finally run the emulated validation. We update our internal state - -- based on the validation result, and throw an error if this fails. If at - -- some point we want to allows mockchain runs with validation errors, the - -- caller will need to catch those errors and do something with them. - newOutputs <- case Emulator.validateCardanoTx newParams eLedgerState cardanoTx of - -- In case of a phase 1 error, we give back the same index - (_, P.Ledger.FailPhase1 _ err) -> throw $ MCEValidationError P.Ledger.Phase1 err - (newELedgerState, P.Ledger.FailPhase2 _ err _) | Just (colInputs, mRetColOutput) <- mCollaterals -> do - -- We update the emulated ledger state - modify' (set mcstLedgerStateL newELedgerState) - -- We remove the collateral utxos from our own stored outputs - forM_ colInputs $ modify' . removeOutput - -- We add the returned collateral to our outputs when it exists - case (mRetColOutput, Map.toList $ P.Ledger.getCardanoTxProducedReturnCollateral cardanoTx) of - (Nothing, []) -> return () - (Just retColOutput, [(txIn, _)]) -> modify' $ addOutput (P.Ledger.fromCardanoTxIn txIn) retColOutput - _ -> fail "Unreachable case when processing return collaterals, please report a bug at https://github.com/tweag/cooked-validators/issues" - -- We throw a mockchain error - throw $ MCEValidationError P.Ledger.Phase2 err - -- In case of success, we update the index with all inputs and outputs - -- contained in the transaction - (newELedgerState, P.Ledger.Success {}) -> do - -- We update the index with the utxos consumed and produced by the tx - modify' (set mcstLedgerStateL newELedgerState) - -- We retrieve the utxos created by the transaction - let utxos = P.Ledger.fromCardanoTxIn . snd <$> P.Ledger.getCardanoTxOutRefs cardanoTx - -- We combine them with their corresponding `TxSkelOut` - let newOutputs = zip utxos (txSkelOutputs finalTxSkel) - -- We add the news utxos to the state - forM_ newOutputs $ modify' . uncurry addOutput - -- And remove the old ones - forM_ (Map.toList $ txSkelInputs finalTxSkel) $ modify' . removeOutput . fst - -- We return the newly created outputs - return newOutputs - -- This is a theoretical unreachable case. Since we fail in Phase 2, it - -- means the transaction involved script, and thus we must have generated - -- collaterals. - (_, P.Ledger.FailPhase2 {}) - | Nothing <- mCollaterals -> - fail "Unreachable case when processing validation result, please report a bug at https://github.com/tweag/cooked-validators/issues" - -- We apply a change of slot when requested in the options - when txSkelOptAutoSlotIncrease $ modify' (over mcstLedgerStateL Emulator.nextSlot) - -- We return the parameters to their original state - modify $ set mcstParamsL oldParams - modify $ over mcstLedgerStateL $ Emulator.updateStateParams oldParams - -- We log the validated transaction - logEvent $ MCLogNewTx (P.Ledger.fromCardanoTxId $ P.Ledger.getCardanoTxId cardanoTx) (fromIntegral $ length $ P.Ledger.getCardanoTxOutRefs cardanoTx) - -- We return the validated transaction - return (cardanoTx, newOutputs) - --- | Waits a certain number of slots and returns the new slot -waitNSlots :: (Member MockChainWrite effs) => Integer -> Sem effs P.Ledger.Slot - --- | Wait for a certain slot, or throws an error if the slot is already past -awaitSlot :: (Members '[MockChainRead, MockChainWrite] effs) => P.Ledger.Slot -> Sem effs P.Ledger.Slot -awaitSlot (P.Ledger.Slot targetSlot) = do - P.Ledger.Slot now <- currentSlot - waitNSlots (targetSlot - now) - --- | Waits until the current slot becomes greater or equal to the slot --- containing the given POSIX time. Note that that it might not wait for --- anything if the current slot is large enough. -awaitEnclosingSlot :: (Members '[MockChainRead, MockChainWrite] effs) => Api.POSIXTime -> Sem effs P.Ledger.Slot -awaitEnclosingSlot time = getEnclosingSlot time >>= awaitSlot - --- | Wait a given number of ms from the lower bound of the current slot and --- returns the current slot after waiting. -waitNMSFromSlotLowerBound :: (Members '[MockChainRead, MockChainWrite, Fail] effs) => Integer -> Sem effs P.Ledger.Slot -waitNMSFromSlotLowerBound duration = currentMSRange >>= awaitEnclosingSlot . (+ fromIntegral duration) . fst - --- | Wait a given number of ms from the upper bound of the current slot and --- returns the current slot after waiting. -waitNMSFromSlotUpperBound :: (Members '[MockChainRead, MockChainWrite, Fail] effs) => Integer -> Sem effs P.Ledger.Slot -waitNMSFromSlotUpperBound duration = currentMSRange >>= awaitEnclosingSlot . (+ fromIntegral duration) . snd - --- | Generates, balances and validates a transaction from a skeleton, and --- returns the validated transaction, alongside the created UTxOs. -validateTxSkel :: (Member MockChainWrite effs) => TxSkel -> Sem effs (P.Ledger.CardanoTx, Utxos) - --- | Same as `validateTxSkel`, but only returns the generated UTxOs -validateTxSkel' :: (Members '[MockChainRead, MockChainWrite] effs) => TxSkel -> Sem effs Utxos -validateTxSkel' = fmap snd . validateTxSkel - --- | Same as `validateTxSkel`, but discards the returned transaction -validateTxSkel_ :: (Member MockChainWrite effs) => TxSkel -> Sem effs () -validateTxSkel_ = void . validateTxSkel - --- | Updates the current parameters -setParams :: (Member MockChainWrite effs) => Emulator.Params -> Sem effs () - --- | Sets the current script to act as the official constitution script -setConstitutionScript :: (Member MockChainWrite effs, ToVScript s) => s -> Sem effs () - --- | Forces the generation of utxos corresponding to certain --- `TxSkelOut`. Returns the created UTxOs, which might differ from the original --- list if some min ADA adjustment occurred. -forceOutputs :: (Member MockChainWrite effs) => [TxSkelOut] -> Sem effs Utxos - --- | Same as `forceOutputs`, but discards the returned outputs -forceOutputs_ :: (Member MockChainWrite effs) => [TxSkelOut] -> Sem effs () -forceOutputs_ = void . forceOutputs diff --git a/src/Cooked/MockChain/Run/Instances.hs b/src/Cooked/MockChain/Instances.hs similarity index 51% rename from src/Cooked/MockChain/Run/Instances.hs rename to src/Cooked/MockChain/Instances.hs index 2fad07196..7d353cc3a 100644 --- a/src/Cooked/MockChain/Run/Instances.hs +++ b/src/Cooked/MockChain/Instances.hs @@ -21,42 +21,46 @@ -- including intermediate hidden in the other instances. This should only be -- used when explicitly executing internal primitives of cooked, such as -- balancing, is required. -module Cooked.MockChain.Run.Instances +module Cooked.MockChain.Instances ( -- * Direct, simple mockchain instance - DirectEffs, + DirectMockChainEffs, DirectMockChain, -- * Staged mockchain instance with all effects FullTweakEffs, FullTweak, - FullEffs, + FullMockChainEffs, FullMockChain, -- * Staged mockchain instance with minimal effects StagedTweakEffs, StagedTweak, - StagedEffs, + StagedMockChainEffs, StagedMockChain, -- * Staged mockchain instance with minimal effects and a custom effect InterpretAlone (..), ExtendedStagedTweakEffs, ExtendedStagedTweak, - ExtendedStagedEffs, + ExtendedStagedMockChainEffs, ExtendedStagedMockChain, ) where -import Cooked.Ltl -import Cooked.MockChain.Effect.Log -import Cooked.MockChain.Effect.Misc -import Cooked.MockChain.Effect.Read -import Cooked.MockChain.Effect.Write -import Cooked.MockChain.Run.Runnable -import Cooked.MockChain.Run.Tweak -import Cooked.MockChain.Runtime.Error -import Cooked.MockChain.Runtime.Journal -import Cooked.MockChain.Runtime.State +import Cooked.Effect.Log +import Cooked.Effect.Misc +import Cooked.Effect.Override +import Cooked.Effect.Params +import Cooked.Effect.Query +import Cooked.Effect.Submission +import Cooked.Effect.Time +import Cooked.Effect.Validation +import Cooked.MockChain.Ltl +import Cooked.MockChain.Run +import Cooked.MockChain.Tweak +import Cooked.Runtime.Error +import Cooked.Runtime.Journal +import Cooked.Runtime.State import Ledger.Tx qualified as P.Ledger import Polysemy import Polysemy.Bundle @@ -67,155 +71,200 @@ import Polysemy.State import Polysemy.Writer -- | The most direct stack of effects to run a mockchain -type DirectEffs = - '[ MockChainWrite, - MockChainRead, - MockChainMisc, +type DirectMockChainEffs = + '[ Validate, + Override, + Query, + Time, + Misc, Fail ] --- | A mockchain computation built on top of the `DirectEffs` stack of effects -type DirectMockChain a = Sem DirectEffs a +-- | A mockchain computation built on top of the `DirectMockChainEffs` stack of +-- effects +type DirectMockChain a = Sem DirectMockChainEffs a -instance RunnableMockChain DirectEffs where - runMockChain mcst = +instance RunnableMockChain DirectMockChainEffs where + runMockChain emInit ciInit = (: []) . run . runWriter - . runMockChainLog fromLogEntry - . runState mcst + . runMockChainLog + . runState ciInit + . runState emInit . runError - . runToCardanoErrorInMockChainError - . runFailInMockChainError - . runMockChainMisc fromAlias fromNote fromAssert - . runMockChainRead - . runMockChainWrite + . mapError CEToCardanoError + . failToError CEFailure + . runMockChainMisc + . runMockChainParams + . runMockChainTime + . runMockChainQuery + . runMockChainOverride + . runMockChainSubmit + . runChainValidate + . insertAt @1 + @'[ Submit + ] + . insertAt @7 + @'[ Error P.Ledger.ToCardanoError, + Error ChainError, + State EmulatorState, + State ChainIndex, + Log, + Writer ChainJournal + ] . insertAt @4 - @[ Error P.Ledger.ToCardanoError, - Error MockChainError, - State MockChainState, - MockChainLog, - Writer MockChainJournal - ] + @'[ Params + ] -- | A stack of effects aimed at being used as modifications for a -- `FullMockChain` computation type FullTweakEffs = - '[ MockChainMisc, - MockChainRead, + '[ Misc, + Query, + Time, + Params, Fail, Error P.Ledger.ToCardanoError, - Error MockChainError, - State MockChainState, - MockChainLog, - Writer MockChainJournal + Error ChainError, + State EmulatorState, + State ChainIndex, + Log, + Writer ChainJournal ] -- | A tweak computation based on the `FullTweakEffs` stack of effects type FullTweak a = TypedTweak FullTweakEffs a --- | A stack of effects which allows everything allowed by `StagedEffs` with the +-- | A stack of effects which allows everything allowed by `StagedMockChainEffs` with the -- addition of all the lower level effects required to interpret it. -type FullEffs = +type FullMockChainEffs = '[ ModifyGlobally (UntypedTweak FullTweakEffs), - MockChainWrite, + Validate, + Override, ModifyLocally (UntypedTweak FullTweakEffs), State [Ltl (UntypedTweak FullTweakEffs)], - MockChainMisc, - MockChainRead, + Misc, + Query, + Time, + Params, Fail, Error P.Ledger.ToCardanoError, - Error MockChainError, - State MockChainState, - MockChainLog, - Writer MockChainJournal, + Error ChainError, + State EmulatorState, + State ChainIndex, + Log, + Writer ChainJournal, NonDet ] --- | A mockchain computation built on top of the `FullEffs` stack of effects -type FullMockChain a = Sem FullEffs a +-- | A mockchain computation built on top of the `FullMockChainEffs` stack of effects +type FullMockChain a = Sem FullMockChainEffs a -instance RunnableMockChain FullEffs where - runMockChain mcst = +instance RunnableMockChain FullMockChainEffs where + runMockChain emInit ciInit = run . runNonDet . runWriter - . runMockChainLog fromLogEntry - . runState mcst + . runMockChainLog + . runState ciInit + . runState emInit . runError - . runToCardanoErrorInMockChainError - . runFailInMockChainError - . runMockChainRead - . runMockChainMisc fromAlias fromNote fromAssert + . mapError CEToCardanoError + . failToError CEFailure + . runMockChainParams + . runMockChainTime + . runMockChainQuery + . runMockChainMisc . evalState [] . runModifyLocally - . runMockChainWrite - . reinterpretMockChainWriteWithTweak @FullTweakEffs + . runMockChainOverride + . runMockChainSubmit + . runChainValidate + . insertAt @1 + @'[ Submit + ] + . reinterpretMockChainValidateWithTweak @FullTweakEffs . runModifyGlobally -- | A stack of effects aimed at being used as modifications for a -- `StagedMockChain` computation type ExtendedStagedTweakEffs extraEff = '[ extraEff, - MockChainMisc, - MockChainRead, + Misc, + Query, + Time, Fail ] -- | A tweak computation based on the `ExtendedStagedTweakEffs` stack of effects type ExtendedStagedTweak extraEff a = TypedTweak (ExtendedStagedTweakEffs extraEff) a --- | A stack of effects which allows everything allowed by `DirectEffs` with the --- addition of branching and `Ltl` modification with tweaks living in +-- | A stack of effects which allows everything allowed by `DirectMockChainEffs` +-- with the addition of branching and `Ltl` modification with tweaks living in -- `ExtendedStagedTweakEffs` -type ExtendedStagedEffs extraEff = +type ExtendedStagedMockChainEffs extraEff = '[ ModifyGlobally (UntypedTweak (ExtendedStagedTweakEffs extraEff)), - MockChainWrite, + Validate, + Override, extraEff, - MockChainMisc, - MockChainRead, + Misc, + Query, + Time, Fail, NonDet ] --- | A mockchain computation built on top of the `ExtendedStagedEffs` stack of +-- | A mockchain computation built on top of the `ExtendedStagedMockChainEffs` stack of -- effects -type ExtendedStagedMockChain extraEff a = Sem (ExtendedStagedEffs extraEff) a +type ExtendedStagedMockChain extraEff a = Sem (ExtendedStagedMockChainEffs extraEff) a -- | The class of effects that can be interpreted on their own on top of an -- arbitrary stack of effects class InterpretAlone eff where runInterpretAlone :: Sem (eff : effs) a -> Sem effs a -instance (InterpretAlone extraEff) => RunnableMockChain (ExtendedStagedEffs extraEff) where - runMockChain mcst = +instance (InterpretAlone extraEff) => RunnableMockChain (ExtendedStagedMockChainEffs extraEff) where + runMockChain emInit ciInit = run . runNonDet . runWriter - . runMockChainLog fromLogEntry - . runState mcst + . runMockChainLog + . runState ciInit + . runState emInit . runError - . runToCardanoErrorInMockChainError - . runFailInMockChainError - . runMockChainRead - . runMockChainMisc fromAlias fromNote fromAssert + . mapError CEToCardanoError + . failToError CEFailure + . runMockChainParams + . runMockChainTime + . runMockChainQuery + . runMockChainMisc . runInterpretAlone . evalState [] . runModifyLocally - . runMockChainWrite - . insertAt @7 - @[ Error P.Ledger.ToCardanoError, - Error MockChainError, - State MockChainState, - MockChainLog, - Writer MockChainJournal - ] - . reinterpretMockChainWriteWithTweak @(ExtendedStagedTweakEffs extraEff) + . runMockChainOverride + . runMockChainSubmit + . runChainValidate + . insertAt @1 + @'[ Submit + ] + . insertAt @10 + @'[ Error P.Ledger.ToCardanoError, + Error ChainError, + State EmulatorState, + State ChainIndex, + Log, + Writer ChainJournal + ] + . reinterpretMockChainValidateWithTweak @(ExtendedStagedTweakEffs extraEff) + . insertAt @8 + @'[ Params + ] . runModifyGlobally - . insertAt @2 - @[ ModifyLocally (UntypedTweak (ExtendedStagedTweakEffs extraEff)), - State [Ltl (UntypedTweak (ExtendedStagedTweakEffs extraEff))] - ] + . insertAt @3 + @'[ ModifyLocally (UntypedTweak (ExtendedStagedTweakEffs extraEff)), + State [Ltl (UntypedTweak (ExtendedStagedTweakEffs extraEff))] + ] -- | A stack of effects aimed at being used as modifications for a -- `StagedMockChain` computation @@ -224,13 +273,13 @@ type StagedTweakEffs = ExtendedStagedTweakEffs (Bundle '[]) -- | A tweak computation based on the `StagedTweakEffs` stack of effects type StagedTweak a = TypedTweak StagedTweakEffs a --- | A stack of effects which allows everything allowed by `DirectEffs` with the --- addition of branching and `Ltl` modification with tweaks living in +-- | A stack of effects which allows everything allowed by `DirectMockChainEffs` +-- with the addition of branching and `Ltl` modification with tweaks living in -- `StagedTweakEffs` -type StagedEffs = ExtendedStagedEffs (Bundle '[]) +type StagedMockChainEffs = ExtendedStagedMockChainEffs (Bundle '[]) --- | A mockchain computation built on top of the `StagedEffs` stack of effects -type StagedMockChain a = Sem StagedEffs a +-- | A mockchain computation built on top of the `StagedMockChainEffs` stack of effects +type StagedMockChain a = Sem StagedMockChainEffs a instance InterpretAlone (Bundle '[]) where runInterpretAlone = runBundle diff --git a/src/Cooked/Ltl.hs b/src/Cooked/MockChain/Ltl.hs similarity index 99% rename from src/Cooked/Ltl.hs rename to src/Cooked/MockChain/Ltl.hs index 61637fa13..a8563088d 100644 --- a/src/Cooked/Ltl.hs +++ b/src/Cooked/MockChain/Ltl.hs @@ -3,7 +3,7 @@ -- | This modules provides the infrastructure to modify sequences of -- transactions using LTL formulaes with atomic modifications. This idea is to -- describe when to apply certain modifications within a trace. -module Cooked.Ltl +module Cooked.MockChain.Ltl ( -- * `Ltl` formulas Ltl (..), diff --git a/src/Cooked/MockChain/Run.hs b/src/Cooked/MockChain/Run.hs new file mode 100644 index 000000000..7fd90a372 --- /dev/null +++ b/src/Cooked/MockChain/Run.hs @@ -0,0 +1,62 @@ +-- | This module exposes the infrastructure to execute mockchain runs +module Cooked.MockChain.Run + ( -- * Running mockchains + RunnableMockChain (..), + runMockChainFromConf, + runMockChainFromInitDist, + runMockChainFromInitDistTemplate, + runMockChainDef, + ) +where + +import Cooked.Effect.Override +import Cooked.MockChain.Config +import Cooked.Runtime.State +import Polysemy + +-- | The class of effects that represent a mockchain run +class RunnableMockChain effs where + -- | Runs a mockchain computation + runMockChain :: EmulatorState -> ChainIndex -> Sem effs a -> [RawMockChainReturn a] + +-- | Runs a `RunnableMockChain` from an initial `MockChainConf` +runMockChainFromConf :: + ( RunnableMockChain effs, + Member Override effs + ) => + MockChainConf a b -> + Sem effs a -> + [b] +runMockChainFromConf (MockChainConf emInitState ciInitState initDist funOnResult) currentRun = + fmap funOnResult $ + runMockChain emInitState ciInitState $ + forceOutputs initDist >> currentRun + +-- | Runs a `RunnableMockChain` from an initial distribution +runMockChainFromInitDist :: + ( RunnableMockChain effs, + Member Override effs + ) => + InitialDistribution -> + Sem effs a -> + [MockChainReturn a] +runMockChainFromInitDist initDist = + runMockChainFromConf $ mockChainConfTemplate {mccInitialDistribution = initDist} + +-- | Same as `runMockChainFromInitDist` using the `initialDistributionTemplate` +runMockChainFromInitDistTemplate :: + ( RunnableMockChain effs, + Member Override effs + ) => + Sem effs a -> + [MockChainReturn a] +runMockChainFromInitDistTemplate = runMockChainFromInitDist initialDistributionTemplate + +-- | Runs a `RunnableMockChain` from a default configuration +runMockChainDef :: + ( RunnableMockChain effs, + Member Override effs + ) => + Sem effs a -> + [MockChainReturn a] +runMockChainDef = runMockChainFromConf mockChainConfTemplate diff --git a/src/Cooked/MockChain/Runtime/Error.hs b/src/Cooked/MockChain/Runtime/Error.hs deleted file mode 100644 index d65ca9570..000000000 --- a/src/Cooked/MockChain/Runtime/Error.hs +++ /dev/null @@ -1,76 +0,0 @@ --- | This module exposes the errors that can be raised during a mockchain run -module Cooked.MockChain.Runtime.Error - ( -- * Mockchain errors - BalancingError (..), - MockChainError (..), - - -- * Interpreting effects into `Error MockChainError` - runToCardanoErrorInMockChainError, - runFailInMockChainError, - ) -where - -import Cooked.Skeleton.User -import Ledger.Index qualified as P.Ledger -import Ledger.Slot qualified as P.Ledger -import Ledger.Tx qualified as P.Ledger -import PlutusLedgerApi.V3 qualified as Api -import Polysemy -import Polysemy.Error -import Polysemy.Fail - --- | Errors that can be produced during balancing -data BalancingError - = -- | The balancing user theoretically has enough funds to balancing the - -- transaction, but this balancing results in a surplus payment which they - -- cannot afford ADA-wise. - NotEnoughFundForExtraMinAda Peer - | -- | The balancing does not have enough funds to sustain the fee required to - -- balance the transaction. - NotEnoughFundForProperFee Peer - | -- | The balancing wallet does not have enough funds to balance the - -- transaction - NotEnoughFund Peer Api.Value - | -- | The provided of collateral UTxOs does not have enough funds to cover - -- the potential collateral cost - NoSuitableCollateral Integer Integer Api.Value - | -- | The balancing user has not be provided, but the balancing requires it - MissingBalancingUser - deriving (Show, Eq) - --- | Errors that can be produced by the blockchain -data MockChainError - = -- | Validation errors, either in Phase 1 or Phase 2 - MCEValidationError P.Ledger.ValidationPhase P.Ledger.ValidationError - | -- | Balancing errors - MCEBalancingError BalancingError - | -- | Translating a skeleton element to its Cardano counterpart failed - MCEToCardanoError P.Ledger.ToCardanoError - | -- | The required reference script is missing from a witness utxo - MCEWrongReferenceScriptError Api.TxOutRef Api.ScriptHash (Maybe Api.ScriptHash) - | -- | A UTxO is missing from the mockchain state - MCEUnknownOutRef Api.TxOutRef - | -- | A jump in time would result in a past slot - MCEPastSlot P.Ledger.Slot P.Ledger.Slot - | -- | An attempt to invoke an unsupported feature has been made - MCEUnsupportedFeature String - | -- | Used to provide 'MonadFail' instances. - MCEFailure String - deriving (Show, Eq) - --- | Interpreting `P.Ledger.ToCardanoError` in terms of `MockChainError` -runToCardanoErrorInMockChainError :: - forall effs a. - (Member (Error MockChainError) effs) => - Sem (Error P.Ledger.ToCardanoError : effs) a -> - Sem effs a -runToCardanoErrorInMockChainError = mapError MCEToCardanoError - --- | Interpreting failures in terms of `MockChainError` -runFailInMockChainError :: - forall effs a. - (Member (Error MockChainError) effs) => - Sem (Fail : effs) a -> - Sem effs a -runFailInMockChainError = interpret $ - \(Fail s) -> throw $ MCEFailure s diff --git a/src/Cooked/MockChain/Runtime/Journal.hs b/src/Cooked/MockChain/Runtime/Journal.hs deleted file mode 100644 index 6519cfd9f..000000000 --- a/src/Cooked/MockChain/Runtime/Journal.hs +++ /dev/null @@ -1,56 +0,0 @@ --- | This module exposes the various events emitted during a mockchain run. -module Cooked.MockChain.Runtime.Journal - ( MockChainJournal (..), - fromLogEntry, - fromAlias, - fromNote, - fromAssert, - ) -where - -import Cooked.MockChain.Effect.Log -import Cooked.Pretty.Class -import Cooked.Pretty.Options -import Data.Map -import Data.Map qualified as Map -import PlutusLedgerApi.V3 qualified as Api - --- | This represents the writable elements that can be emitted throughout a --- mockchain run. -data MockChainJournal where - MockChainJournal :: - { -- | Log entries generated by cooked-validators - mcbLog :: [MockChainLogEntry], - -- | Aliases stored by the user - mcbAliases :: Map Api.BuiltinByteString String, - -- | Notes taken by the user, parameterized by some pretty cooked options, - -- to get a better display at the end of the run - mcbNotes :: [PrettyCookedOpts -> DocCooked], - -- | Assertions gathered during the run, alongside their associated error - -- messages to display in case of failure - mcbAssertions :: [(String, Bool)] - } -> - MockChainJournal - -instance Semigroup MockChainJournal where - MockChainJournal l a n p <> MockChainJournal l' a' n' p' = - MockChainJournal (l <> l') (a <> a') (n <> n') (p <> p') - -instance Monoid MockChainJournal where - mempty = MockChainJournal mempty mempty mempty mempty - --- | Build a `MockChainJournal` from a single log entry -fromLogEntry :: MockChainLogEntry -> MockChainJournal -fromLogEntry entry = mempty {mcbLog = [entry]} - --- | Build a `MockChainJournal` from a single alias -fromAlias :: String -> Api.BuiltinByteString -> MockChainJournal -fromAlias s hash = mempty {mcbAliases = Map.singleton hash s} - --- | Build a `MockChainJournal` from a single note -fromNote :: (PrettyCookedOpts -> DocCooked) -> MockChainJournal -fromNote s = mempty {mcbNotes = [s]} - --- | Build a `MockChainJournal` from a single assertion and error message -fromAssert :: String -> Bool -> MockChainJournal -fromAssert s p = mempty {mcbAssertions = [(s, p)]} diff --git a/src/Cooked/MockChain/Testing.hs b/src/Cooked/MockChain/Testing.hs index 4fe651bc8..b38311ef1 100644 --- a/src/Cooked/MockChain/Testing.hs +++ b/src/Cooked/MockChain/Testing.hs @@ -85,22 +85,22 @@ module Cooked.MockChain.Testing ) where +import Cardano.Ledger.Alonzo.Plutus.Evaluate qualified as Alonzo import Control.Exception qualified as E import Control.Monad -import Cooked.MockChain.Effect.Log -import Cooked.MockChain.Effect.Write -import Cooked.MockChain.Run.Runnable -import Cooked.MockChain.Runtime.Error -import Cooked.MockChain.Runtime.Journal -import Cooked.MockChain.Runtime.State +import Cooked.Effect.Override +import Cooked.MockChain.Config +import Cooked.MockChain.Run import Cooked.Pretty +import Cooked.Runtime.Error +import Cooked.Runtime.Journal +import Cooked.Runtime.State import Data.Default import Data.List (isInfixOf) +import Data.Map qualified as Map import Data.Set qualified as Set import Data.Text qualified as T -import Ledger.Index qualified as P.Ledger import Plutus.Script.Utils.Address qualified as Script -import PlutusLedgerApi.V1.Scripts qualified as Api import PlutusLedgerApi.V1.Value qualified as Api import Polysemy import Test.QuickCheck qualified as QC @@ -258,7 +258,7 @@ assertSameSets l r = -- * Data structure to test mockchain traces {-- - Note on properties over the log (or list of 'MockChainLogEntry'): our + Note on properties over the log (or list of 'ChainLogEntry'): our 'Test' structure does not directly embed a predicate over the log. Instead it is embedded in both the failure and success prediates. The reason is simple: the log is generated and accessible in both cases and thus it is @@ -274,10 +274,10 @@ assertSameSets l r = --} -- | Type of properties over failures -type FailureProp prop = PrettyCookedOpts -> [MockChainLogEntry] -> MockChainError -> UtxoState -> prop +type FailureProp prop = PrettyCookedOpts -> [ChainLogEntry] -> ChainError -> UtxoState -> prop -- | Type of properties over successes -type SuccessProp a prop = PrettyCookedOpts -> [MockChainLogEntry] -> a -> UtxoState -> prop +type SuccessProp a prop = PrettyCookedOpts -> [ChainLogEntry] -> a -> UtxoState -> prop -- | Type of properties over the number of run outcomes. This does not -- necessitate a 'PrettyCookedOpts' as parameter as an 'Integer' does not @@ -285,13 +285,13 @@ type SuccessProp a prop = PrettyCookedOpts -> [MockChainLogEntry] -> a -> UtxoSt type SizeProp prop = Integer -> prop -- | Type of properties over the mockchain log -type LogProp prop = PrettyCookedOpts -> [MockChainLogEntry] -> prop +type LogProp prop = PrettyCookedOpts -> [ChainLogEntry] -> prop -- | Type of properties over the 'UtxoState' type StateProp prop = PrettyCookedOpts -> UtxoState -> prop -- | Type of trace runners -type Runner effs a b = MockChainState -> InitialDistribution -> Sem effs a -> [MockChainReturn b] +type Runner effs a b = EmulatorState -> ChainIndex -> InitialDistribution -> Sem effs a -> [MockChainReturn b] -- | Data structure to test a mockchain trace. @a@ is the return typed of the -- tested trace, @prop@ is the domain in which the properties live. This is not @@ -301,8 +301,10 @@ data Test effs a b prop = Test testTrace :: Sem effs a, -- | The runner of the trace, possibly changing the return type testRunner :: Runner effs a b, - -- | The initial state from which the trace should be run - testInitState :: MockChainState, + -- | The initial emulator state from which the trace should be run + testInitEmulatorState :: EmulatorState, + -- | The initial chain index from which the trace should be run + testInitChainIndex :: ChainIndex, -- | The initial distribution from which the trace should be run testInitDist :: InitialDistribution, -- | The requirement on the number of results @@ -330,13 +332,13 @@ testToProp :: Test effs a b prop -> prop testToProp Test {..} = - let results = testRunner testInitState testInitDist testTrace + let results = testRunner testInitEmulatorState testInitChainIndex testInitDist testTrace in testSizeProp (toInteger (length results)) .&&. testAll - ( \ret@(MockChainReturn outcome _ state (MockChainJournal mcLog names _ assertions)) -> + ( \ret@(MockChainReturn outcome _ state (ChainJournal mcLog names _ assertions)) -> let pcOpts = addHashNames names testPrettyOpts in testConjoin - [ testConjoin $ uncurry testBoolMsg <$> assertions, + [ testConjoin $ (\(msg, b) -> testBoolMsg (renderString id (msg pcOpts)) b) <$> assertions, testCounterexample (renderString (prettyCookedOpt pcOpts) ret) $ case outcome of @@ -394,7 +396,7 @@ testCookedQCFromInitDistTemplate name = -- | A test template which expects a success from a trace. This test template is -- built from a trace and a dedicated runner, to be used for runs that do not -- implement `RunnableMockChain`. One of the intended uses is for running --- `Cooked.MockChain.Instance.StagedInjectMockChain` when the additional effect +-- `Cooked.Instance.StagedInjectMockChain` when the additional effect -- results in a extended return value (such as a resulting state). mustSucceedTest' :: (IsProp prop) => @@ -405,7 +407,8 @@ mustSucceedTest' runner trace = Test { testTrace = trace, testRunner = runner, - testInitState = def, + testInitEmulatorState = def, + testInitChainIndex = def, testInitDist = def, testSizeProp = isAtLeastOfSize 1, testFailureProp = \_ _ _ _ -> testFailureMsg "💀 Unexpected failure!", @@ -417,12 +420,12 @@ mustSucceedTest' runner trace = mustSucceedTest :: ( IsProp prop, RunnableMockChain effs, - Member MockChainWrite effs + Member Override effs ) => Sem effs a -> Test effs a a prop -mustSucceedTest = mustSucceedTest' $ \initState initDist -> - runMockChainFromConf $ MockChainConf initState initDist unRawMockChainReturn +mustSucceedTest = mustSucceedTest' $ \emInitState ciInitState initDist -> + runMockChainFromConf $ MockChainConf emInitState ciInitState initDist unRawMockChainReturn -- | A test template which expects a failure from a trace. See -- `mustSucceedTest'` for more information on its intended usage. @@ -435,7 +438,8 @@ mustFailTest' runner trace = Test { testTrace = trace, testRunner = runner, - testInitState = def, + testInitEmulatorState = def, + testInitChainIndex = def, testInitDist = def, testSizeProp = const testSuccess, testFailureProp = \_ _ _ _ -> testSuccess, @@ -447,12 +451,12 @@ mustFailTest' runner trace = mustFailTest :: ( IsProp prop, RunnableMockChain effs, - Member MockChainWrite effs + Member Override effs ) => Sem effs a -> Test effs a a prop -mustFailTest = mustFailTest' $ \initState initDist -> - runMockChainFromConf $ MockChainConf initState initDist unRawMockChainReturn +mustFailTest = mustFailTest' $ \emInitState ciInitState initDist -> + runMockChainFromConf $ MockChainConf emInitState ciInitState initDist unRawMockChainReturn -- * Appending elements (in particular requirements) to existing tests @@ -549,17 +553,26 @@ withFailureProp test failureProp = withErrorProp :: (IsProp prop) => Test effs a b prop -> - (MockChainError -> prop) -> + (ChainError -> prop) -> Test effs a b prop withErrorProp test errorProp = withFailureProp test (\_ _ err _ -> errorProp err) -- * Specific properties around failures +-- | Whether a script failure is a genuine Plutus (phase 2) evaluation failure. +-- Only the 'Alonzo.ValidationFailure' constructor is considered a phase 2 +-- failure; every other constructor is treated as a phase 1 failure. +isValidationFailure :: Alonzo.TransactionScriptFailure era -> Bool +isValidationFailure Alonzo.ValidationFailure {} = True +isValidationFailure _ = False + -- | A property to ensure a phase 1 failure isPhase1Failure :: (IsProp prop) => FailureProp prop -isPhase1Failure _ _ (MCEValidationError P.Ledger.Phase1 _) _ = testSuccess +isPhase1Failure _ _ (CESubmissionFailures _) _ = testSuccess +isPhase1Failure _ _ (CEExUnitsFailures failures) _ + | not (any isValidationFailure (Map.elems failures)) = testSuccess isPhase1Failure pcOpts _ e _ = testFailureMsg $ "Expected phase 1 evaluation failure, got: " @@ -569,7 +582,8 @@ isPhase1Failure pcOpts _ e _ = isPhase2Failure :: (IsProp prop) => FailureProp prop -isPhase2Failure _ _ (MCEValidationError P.Ledger.Phase2 _) _ = testSuccess +isPhase2Failure _ _ (CEExUnitsFailures failures) _ + | any isValidationFailure (Map.elems failures) = testSuccess isPhase2Failure pcOpts _ e _ = testFailureMsg $ "Expected phase 2 evaluation failure, got: " @@ -580,9 +594,10 @@ isPhase1FailureWithMsg :: (IsProp prop) => String -> FailureProp prop -isPhase1FailureWithMsg s _ _ (MCEValidationError P.Ledger.Phase1 (P.Ledger.CardanoLedgerValidationError text)) _ - | s `isInfixOf` T.unpack text = - testSuccess +isPhase1FailureWithMsg s _ _ (CESubmissionFailures failures) _ + | any (isInfixOf s . show) failures = testSuccess +isPhase1FailureWithMsg s _ _ (CEExUnitsFailures failures) _ + | any (\f -> not (isValidationFailure f) && s `isInfixOf` show f) (Map.elems failures) = testSuccess isPhase1FailureWithMsg _ pcOpts _ e _ = testFailureMsg $ "Expected phase 1 evaluation failure with constrained messages, got: " @@ -593,9 +608,8 @@ isPhase2FailureWithMsg :: (IsProp prop) => String -> FailureProp prop -isPhase2FailureWithMsg s _ _ (MCEValidationError P.Ledger.Phase2 (P.Ledger.ScriptFailure (Api.EvaluationError texts _))) _ - | any (isInfixOf s . T.unpack) texts = - testSuccess +isPhase2FailureWithMsg s _ _ (CEExUnitsFailures failures) _ + | not $ null [text | Alonzo.ValidationFailure _ _ logs _ <- Map.elems failures, (T.unpack -> text) <- logs, s `isInfixOf` text] = testSuccess isPhase2FailureWithMsg _ pcOpts _ e _ = testFailureMsg $ "Expected phase 2 evaluation failure with constrained messages, got: " @@ -648,7 +662,7 @@ isAtMostOfSize n1 n2 = -- * Specific properties over the log -- | Ensures a certain event has been emitted. This uses the constructor's name --- of the 'MockChainLogEntry' by relying on 'show' being lazy. +-- of the 'ChainLogEntry' by relying on 'show' being lazy. happened :: (IsProp prop) => String -> @@ -666,7 +680,7 @@ happened eventName _ log <> ")" -- | Ensures a certain event has not been emitted. This uses the constructor's --- name of the 'MockChainLogEntry' by relying on 'show' being lazy. +-- name of the 'ChainLogEntry' by relying on 'show' being lazy. didNotHappen :: (IsProp prop) => String -> LogProp prop didNotHappen eventName _ log | not (eventName `Set.member` Set.fromList (head . words . show <$> log)) = testSuccess didNotHappen eventName _ _ = @@ -746,7 +760,7 @@ mustFailInPhase2Test' runner trace = mustFailInPhase2Test :: ( IsProp prop, RunnableMockChain effs, - Member MockChainWrite effs + Member Override effs ) => Sem effs a -> Test effs a a prop @@ -769,7 +783,7 @@ mustFailInPhase2WithMsgTest' msg runner trace = mustFailInPhase2WithMsgTest :: ( IsProp prop, RunnableMockChain effs, - Member MockChainWrite effs + Member Override effs ) => String -> Sem effs a -> @@ -790,7 +804,7 @@ mustFailInPhase1Test' runner trace = mustFailInPhase1Test :: ( IsProp prop, RunnableMockChain effs, - Member MockChainWrite effs + Member Override effs ) => Sem effs a -> Test effs a a prop @@ -812,7 +826,7 @@ mustFailInPhase1WithMsgTest' msg runner trace = mustFailInPhase1WithMsgTest :: ( IsProp prop, RunnableMockChain effs, - Member MockChainWrite effs + Member Override effs ) => String -> Sem effs a -> @@ -836,7 +850,7 @@ mustSucceedWithSizeTest' size runner trace = mustSucceedWithSizeTest :: ( IsProp prop, RunnableMockChain effs, - Member MockChainWrite effs + Member Override effs ) => Integer -> Sem effs a -> @@ -860,7 +874,7 @@ mustFailWithSizeTest' size runner trace = mustFailWithSizeTest :: ( IsProp prop, RunnableMockChain effs, - Member MockChainWrite effs + Member Override effs ) => Integer -> Sem effs a -> diff --git a/src/Cooked/MockChain/Run/Tweak.hs b/src/Cooked/MockChain/Tweak.hs similarity index 80% rename from src/Cooked/MockChain/Run/Tweak.hs rename to src/Cooked/MockChain/Tweak.hs index bb88f19c8..127b0dc59 100644 --- a/src/Cooked/MockChain/Run/Tweak.hs +++ b/src/Cooked/MockChain/Tweak.hs @@ -1,8 +1,8 @@ -- | This module applies the `Cooked.Tweak.Common.Tweak` effect for the purpose -- of modifying transaction skeleton before sending them for validation. -module Cooked.MockChain.Run.Tweak +module Cooked.MockChain.Tweak ( -- * Modifying mockchain runs using tweaks - reinterpretMockChainWriteWithTweak, + reinterpretMockChainValidateWithTweak, -- * Tweaks geared for 'Cooked.Skeleton.TxSkel' modifications TypedTweak, @@ -19,10 +19,9 @@ module Cooked.MockChain.Run.Tweak where import Control.Monad -import Cooked.Ltl -import Cooked.MockChain.Effect.Write +import Cooked.Effect.Validation +import Cooked.MockChain.Ltl import Cooked.Tweak.Common -import Data.Coerce import Polysemy import Polysemy.Internal import Polysemy.NonDet @@ -37,7 +36,7 @@ data UntypedTweak tweakEffs where -- | Applies a 'Tweak' to every step in a trace where it is applicable, -- branching at any such locations. The tweak must apply at least once. somewhere :: - (Members '[ModifyGlobally (UntypedTweak tweakEffs)] effs) => + (Member (ModifyGlobally (UntypedTweak tweakEffs)) effs) => TypedTweak tweakEffs b -> Sem effs a -> Sem effs a @@ -46,7 +45,7 @@ somewhere = modifyLtl . ltlEventually . LtlAtom . UntypedTweak -- | Applies a 'Tweak' to every transaction in a given trace. Fails if the tweak -- fails anywhere in the trace. everywhere :: - (Members '[ModifyGlobally (UntypedTweak tweakEffs)] effs) => + (Member (ModifyGlobally (UntypedTweak tweakEffs)) effs) => TypedTweak tweakEffs b -> Sem effs a -> Sem effs a @@ -55,7 +54,7 @@ everywhere = modifyLtl . ltlAlways . LtlAtom . UntypedTweak -- | Ensures a given 'Tweak' can never successfully be applied in a computation, -- and leaves the computation unchanged. nowhere :: - (Members '[ModifyGlobally (UntypedTweak tweakEffs)] effs) => + (Member (ModifyGlobally (UntypedTweak tweakEffs)) effs) => TypedTweak tweakEffs b -> Sem effs a -> Sem effs a @@ -64,7 +63,7 @@ nowhere = modifyLtl . ltlNever . LtlAtom . UntypedTweak -- | Apply a given 'Tweak' at every location in a computation where it does not -- fail, which might never occur. whenAble :: - (Members '[ModifyGlobally (UntypedTweak tweakEffs)] effs) => + (Member (ModifyGlobally (UntypedTweak tweakEffs)) effs) => TypedTweak tweakEffs b -> Sem effs a -> Sem effs a @@ -76,7 +75,7 @@ whenAble = modifyLtl . ltlWhenPossible . LtlAtom . UntypedTweak -- See also `Cooked.Tweak.Labels.labelled` to select transactions based on -- labels instead of their index. there :: - (Members '[ModifyGlobally (UntypedTweak tweakEffs)] effs) => + (Member (ModifyGlobally (UntypedTweak tweakEffs)) effs) => Integer -> TypedTweak tweakEffs b -> Sem effs a -> @@ -94,15 +93,16 @@ there n = modifyLtl . ltlDelay n . LtlAtom . UntypedTweak -- given @arguments@. Then `withTweak` says "I want to modify the transaction -- returned by this endpoint in the following way". withTweak :: - (Members '[ModifyGlobally (UntypedTweak tweakEffs)] effs) => + (Member (ModifyGlobally (UntypedTweak tweakEffs)) effs) => Sem effs a -> TypedTweak tweakEffs b -> Sem effs a withTweak = flip (there 0) --- | Reinterpretes `MockChainWrite` in itself, when the `ModifyLocally` effect --- exists in the stack, applying the relevant modifications in the process. -reinterpretMockChainWriteWithTweak :: +-- | Reinterpretes `Validate` in itself, when the `ModifyLocally` +-- effect exists in the stack, applying the relevant modifications in the +-- process. +reinterpretMockChainValidateWithTweak :: forall tweakEffs effs a. ( Members '[ ModifyLocally (UntypedTweak tweakEffs), @@ -111,9 +111,9 @@ reinterpretMockChainWriteWithTweak :: effs, Subsume tweakEffs effs ) => - Sem (MockChainWrite : effs) a -> - Sem (MockChainWrite : effs) a -reinterpretMockChainWriteWithTweak = reinterpret @MockChainWrite $ \case + Sem (Validate : effs) a -> + Sem (Validate : effs) a +reinterpretMockChainValidateWithTweak = reinterpret @Validate $ \case ValidateTxSkel skel -> do requirements <- getRequirements let sumTweak :: TypedTweak tweakEffs () = @@ -130,4 +130,3 @@ reinterpretMockChainWriteWithTweak = reinterpret @MockChainWrite $ \case requirements newTxSkel <- raise $ subsume_ $ fst <$> runTweak skel sumTweak validateTxSkel newTxSkel - a -> send $ coerce a diff --git a/src/Cooked/MockChain/UtxoSearch.hs b/src/Cooked/MockChain/UtxoSearch.hs deleted file mode 100644 index 09e82d43a..000000000 --- a/src/Cooked/MockChain/UtxoSearch.hs +++ /dev/null @@ -1,258 +0,0 @@ --- | This module provides a convenient framework to look through UTxOs and: --- - filter them in a convenient manner --- - extract pieces of information from them -module Cooked.MockChain.UtxoSearch - ( -- * UTxO searches - UtxoSearch, - beginSearch, - beginSearchPure, - - -- * Processing search result - UtxoSearchResult, - getOutputs, - getOutputsAndExtracts, - getExtracts, - getTxOutRefs, - getTxOutRefsAndOutputs, - - -- * Basic UTxO searches - utxosAtSearch, - allUtxosSearch, - txSkelOutByRefSearch, - txSkelOutByRefSearch', - - -- * Extracting new information from UTxOs - extract, - extractPure, - extractAFold, - extractTotal, - extractPureTotal, - extractGetter, - - -- * Filtering some UTxOs out - ensure, - ensurePure, - ensureAFoldIs, - ensureAFoldIsn't, - - -- * Cooked filters - ensureOnlyValueOutputs, - ensureVanillaOutputs, - ensureProperReferenceScript, - ) -where - -import Control.Monad (filterM, forM) -import Cooked.Families hiding (Member) -import Cooked.MockChain.Common -import Cooked.MockChain.Effect.Read -import Cooked.Skeleton.Datum -import Cooked.Skeleton.Output -import Cooked.Skeleton.Value -import Data.Functor -import Data.Maybe -import Optics.Core -import Optics.Core.Extras -import Plutus.Script.Utils.Address qualified as Script -import Plutus.Script.Utils.Scripts qualified as Script -import PlutusLedgerApi.V3 qualified as Api -import Polysemy - --- | Raw result of a `UtxoSearch`. We store the `Api.TxOutRef` of the output, --- alongside an heterogeneous list starting with the output in question, --- followed by any element that was extracted during the search. -type UtxoSearchResult elems = [(Api.TxOutRef, HList (TxSkelOut ': elems))] - --- | A `UtxoSearch` is a computation that returns a list of UTxOs alongside --- their `TxSkelOut` counterpart and a list of other elements retrieved from the --- output. The idea is to begin with a simple search and refine the search with --- filters while appending new elements to the list. -type UtxoSearch effs elems = Sem effs (UtxoSearchResult elems) - --- | Wraps up a computation returning a `Utxos` into a `UtxoSearch` -beginSearch :: - Sem effs Utxos -> - UtxoSearch effs '[] -beginSearch = fmap (fmap (fmap (`HCons` HEmpty))) - --- | Same as `beginSearch` with a pure input -beginSearchPure :: - Utxos -> - UtxoSearch effs '[] -beginSearchPure = beginSearch . return - --- | Retrieves the `TxSkelOut`s from a `UtxoSearchResult` -getOutputs :: - Sem effs (UtxoSearchResult elems) -> - Sem effs [TxSkelOut] -getOutputs = fmap (fmap (hHead . snd)) - --- | Retrieves the `TxSkelOut`s from a `UtxoSearchResult` alongside the --- extracted elements -getOutputsAndExtracts :: - Sem effs (UtxoSearchResult elems) -> - Sem effs [(TxSkelOut, HList elems)] -getOutputsAndExtracts = - fmap (fmap (\(_, HCons output l) -> (output, l))) - --- | Retrieves the extracted elements from a `UtxoSearchResult` -getExtracts :: - Sem effs (UtxoSearchResult elems) -> - Sem effs [HList elems] -getExtracts = fmap (fmap (hTail . snd)) - --- | Retrieves the `Api.TxOutRef`s from a `UtxoSearchResult` -getTxOutRefs :: - Sem effs (UtxoSearchResult elems) -> - Sem effs [Api.TxOutRef] -getTxOutRefs = fmap (fmap fst) - --- | Retrieves both the `Api.TxOutRef`s and `TxSkelOut`s from a `UtxoSearchResult` -getTxOutRefsAndOutputs :: - Sem effs (UtxoSearchResult elems) -> - Sem effs Utxos -getTxOutRefsAndOutputs = fmap (fmap (\(oRef, HCons output _) -> (oRef, output))) - --- | Searches for utxos at a given address with a given filter -utxosAtSearch :: - (Member MockChainRead effs, Script.ToCredential pkh) => - pkh -> - (UtxoSearch effs '[] -> UtxoSearch effs els) -> - UtxoSearch effs els -utxosAtSearch pkh filters = filters $ beginSearch $ utxosAt pkh - --- | Searches for all the known utxos with a given filter -allUtxosSearch :: - (Member MockChainRead effs) => - (UtxoSearch effs '[] -> UtxoSearch effs els) -> - UtxoSearch effs els -allUtxosSearch filters = filters $ beginSearch allUtxos - --- | Searches for utxos belonging to a given list with a given filter -txSkelOutByRefSearch :: - (Member MockChainRead effs) => - [Api.TxOutRef] -> - (UtxoSearch effs '[] -> UtxoSearch effs els) -> - UtxoSearch effs els -txSkelOutByRefSearch utxos filters = - filters $ beginSearch (zip utxos <$> mapM txSkelOutByRef utxos) - --- | Searches for utxos belonging to a given list with no filter -txSkelOutByRefSearch' :: - (Member MockChainRead effs) => - [Api.TxOutRef] -> - UtxoSearch effs '[] -txSkelOutByRefSearch' = (`txSkelOutByRefSearch` id) - --- | Extracts a new element from the currently selected outputs, filtering in --- the process out utxos for which this element is not available -extract :: - (TxSkelOut -> Sem effs (Maybe b)) -> - UtxoSearch effs els -> - UtxoSearch effs (b ': els) -extract extractFun comp = do - resl <- comp - resl' <- forM resl $ - \(oRef, HCons txSkelOut other) -> do - res <- extractFun txSkelOut - return $ res <&> (\x -> (oRef, HCons txSkelOut (HCons x other))) - return $ catMaybes resl' - --- | Same as `extract`, but with a pure extraction function -extractPure :: - (TxSkelOut -> Maybe b) -> - UtxoSearch effs els -> - UtxoSearch effs (b ': els) -extractPure = extract . (return .) - --- | Same as `extractPure`, using an affine fold to extract the element -extractAFold :: - (Is k An_AffineFold) => - Optic' k is TxSkelOut b -> - UtxoSearch effs els -> - UtxoSearch effs (b ': els) -extractAFold = extractPure . preview - --- | Same as `extract`, but with a total extraction function -extractTotal :: - (TxSkelOut -> Sem effs b) -> - UtxoSearch effs els -> - UtxoSearch effs (b ': els) -extractTotal = extract . (fmap Just .) - --- | Same as `extract`, but with a pure and total extraction function -extractPureTotal :: - (TxSkelOut -> b) -> - UtxoSearch effs els -> - UtxoSearch effs (b ': els) -extractPureTotal = extractTotal . (return .) - --- | Same as `extractPureTotal`, using a getter to extract the element -extractGetter :: - (Is k A_Getter) => - Optic' k is TxSkelOut b -> - UtxoSearch effs els -> - UtxoSearch effs (b ': els) -extractGetter = extractPureTotal . view - --- | Ensures the outputs resulting from the search satisfy the given predicate -ensure :: - (TxSkelOut -> Sem effs Bool) -> - UtxoSearch effs els -> - UtxoSearch effs els -ensure filterF comp = - comp >>= filterM (filterF . hHead . snd) - --- | Same as `ensure`, but with a pure predicate -ensurePure :: - (TxSkelOut -> Bool) -> - UtxoSearch effs els -> - UtxoSearch effs els -ensurePure = ensure . (return .) - --- | Ensures the outputs resulting from the search contain the focus of the --- given affine fold -ensureAFoldIs :: - (Is k An_AffineFold) => - Optic' k is TxSkelOut b -> - UtxoSearch effs els -> - UtxoSearch effs els -ensureAFoldIs = ensurePure . is - --- | Ensures the outputs resulting from the search do not contain the focus of --- the given affine fold -ensureAFoldIsn't :: - (Is k An_AffineFold) => - Optic' k is TxSkelOut b -> - UtxoSearch effs els -> - UtxoSearch effs els -ensureAFoldIsn't = ensurePure . isn't - --- | Ensures the outputs resulting from the search do not have a reference --- script, nor a staking credential, nor a datum -ensureOnlyValueOutputs :: - UtxoSearch effs els -> - UtxoSearch effs els -ensureOnlyValueOutputs = - ensureAFoldIsn't txSkelOutReferenceScriptAT - . ensureAFoldIsn't txSkelOutStakingCredentialAT - . ensureAFoldIsn't (txSkelOutDatumL % txSkelOutDatumKindAT) - --- | Same as 'ensureOnlyValueOutputs', but also ensures the searched outputs do not --- contain non-ADA assets. -ensureVanillaOutputs :: - UtxoSearch effs els -> - UtxoSearch effs els -ensureVanillaOutputs = - ensureAFoldIs (txSkelOutValueL % valueLovelaceP) - . ensureOnlyValueOutputs - --- | Ensures the outputs resulting from the search have the given script as a --- reference script -ensureProperReferenceScript :: - (Script.ToScriptHash s) => - s -> - UtxoSearch effs els -> - UtxoSearch effs els -ensureProperReferenceScript (Script.toScriptHash -> sHash) = - ensureAFoldIs (txSkelOutReferenceScriptHashAF % filtered (== sHash)) diff --git a/src/Cooked/Pretty/Class.hs b/src/Cooked/Pretty/Class.hs index ab8d810d8..f148cb389 100644 --- a/src/Cooked/Pretty/Class.hs +++ b/src/Cooked/Pretty/Class.hs @@ -15,9 +15,9 @@ module Cooked.Pretty.Class ) where -import Cooked.Families import Cooked.Pretty.Hashable import Cooked.Pretty.Options +import Cooked.Utilities.Families import Data.ByteString qualified as ByteString import Data.Default import Data.Map qualified as Map diff --git a/src/Cooked/Pretty/Hashable.hs b/src/Cooked/Pretty/Hashable.hs index 898ff9892..1806e3aaf 100644 --- a/src/Cooked/Pretty/Hashable.hs +++ b/src/Cooked/Pretty/Hashable.hs @@ -6,7 +6,7 @@ module Cooked.Pretty.Hashable ) where -import Cooked.Wallet +import Cooked.Utilities.Wallet import Plutus.Script.Utils.Address qualified as Script import Plutus.Script.Utils.Data qualified as Script import Plutus.Script.Utils.Scripts qualified as Script diff --git a/src/Cooked/Pretty/MockChain.hs b/src/Cooked/Pretty/MockChain.hs index b3260106b..50905ad59 100644 --- a/src/Cooked/Pretty/MockChain.hs +++ b/src/Cooked/Pretty/MockChain.hs @@ -4,16 +4,16 @@ -- 'PrettyCookedMaybe' instances for data types returned by a @MockChain@ run. module Cooked.Pretty.MockChain () where -import Cooked.MockChain.Effect.Log -import Cooked.MockChain.Run.Runnable -import Cooked.MockChain.Runtime.Error -import Cooked.MockChain.Runtime.Journal -import Cooked.MockChain.Runtime.State +import Cardano.Api qualified as Cardano +import Cooked.MockChain.Config import Cooked.Pretty.Class import Cooked.Pretty.Options import Cooked.Pretty.Skeleton +import Cooked.Runtime.Error +import Cooked.Runtime.Journal +import Cooked.Runtime.State import Cooked.Skeleton.User -import Cooked.Wallet (walletPKHashToId) +import Cooked.Utilities.Wallet (walletPKHashToId) import Data.Function (on) import Data.List (intersperse) import Data.List qualified as List @@ -40,7 +40,7 @@ instance (Show a) => PrettyCooked [MockChainReturn a] where (PP.align . prettyCookedOpt opts <$> outcomes) instance (Show a) => PrettyCooked (MockChainReturn a) where - prettyCookedOpt opts' (MockChainReturn res outputs (UtxoState available consumed) (MockChainJournal entries ((`addHashNames` opts') -> opts) noteBook assertions)) = + prettyCookedOpt opts' (MockChainReturn res outputs (UtxoState available consumed) (ChainJournal entries ((`addHashNames` opts') -> opts) noteBook assertions)) = PP.vsep $ [ prettyItemize opts "📔 Notes:" "-" $ ($ opts) <$> noteBook | pcOptPrintNotebook opts && not (null noteBook) @@ -49,7 +49,7 @@ instance (Show a) => PrettyCooked (MockChainReturn a) where | pcOptPrintLog opts && not (null entries) ] <> [ prettyItemize opts (if all snd assertions then "✅ Assertions:" else "❌ Assertions:") "-" $ - (\(s, b) -> (if b then "✔" else "✘") <+> prettyCookedOpt opts s) <$> assertions + (\(s, b) -> (if b then "✔" else "✘") <+> s opts) <$> assertions | pcOptPrintAssertions opts && not (null assertions) ] <> [ "🗑️" <+> prettyCookedOpt opts consumed @@ -85,48 +85,64 @@ instance PrettyCooked BalancingError where "Resulting minimal collateral value was" <+> prettyCookedOpt opts colVal ] -instance PrettyCooked MockChainError where - prettyCookedOpt opts (MCEValidationError plutusPhase plutusError) = - PP.vsep ["Validation error " <+> prettyCookedOpt opts plutusPhase, PP.indent 2 (prettyCookedOpt opts plutusError)] - prettyCookedOpt opts (MCEBalancingError err) = prettyCookedOpt opts err - prettyCookedOpt _ (MCEToCardanoError cardanoError) = +instance PrettyCooked ChainError where + prettyCookedOpt opts (CEExUnitsFailures failures) = + prettyItemize opts "Execution units failures:" "-" (PP.viaShow <$> Map.elems failures :: [DocCooked]) + prettyCookedOpt opts (CESubmissionFailures failures) = + prettyItemize opts "Submission failures:" "-" (PP.viaShow <$> failures :: [DocCooked]) + prettyCookedOpt opts (CEBalancingError err) = prettyCookedOpt opts err + prettyCookedOpt _ (CEToCardanoError cardanoError) = "Transaction generation error:" <+> PP.pretty cardanoError - prettyCookedOpt opts (MCEUnknownOutRef txOutRef) = "Unknown transaction output ref:" <+> prettyCookedOpt opts txOutRef - prettyCookedOpt opts (MCEWrongReferenceScriptError oRef expected got) = + prettyCookedOpt opts (CEUnknownOutRef txOutRef) = "Unknown transaction output ref:" <+> prettyCookedOpt opts txOutRef + prettyCookedOpt opts (CEWrongReferenceScriptError oRef expected got) = "Unable to fetch the following reference script:" <+> prettyHash opts expected <+> "in the following UTxO:" <+> prettyCookedOpt opts oRef <+> "but instead got:" <+> (case got of Nothing -> "none"; Just sHash -> prettyHash opts sHash) - prettyCookedOpt _ (MCEUnsupportedFeature feature) = "Unsupported feature:" <+> PP.pretty feature - prettyCookedOpt _ (MCEPastSlot current target) = - "Unable to move back in time; current slot:" - <+> PP.viaShow current - <+> "; target slot:" - <+> PP.viaShow target - prettyCookedOpt _ (MCEFailure msg) = "Failed with:" <+> PP.pretty msg + prettyCookedOpt _ (CEUnsupportedFeature feature) = "Unsupported feature:" <+> PP.pretty feature + prettyCookedOpt opts (CESpendingHashOnlyDatum txOutRef datumHash) = + "Unable to spend the following output, whose datum is only known by its hash:" + <+> prettyCookedOpt opts txOutRef + <+> "with datum hash:" + <+> prettyHash opts datumHash + prettyCookedOpt opts (CESpendingHashOnlyScript txOutRef scriptHash) = + "Unable to spend the following output, whose script is only known by its hash:" + <+> prettyCookedOpt opts txOutRef + <+> "with script hash:" + <+> prettyHash opts scriptHash + <+> "; the full script must be provided" + prettyCookedOpt _ (CENodeToClientVersionError (Cardano.UnsupportedNtcVersionError current allowed)) = + "Unsupported query version:" <+> PP.viaShow current <+> "; allowed:" <+> PP.viaShow allowed + prettyCookedOpt _ (CEEraMismatch (Cardano.EraMismatch ledgerEra txEra)) = + "Era mismatch. Expected:" <+> PP.viaShow ledgerEra <+> ", got:" <+> PP.viaShow txEra + prettyCookedOpt _ (CEAcquiringFailure err) = + "Acquiring failure:" <+> PP.viaShow err + prettyCookedOpt _ (CETooFarAway err) = + "Unforseeable future:" <+> PP.viaShow err + prettyCookedOpt _ (CEFailure msg) = "Failed with:" <+> PP.pretty msg -instance PrettyCooked (Contextualized [MockChainLogEntry]) where +instance PrettyCooked (Contextualized [ChainLogEntry]) where prettyCookedOpt opts (Contextualized outputs entries) = prettyItemize opts "📖 MockChain run log:" "⁍" (fmap (prettyCookedOpt opts . Contextualized outputs) entries) --- | This prints a 'MockChainLogEntry'. In the log, we know a transaction has --- been validated if the 'MCLogSubmittedTxSkel' is followed by a 'MCLogNewTx'. -instance PrettyCooked (Contextualized MockChainLogEntry) where - prettyCookedOpt opts (Contextualized _ (MCLogAdjustedTxSkelOut skelOut newAda)) = +-- | This prints a 'ChainLogEntry'. In the log, we know a transaction has +-- been validated if the 'CLogSubmittedTxSkel' is followed by a 'CLogNewTx'. +instance PrettyCooked (Contextualized ChainLogEntry) where + prettyCookedOpt opts (Contextualized _ (CLogAdjustedTxSkelOut skelOut newAda)) = prettyItemize opts ("New ADA adjustment of" <+> prettyCookedOpt opts (Script.toValue newAda) <+> "performed for output:") "-" skelOut - prettyCookedOpt opts (Contextualized outputs (MCLogSubmittedTxSkel skel)) = + prettyCookedOpt opts (Contextualized outputs (CLogSubmittedTxSkel skel)) = prettyItemize opts "New raw skeleton submitted to the adjustment pipeline:" "-" (Contextualized outputs skel) - prettyCookedOpt opts (Contextualized outputs (MCLogAdjustedTxSkel skel fee mCollaterals)) = + prettyCookedOpt opts (Contextualized outputs (CLogAdjustedTxSkel skel fee mCollaterals)) = prettyItemize opts "New adjusted skeleton submitted for validation:" @@ -145,15 +161,31 @@ instance PrettyCooked (Contextualized MockChainLogEntry) where mCollaterals ) ) - prettyCookedOpt opts (Contextualized _ (MCLogNewTx txId nb)) = + prettyCookedOpt opts (Contextualized _ (CLogNewTx txId validity)) = prettyItemize opts - "New transaction successfully validated:" + "New transaction produced:" "-" - [ "Transaction id:" <+> prettyHash opts txId, - "Number of new outputs:" <+> PP.pretty nb - ] - prettyCookedOpt opts (Contextualized _ (MCLogDiscardedUtxos n s)) = + ( ("Transaction id:" <+> prettyHash opts txId) + : case validity of + Valid nbInputs nbOutputs -> + [ "Validity: valid", + "Number of consumed inputs:" <+> PP.pretty nbInputs, + "Number of new outputs:" <+> PP.pretty nbOutputs + ] + InvalidPhase1 -> + ["Validity: invalid in phase 1 (no ledger change)"] + InvalidPhase2 nbColInputs nbRetColOutputs -> + [ "Validity: invalid in phase 2", + "Number of consumed collateral inputs:" <+> PP.pretty nbColInputs, + "Number of return collateral outputs:" <+> PP.pretty nbRetColOutputs + ] + ) + prettyCookedOpt opts (Contextualized _ (CELogExUnitsFailures failures)) = + prettyItemize opts "Warning: execution units failures:" "-" (PP.viaShow <$> Map.elems failures :: [DocCooked]) + prettyCookedOpt opts (Contextualized _ (CELogSubmissionFailures failures)) = + prettyItemize opts "Warning: submission failures:" "-" (PP.viaShow <$> failures :: [DocCooked]) + prettyCookedOpt opts (Contextualized _ (CLogDiscardedUtxos n s)) = prettyItemize @[DocCooked] opts "Warning:" @@ -161,7 +193,7 @@ instance PrettyCooked (Contextualized MockChainLogEntry) where [ PP.pretty n <+> "balancing UTxOs were discarded", PP.pretty s ] - prettyCookedOpt opts (Contextualized _ (MCLogUnusedCollaterals source)) = + prettyCookedOpt opts (Contextualized _ (CLogUnusedCollaterals source)) = prettyItemize opts "Warning" @@ -170,7 +202,7 @@ instance PrettyCooked (Contextualized MockChainLogEntry) where "Source:" <+> either (prettyCookedOpt opts) (("Given set of size" <+>) . PP.pretty . length) source, "The transaction does not require any collateral" ] - prettyCookedOpt opts (Contextualized _ (MCLogAddedReferenceScript red oRef sHash)) = + prettyCookedOpt opts (Contextualized _ (CLogAddedReferenceScript red oRef sHash)) = prettyItemize opts "New automated attachment of a reference script:" @@ -180,13 +212,13 @@ instance PrettyCooked (Contextualized MockChainLogEntry) where ] ++ prettyCookedOptList opts red ) - prettyCookedOpt opts (Contextualized _ (MCLogAutoFilledWithdrawalAmount cred amount)) = + prettyCookedOpt opts (Contextualized _ (CLogAutoFilledWithdrawalAmount cred amount)) = prettyItemize opts "New auto-filled withdrawal amount:" "-" [prettyCookedOpt opts cred, prettyCookedOpt opts (Script.toValue amount)] - prettyCookedOpt opts (Contextualized _ (MCLogAutoFilledConstitution constitution)) = + prettyCookedOpt opts (Contextualized _ (CLogAutoFilledConstitution constitution)) = "New auto-filled constitution:" <+> prettyHash opts constitution -- | Pretty print a 'UtxoState'. Print the known wallets first, then unknown @@ -263,6 +295,7 @@ instance PrettyCookedList UtxoPayloadSet where splitDatum :: UtxoPayloadDatum -> Maybe (DocCooked, Bool) splitDatum NoUtxoPayloadDatum = Nothing splitDatum (SomeUtxoPayloadDatum dat b) = Just (prettyCookedOpt opts dat, b) + splitDatum (UtxoPayloadDatumHash hash) = Just (prettyHash opts hash, True) newtype CollateralInput = CollateralInput {unCollateralInput :: Api.TxOutRef} diff --git a/src/Cooked/Pretty/Options.hs b/src/Cooked/Pretty/Options.hs index 3f696a705..38a254836 100644 --- a/src/Cooked/Pretty/Options.hs +++ b/src/Cooked/Pretty/Options.hs @@ -11,7 +11,7 @@ module Cooked.Pretty.Options where import Cooked.Pretty.Hashable -import Cooked.Wallet +import Cooked.Utilities.Wallet import Data.Bifunctor (first) import Data.Default import Data.Map (Map) diff --git a/src/Cooked/Pretty/Skeleton.hs b/src/Cooked/Pretty/Skeleton.hs index 9cc812e9b..aadd3f358 100644 --- a/src/Cooked/Pretty/Skeleton.hs +++ b/src/Cooked/Pretty/Skeleton.hs @@ -5,11 +5,9 @@ module Cooked.Pretty.Skeleton (Contextualized (..)) where import Cooked.Pretty.Class -import Cooked.Pretty.Options import Cooked.Pretty.Plutus () import Cooked.Skeleton -import Cooked.Wallet (Wallet) -import Data.Default +import Cooked.Utilities.Wallet (Wallet) import Data.Map (Map) import Data.Map qualified as Map import Data.Maybe (catMaybes) @@ -66,6 +64,7 @@ instance PrettyCooked TxSkelCertificate where instance PrettyCookedList (User req mode) where prettyCookedOptListMaybe opt (UserPubKey (Script.toPubKeyHash -> pkh)) = [Just ("User" <+> prettyHash opt pkh)] prettyCookedOptListMaybe opt (UserScript (toVScript -> vScript)) = [Just ("Script" <+> prettyHash opt vScript)] + prettyCookedOptListMaybe opt (UserScriptHash sHash) = [Just ("Script" <+> prettyHash opt sHash)] prettyCookedOptListMaybe opt (UserRedeemedScript (toVScript -> script) red) = Just (prettyHash opt script) : prettyCookedOptListMaybe opt red @@ -277,41 +276,41 @@ instance PrettyCookedMaybe TxSkelOutDatum where <> prettyHash opts (Api.toBuiltinData dat) <> "):" <+> PP.align (prettyCookedOpt opts dat) + prettyCookedOptMaybe opts (SomeTxSkelOutDatumHash hash) = + Just $ "Datum (hash only)" <+> "(" <> prettyHash opts hash <> ")" --- | Pretty-print a list of transaction skeleton options, only printing an --- option if its value is non-default. +-- | Pretty-print a list of transaction skeleton options, printing every option +-- (except the opaque transaction modification). instance PrettyCookedList TxSkelOpts where - prettyCookedOptListMaybe + prettyCookedOptList opts ( TxSkelOpts - txSkelOptAutoSlotIncrease _ txSkelOptBalancingPolicy txSkelOptFeePolicy txSkelOptBalanceOutputPolicy txSkelOptBalancingUtxos - _ txSkelOptCollateralUtxos - txSkelOptDeferFailures txSkelOptMaxNbOfBalancingUtxos + txSkelOptHaltOnExUnitsFailures + txSkelOptHaltOnSubmissionFailures ) = - [ prettyIfNot True prettyAutoSlotIncrease txSkelOptAutoSlotIncrease, - prettyIfNot def prettyBalanceOutputPolicy txSkelOptBalanceOutputPolicy, - prettyIfNot def prettyBalanceFeePolicy txSkelOptFeePolicy, - prettyIfNot def prettyBalancingPolicy txSkelOptBalancingPolicy, - prettyIfNot def prettyBalancingUtxos txSkelOptBalancingUtxos, - prettyIfNot def prettyCollateralUtxos txSkelOptCollateralUtxos, - prettyIfNot False (const "Defer Phase 2 failures during balancing") txSkelOptDeferFailures, - ("Limit the number of balancing Utxos to " <>) . PP.pretty <$> txSkelOptMaxNbOfBalancingUtxos + [ prettyBalanceOutputPolicy txSkelOptBalanceOutputPolicy, + prettyBalanceFeePolicy txSkelOptFeePolicy, + prettyBalancingPolicy txSkelOptBalancingPolicy, + prettyBalancingUtxos txSkelOptBalancingUtxos, + prettyCollateralUtxos txSkelOptCollateralUtxos, + prettyMaxNbOfBalancingUtxos txSkelOptMaxNbOfBalancingUtxos, + prettyHaltOnFailures "computing execution units" txSkelOptHaltOnExUnitsFailures, + prettyHaltOnFailures "submission" txSkelOptHaltOnSubmissionFailures ] where - prettyIfNot :: (Eq a) => a -> (a -> DocCooked) -> a -> Maybe DocCooked - prettyIfNot defaultValue f x - | x == defaultValue && not (pcOptPrintDefaultTxSkelOpts opts) = Nothing - | otherwise = Just $ f x - prettyAutoSlotIncrease :: Bool -> DocCooked - prettyAutoSlotIncrease True = "Automatic slot increase" - prettyAutoSlotIncrease False = "No automatic slot increase" + prettyMaxNbOfBalancingUtxos :: Maybe Integer -> DocCooked + prettyMaxNbOfBalancingUtxos Nothing = "No limit on the number of balancing Utxos" + prettyMaxNbOfBalancingUtxos (Just n) = "Limit the number of balancing Utxos to " <> PP.pretty n + prettyHaltOnFailures :: DocCooked -> Bool -> DocCooked + prettyHaltOnFailures step False = "Proceed after failures while" <+> step + prettyHaltOnFailures step True = "Halt after failures while" <+> step prettyBalanceOutputPolicy :: BalanceOutputPolicy -> DocCooked prettyBalanceOutputPolicy AdjustExistingOutput = "Balance policy: Adjust existing outputs" prettyBalanceOutputPolicy DontAdjustExistingOutput = "Balance policy: Don't adjust existing outputs" diff --git a/src/Cooked/Runtime.hs b/src/Cooked/Runtime.hs new file mode 100644 index 000000000..d9406cf64 --- /dev/null +++ b/src/Cooked/Runtime.hs @@ -0,0 +1,7 @@ +-- | This module centralizes the running-state types of a chain run. It is an +-- umbrella re-exporting all the runtime submodules (errors, journal and state). +module Cooked.Runtime (module X) where + +import Cooked.Runtime.Error as X +import Cooked.Runtime.Journal as X +import Cooked.Runtime.State as X diff --git a/src/Cooked/Runtime/Error.hs b/src/Cooked/Runtime/Error.hs new file mode 100644 index 000000000..8d08dde68 --- /dev/null +++ b/src/Cooked/Runtime/Error.hs @@ -0,0 +1,66 @@ +-- | This module exposes the errors that can be raised during a mockchain run +module Cooked.Runtime.Error + ( -- * Mockchain errors + BalancingError (..), + ChainError (..), + ) +where + +import Cardano.Api qualified as Cardano +import Cooked.Skeleton.User +import Cooked.Utilities.Aliases +import Ledger.Tx qualified as P.Ledger +import PlutusLedgerApi.V3 qualified as Api + +-- | Errors that can be produced during balancing +data BalancingError + = -- | The balancing user theoretically has enough funds to balancing the + -- transaction, but this balancing results in a surplus payment which they + -- cannot afford ADA-wise. + NotEnoughFundForExtraMinAda Peer + | -- | The balancing does not have enough funds to sustain the fee required to + -- balance the transaction. + NotEnoughFundForProperFee Peer + | -- | The balancing wallet does not have enough funds to balance the + -- transaction + NotEnoughFund Peer Api.Value + | -- | The provided of collateral UTxOs does not have enough funds to cover + -- the potential collateral cost + NoSuitableCollateral Integer Integer Api.Value + | -- | The balancing user has not be provided, but the balancing requires it + MissingBalancingUser + deriving (Show, Eq) + +-- | Errors that can be produced by the blockchain +data ChainError + = -- | Failures occurring while computing execution units + CEExUnitsFailures ExUnitsFailures + | -- | Failures occurring while submitting the transaction for validation + CESubmissionFailures SubmissionFailures + | -- | Balancing errors + CEBalancingError BalancingError + | -- | Translating a skeleton element to its Cardano counterpart failed + CEToCardanoError P.Ledger.ToCardanoError + | -- | The required reference script is missing from a witness utxo + CEWrongReferenceScriptError Api.TxOutRef Api.ScriptHash (Maybe Api.ScriptHash) + | -- | A UTxO is missing from the mockchain state + CEUnknownOutRef Api.TxOutRef + | -- | An attempt to invoke an unsupported feature has been made + CEUnsupportedFeature String + | -- | An attempt to spend a script output whose datum is only known by its + -- hash, which does not provide the datum content required by the witness + CESpendingHashOnlyDatum Api.TxOutRef Api.DatumHash + | -- | An attempt to spend a script output whose script is only known by its + -- hash, without providing the full script through a matching reference input + CESpendingHashOnlyScript Api.TxOutRef Api.ScriptHash + | -- | The node does not support a specific versioned query + CENodeToClientVersionError Cardano.UnsupportedNtcVersionError + | -- | A mismatch exist between a submitted transaction and the node + CEEraMismatch Cardano.EraMismatch + | -- | Failure to get a response from querying a node + CEAcquiringFailure Cardano.AcquiringFailure + | -- | Looking to far into the future, beyond uncertainty + CETooFarAway Cardano.PastHorizonException + | -- | Used to provide 'MonadFail' instances. + CEFailure String + deriving (Show) diff --git a/src/Cooked/Runtime/Journal.hs b/src/Cooked/Runtime/Journal.hs new file mode 100644 index 000000000..66e80f211 --- /dev/null +++ b/src/Cooked/Runtime/Journal.hs @@ -0,0 +1,105 @@ +-- | This module exposes the various events emitted during a mockchain run. +module Cooked.Runtime.Journal + ( TxValidity (..), + ChainLogEntry (..), + ChainJournal (..), + fromLogEntry, + fromAlias, + fromNote, + fromAssert, + ) +where + +import Cooked.Pretty.Class +import Cooked.Pretty.Options +import Cooked.Skeleton +import Cooked.Utilities.Aliases +import Data.Map +import Data.Map qualified as Map +import Plutus.Script.Utils.Scripts qualified as Script +import PlutusLedgerApi.V3 qualified as Api + +-- | The validity of a transaction +data TxValidity + = -- | The transaction is valid, we store the number of inputs and outputs + Valid Int Int + | -- | The transaction is invalid in phase 1 (no ledger change) + InvalidPhase1 + | -- | The transaction is invalid in phase 2, we store the number of collateral + -- inputs and return collateral outputs + InvalidPhase2 Int Int + deriving (Show) + +-- | Events logged when processing transaction skeletons +data ChainLogEntry + = -- | Logging a Skeleton as it is submitted by the user. + CLogSubmittedTxSkel TxSkel + | -- | Logging a Skeleton as it has been adjusted by the balancing mechanism, + -- alongside fee, and possible collateral utxos and return collateral user. + CLogAdjustedTxSkel TxSkel Fee (Maybe Collaterals) + | -- | Logging the production of a new transaction, with its ID as well as its + -- validity. + CLogNewTx Api.TxId TxValidity + | -- | Logging the fact that utxos provided by the user for balancing have to be + -- discarded for a specific reason. + CLogDiscardedUtxos Integer String + | -- | Logging the fact that utxos provided as collaterals will not be used + -- because the transaction does not involve scripts. There are 2 cases, + -- depending on whether the user has provided an explicit user or a set of + -- utxos to be used as collaterals. + CLogUnusedCollaterals (Either Peer CollateralIns) + | -- | Logging the automatic addition of a reference script + CLogAddedReferenceScript TxSkelRedeemer Api.TxOutRef Script.ScriptHash + | -- | Logging the automatic addition of a withdrawal amount + CLogAutoFilledWithdrawalAmount Api.Credential Api.Lovelace + | -- | Logging the automatic addition of the constitution script + CLogAutoFilledConstitution Api.ScriptHash + | -- | Logging the automatic adjustment of a min ada amount + CLogAdjustedTxSkelOut TxSkelOut Api.Lovelace + | -- | Logging the existence of failures uncovered during the computation of + -- execution units, when they're not treated as fatal. + CELogExUnitsFailures ExUnitsFailures + | -- | Logging the existence of failures uncovered during submission, when + -- they're not treated as fatal. + CELogSubmissionFailures SubmissionFailures + deriving (Show) + +-- | This represents the writable elements that can be emitted throughout a +-- mockchain run. +data ChainJournal where + ChainJournal :: + { -- | Log entries generated by cooked-validators + mcbLog :: [ChainLogEntry], + -- | Aliases stored by the user + mcbAliases :: Map Api.BuiltinByteString String, + -- | Notes taken by the user, parameterized by some pretty cooked options, + -- to get a better display at the end of the run + mcbNotes :: [PrettyCookedOpts -> DocCooked], + -- | Assertions gathered during the run, alongside their associated error + -- messages to display in case of failure + mcbAssertions :: [(PrettyCookedOpts -> DocCooked, Bool)] + } -> + ChainJournal + +instance Semigroup ChainJournal where + ChainJournal l a n p <> ChainJournal l' a' n' p' = + ChainJournal (l <> l') (a <> a') (n <> n') (p <> p') + +instance Monoid ChainJournal where + mempty = ChainJournal mempty mempty mempty mempty + +-- | Build a `ChainJournal` from a single log entry +fromLogEntry :: ChainLogEntry -> ChainJournal +fromLogEntry entry = mempty {mcbLog = [entry]} + +-- | Build a `ChainJournal` from a single alias +fromAlias :: String -> Api.BuiltinByteString -> ChainJournal +fromAlias s hash = mempty {mcbAliases = Map.singleton hash s} + +-- | Build a `ChainJournal` from a single note +fromNote :: (PrettyCookedOpts -> DocCooked) -> ChainJournal +fromNote s = mempty {mcbNotes = [s]} + +-- | Build a `ChainJournal` from a single assertion and error message +fromAssert :: (PrettyCookedOpts -> DocCooked) -> Bool -> ChainJournal +fromAssert s p = mempty {mcbAssertions = [(s, p)]} diff --git a/src/Cooked/MockChain/Runtime/State.hs b/src/Cooked/Runtime/State.hs similarity index 64% rename from src/Cooked/MockChain/Runtime/State.hs rename to src/Cooked/Runtime/State.hs index 2c0f6afe7..c5700f90b 100644 --- a/src/Cooked/MockChain/Runtime/State.hs +++ b/src/Cooked/Runtime/State.hs @@ -1,9 +1,18 @@ --- | This module exposes the internal state in which our direct simulation is --- run (`MockChainState`), as well as a restricted and simplified version --- (`UtxoState`). The latter only consists of Utxos with a focus on who owns --- those Utxos. You can see this as having some sort of an "account" view of the --- ledger state, which typically does not exist in Cardano. This is useful for --- two reasons: +-- | This module exposes the two independent pieces of state in which our direct +-- simulation is run: +-- +-- - `EmulatorState`, which gathers the emulator-specific data (the emulator +-- `Emulator.Params` and the `Emulator.EmulatedLedgerState`). This is only +-- relevant when running against the emulated ledger. +-- +-- - `ChainIndex`, which gathers the backend-agnostic data (the map of known +-- outputs and the current constitution script). This piece of state is also +-- meaningful for the node backend, which keeps its own local `ChainIndex`. +-- +-- It also exposes a restricted and simplified view (`UtxoState`). The latter +-- only consists of Utxos with a focus on who owns those Utxos. You can see this +-- as having some sort of an "account" view of the ledger state, which typically +-- does not exist in Cardano. This is useful for two reasons: -- -- - For printing purposes, where it is much more convenient to see the available -- assets as "who owns what" rather than as a set of mixed Utxos. @@ -11,20 +20,25 @@ -- - For testings purposes, when querying the final state of a run is -- needed. For instance, properties such as "does Alice indeed owns 3 XXX -- tokens at the end of this run?" become much easier to express. -module Cooked.MockChain.Runtime.State - ( -- * `MockChainState` and associated optics - MockChainState (..), - mcstParamsL, - mcstLedgerStateL, - mcstOutputsL, - mcstConstitutionL, - mcstMOutputL, - - -- * Helpers to add or remove outputs from a `MockChainState` +module Cooked.Runtime.State + ( -- * `EmulatorState` and associated optics + EmulatorState (..), + emulatorStateParamsL, + emulatorStateLedgerStateL, + + -- * `ChainIndex` and associated optics + ChainIndex (..), + chainIndexOutputsL, + chainIndexConstitutionL, + chainIndexMOutputL, + + -- * Helpers to add or remove outputs from a `ChainIndex` addOutput, + addOutputs, removeOutput, + removeOutputs, - -- * `UtxoState`: A simplified, address-focused view on a `MockChainState` + -- * `UtxoState`: A simplified, address-focused view on a `ChainIndex` UtxoPayloadDatum (..), utxoPayloadDatumKindAT, utxoPayloadDatumTypedAT, @@ -43,8 +57,8 @@ module Cooked.MockChain.Runtime.State -- * Querying the assets owned by a given address holdsInState, - -- * Transforming a `MockChainState` into an `UtxoState` - mcstToUtxoState, + -- * Transforming a `ChainIndex` into an `UtxoState` + chainIndexToUtxoState, ) where @@ -63,50 +77,73 @@ import Plutus.Script.Utils.Address qualified as Script import PlutusLedgerApi.V1.Value qualified as Api import PlutusLedgerApi.V3 qualified as Api --- | The state used to run the simulation in 'Cooked.MockChain.Direct' -data MockChainState where - MockChainState :: +-- | The emulator-specific state used to run the simulation in +-- 'Cooked.Direct'. It only makes sense when running against the +-- emulated ledger. +data EmulatorState where + EmulatorState :: { -- | The parameters of the emulated blockchain - mcstParams :: Emulator.Params, + emulatorStateParams :: Emulator.Params, -- | The ledger state of the emulated blockchain - mcstLedgerState :: Emulator.EmulatedLedgerState, - -- | Associates to each 'Api.TxOutRef' the 'TxSkelOut' that produced it, + emulatorStateLedgerState :: Emulator.EmulatedLedgerState + } -> + EmulatorState + deriving (Show) + +-- | Focuses on the parameters of an 'EmulatorState' +makeLensesFor [("emulatorStateParams", "emulatorStateParamsL")] ''EmulatorState + +-- | Focuses on the ledger state of an 'EmulatorState' +makeLensesFor [("emulatorStateLedgerState", "emulatorStateLedgerStateL")] ''EmulatorState + +instance Default EmulatorState where + def = EmulatorState def (Emulator.initialState def) + +-- | The backend-agnostic state used to run the simulation. It gathers the map +-- of known outputs and the current constitution script. It is also meaningful +-- for the node backend, which keeps its own local 'ChainIndex'. +data ChainIndex where + ChainIndex :: + { -- | Associates to each 'Api.TxOutRef' the 'TxSkelOut' that produced it, -- alongside a boolean to state whether this UTxO is still present in the -- index ('True') or has already been consumed ('False'). - mcstOutputs :: Map Api.TxOutRef (TxSkelOut, Bool), + chainIndexOutputs :: Map Api.TxOutRef (TxSkelOut, Bool), -- | The constitution script to be used with proposals - mcstConstitution :: Maybe VScript + chainIndexConstitution :: Maybe VScript } -> - MockChainState + ChainIndex deriving (Show) --- | Focuses on the parameters of a 'MockChainState' -makeLensesFor [("mcstParams", "mcstParamsL")] ''MockChainState +-- | Focuses on the outputs of a 'ChainIndex' +makeLensesFor [("chainIndexOutputs", "chainIndexOutputsL")] ''ChainIndex --- | Focuses on the ledger state of a 'MockChainState' -makeLensesFor [("mcstLedgerState", "mcstLedgerStateL")] ''MockChainState +-- | Focuses on the constitution script of a 'ChainIndex' +makeLensesFor [("chainIndexConstitution", "chainIndexConstitutionL")] ''ChainIndex --- | Focuses on the outputs of a 'MockChainState' -makeLensesFor [("mcstOutputs", "mcstOutputsL")] ''MockChainState +instance Default ChainIndex where + def = ChainIndex Map.empty Nothing --- | Focuses on the constitution script of a 'MockChainState' -makeLensesFor [("mcstConstitution", "mcstConstitutionL")] ''MockChainState +-- | Accesses a given available Utxo from a `ChainIndex` +chainIndexMOutputL :: Api.TxOutRef -> Lens' ChainIndex (Maybe TxSkelOut) +chainIndexMOutputL oRef = chainIndexOutputsL % at oRef % iso (fmap fst) (fmap (,True)) -instance Default MockChainState where - def = MockChainState def (Emulator.initialState def) Map.empty Nothing +-- | Stores an output in a 'ChainIndex' +addOutput :: Api.TxOutRef -> TxSkelOut -> ChainIndex -> ChainIndex +addOutput oRef = set (chainIndexMOutputL oRef) . Just --- | Accesses a given available Utxo from a `MockChainState` -mcstMOutputL :: Api.TxOutRef -> Lens' MockChainState (Maybe TxSkelOut) -mcstMOutputL oRef = mcstOutputsL % at oRef % iso (fmap fst) (fmap (,True)) +-- | Stores a list of outputs in a 'ChainIndex' +addOutputs :: [(Api.TxOutRef, TxSkelOut)] -> ChainIndex -> ChainIndex +addOutputs outputs chainIndex = + foldl (\index (oRef, output) -> addOutput oRef output index) chainIndex outputs --- | Stores an output in a 'MockChainState' -addOutput :: Api.TxOutRef -> TxSkelOut -> MockChainState -> MockChainState -addOutput oRef = set (mcstMOutputL oRef) . Just - --- | Removes an output from the 'MockChainState'. This does not actually remove +-- | Removes an output from the 'ChainIndex'. This does not actually remove -- it from the map, but instead marks its availability to @False@ -removeOutput :: Api.TxOutRef -> MockChainState -> MockChainState -removeOutput oRef = set (mcstOutputsL % at oRef % _Just % _2) False +removeOutput :: Api.TxOutRef -> ChainIndex -> ChainIndex +removeOutput oRef = set (chainIndexOutputsL % at oRef % _Just % _2) False + +-- | Removes several outputs from a 'ChainIndex' using 'removeOutput' each time +removeOutputs :: (Foldable t) => t Api.TxOutRef -> ChainIndex -> ChainIndex +removeOutputs l index = foldl (flip removeOutput) index l -- | A simplified version of a 'Cooked.Skeleton.Datum.TxSkelOutDatum' which only -- stores the actual datum and whether it is hashed (@True@) or inline @@ -116,6 +153,7 @@ removeOutput oRef = set (mcstOutputsL % at oRef % _Just % _2) False data UtxoPayloadDatum where NoUtxoPayloadDatum :: UtxoPayloadDatum SomeUtxoPayloadDatum :: (DatumConstrs dat) => dat -> Bool -> UtxoPayloadDatum + UtxoPayloadDatumHash :: Api.DatumHash -> UtxoPayloadDatum -- | Focuses on the optional hashed flag of a 'UtxoPayloadDatum' utxoPayloadDatumKindAT :: AffineTraversal' UtxoPayloadDatum Bool @@ -124,11 +162,13 @@ utxoPayloadDatumKindAT = ( \case NoUtxoPayloadDatum -> Left NoUtxoPayloadDatum SomeUtxoPayloadDatum _ b -> Right b + UtxoPayloadDatumHash _ -> Right True ) ( flip ( \kind -> \case NoUtxoPayloadDatum -> NoUtxoPayloadDatum SomeUtxoPayloadDatum content _ -> SomeUtxoPayloadDatum content kind + datum@(UtxoPayloadDatumHash _) -> datum ) ) @@ -146,6 +186,7 @@ utxoPayloadDatumTypedAT = ( \content -> \case NoUtxoPayloadDatum -> NoUtxoPayloadDatum SomeUtxoPayloadDatum _ kind -> SomeUtxoPayloadDatum content kind + UtxoPayloadDatumHash _ -> SomeUtxoPayloadDatum content True ) ) @@ -159,6 +200,9 @@ instance Ord UtxoPayloadDatum where (SomeUtxoPayloadDatum (Api.toBuiltinData -> dat) b) (SomeUtxoPayloadDatum (Api.toBuiltinData -> dat') b') = compare (dat, b) (dat', b') + compare SomeUtxoPayloadDatum {} _ = LT + compare _ SomeUtxoPayloadDatum {} = GT + compare (UtxoPayloadDatumHash hash) (UtxoPayloadDatumHash hash') = compare hash hash' instance Eq UtxoPayloadDatum where dat == dat' = compare dat dat' == EQ @@ -253,10 +297,10 @@ holdsInState (Script.toAddress -> address) = maybe mempty utxoPayloadSetTotal . utxoPayloadSetTotal :: UtxoPayloadSet -> Api.Value utxoPayloadSetTotal = foldOf (utxoPayloadSetListI % folded % utxoPayloadValueL) --- | Builds a 'UtxoState' from a 'MockChainState' -mcstToUtxoState :: MockChainState -> UtxoState -mcstToUtxoState = - List.foldl' extractPayload mempty . Map.toList . mcstOutputs +-- | Builds a 'UtxoState' from a 'ChainIndex' +chainIndexToUtxoState :: ChainIndex -> UtxoState +chainIndexToUtxoState = + List.foldl' extractPayload mempty . Map.toList . chainIndexOutputs where extractPayload :: UtxoState -> (Api.TxOutRef, (TxSkelOut, Bool)) -> UtxoState extractPayload utxoState (txOutRef, (txSkelOut, bool)) = @@ -269,6 +313,7 @@ mcstToUtxoState = ( case view txSkelOutDatumL txSkelOut of NoTxSkelOutDatum -> NoUtxoPayloadDatum SomeTxSkelOutDatum content kind -> SomeUtxoPayloadDatum content (kind /= Inline) + SomeTxSkelOutDatumHash hash -> UtxoPayloadDatumHash hash ) (preview txSkelOutReferenceScriptHashAF txSkelOut) ] diff --git a/src/Cooked/Skeleton.hs b/src/Cooked/Skeleton.hs index 9f58e1b96..17dde5520 100644 --- a/src/Cooked/Skeleton.hs +++ b/src/Cooked/Skeleton.hs @@ -43,6 +43,8 @@ module Cooked.Skeleton -- * Smart constructor txSkelTemplate, + txSkelEmulatorTemplate, + txSkelNodeTemplate, -- * Utilities txSkelKnownTxOutRefs, @@ -66,7 +68,6 @@ import Cooked.Skeleton.User as X import Cooked.Skeleton.ValidityRange as X import Cooked.Skeleton.Value as X import Cooked.Skeleton.Withdrawal as X -import Data.Default import Data.Map (Map) import Data.Map qualified as Map import Data.Set (Set) @@ -241,12 +242,13 @@ txSkelRedeemersT = txSkelSpendingRedeemersT `adjoin` (txSkelRedeemedScriptsT % userRedeemerL) --- | A convenience template of an empty transaction skeleton. -txSkelTemplate :: TxSkel -txSkelTemplate = +-- | A convenience template of an empty transaction skeleton, parameterized by +-- the transaction options to use. +txSkelTemplate :: TxSkelOpts -> TxSkel +txSkelTemplate opts = TxSkel { txSkelLabels = mempty, - txSkelOpts = def, + txSkelOpts = opts, txSkelMints = mempty, txSkelValidityRange = Api.always, txSkelSignatories = mempty, @@ -258,6 +260,16 @@ txSkelTemplate = txSkelCertificates = mempty } +-- | A convenience template of an empty transaction skeleton, using options +-- tailored for the emulator backend ('txSkelOptsEmulatorTemplate'). +txSkelEmulatorTemplate :: TxSkel +txSkelEmulatorTemplate = txSkelTemplate txSkelOptsEmulatorTemplate + +-- | A convenience template of an empty transaction skeleton, using options +-- tailored for a deployed node backend ('txSkelOptsNodeTemplate'). +txSkelNodeTemplate :: TxSkel +txSkelNodeTemplate = txSkelTemplate txSkelOptsNodeTemplate + -- | All 'Api.TxOutRef's in reference inputs from redeemers txSkelReferenceInputsInRedeemers :: TxSkel -> Set Api.TxOutRef txSkelReferenceInputsInRedeemers = diff --git a/src/Cooked/Skeleton/Certificate.hs b/src/Cooked/Skeleton/Certificate.hs index e87a4a13b..b73c45c20 100644 --- a/src/Cooked/Skeleton/Certificate.hs +++ b/src/Cooked/Skeleton/Certificate.hs @@ -17,9 +17,9 @@ module Cooked.Skeleton.Certificate ) where -import Cooked.Families import Cooked.Skeleton.Redeemer import Cooked.Skeleton.User +import Cooked.Utilities.Families import Data.Kind (Type) import Data.Typeable (Typeable, cast) import Ledger.Slot qualified as P.Ledger diff --git a/src/Cooked/Skeleton/Datum.hs b/src/Cooked/Skeleton/Datum.hs index f606fe384..0a1fbd4b2 100644 --- a/src/Cooked/Skeleton/Datum.hs +++ b/src/Cooked/Skeleton/Datum.hs @@ -18,6 +18,7 @@ module Cooked.Skeleton.Datum txSkelOutDatumDatumAF, txSkelOutDatumDatumHashAF, txSkelOutDatumOutputDatumG, + txSkelOutDatumOutputDatumI, ) where @@ -75,16 +76,19 @@ datumKindResolvedP = -- | Datums to be placed in 'Cooked.Skeleton.TxSkel' outputs, which are either -- empty, or composed of a datum content and its placement data TxSkelOutDatum where - -- | use no datum + -- | Don't use any datum NoTxSkelOutDatum :: TxSkelOutDatum - -- | use some datum content and associated placement + -- | Use some datum content with a datum kind SomeTxSkelOutDatum :: (DatumConstrs dat) => dat -> DatumKind -> TxSkelOutDatum + -- | Use some datum hash only + SomeTxSkelOutDatumHash :: Api.DatumHash -> TxSkelOutDatum deriving instance Show TxSkelOutDatum instance Eq TxSkelOutDatum where NoTxSkelOutDatum == NoTxSkelOutDatum = True (SomeTxSkelOutDatum (Api.toBuiltinData -> dat) b) == (SomeTxSkelOutDatum (Api.toBuiltinData -> dat') b') = (dat, b) == (dat', b') + (SomeTxSkelOutDatumHash hash) == (SomeTxSkelOutDatumHash hash') = hash == hash' _ == _ = False instance Ord TxSkelOutDatum where @@ -95,6 +99,9 @@ instance Ord TxSkelOutDatum where (SomeTxSkelOutDatum (Api.toBuiltinData -> dat) b) (SomeTxSkelOutDatum (Api.toBuiltinData -> dat') b') = compare (dat, b) (dat', b') + compare SomeTxSkelOutDatum {} _ = LT + compare _ SomeTxSkelOutDatum {} = GT + compare (SomeTxSkelOutDatumHash hash) (SomeTxSkelOutDatumHash hash') = compare hash hash' -- * Optics working on 'TxSkelOutDatum' @@ -105,11 +112,13 @@ txSkelOutDatumKindAT = ( \case NoTxSkelOutDatum -> Left NoTxSkelOutDatum SomeTxSkelOutDatum _ kind -> Right kind + SomeTxSkelOutDatumHash _ -> Right (Hashed NotResolved) ) ( flip ( \kind -> \case NoTxSkelOutDatum -> NoTxSkelOutDatum SomeTxSkelOutDatum content _ -> SomeTxSkelOutDatum content kind + datum@(SomeTxSkelOutDatumHash _) -> datum ) ) @@ -135,6 +144,7 @@ txSkelOutDatumTypedAT = ( \content -> \case NoTxSkelOutDatum -> NoTxSkelOutDatum SomeTxSkelOutDatum _ kind -> SomeTxSkelOutDatum content kind + SomeTxSkelOutDatumHash _ -> SomeTxSkelOutDatum content (Hashed NotResolved) ) ) @@ -144,7 +154,12 @@ txSkelOutDatumDatumAF = txSkelOutDatumTypedAT % to Api.Datum -- | Retrieves the optional 'Api.DatumHash' of a 'TxSkelOutDatum' txSkelOutDatumDatumHashAF :: AffineFold TxSkelOutDatum Api.DatumHash -txSkelOutDatumDatumHashAF = txSkelOutDatumDatumAF % to Script.datumHash +txSkelOutDatumDatumHashAF = + afolding + ( \case + SomeTxSkelOutDatumHash hash -> Just hash + datum -> Script.datumHash <$> preview txSkelOutDatumDatumAF datum + ) -- | Retrieves the 'Api.OutputDatum' of a 'TxSkelOutDatum' txSkelOutDatumOutputDatumG :: Getter TxSkelOutDatum Api.OutputDatum @@ -154,3 +169,18 @@ instance Script.ToOutputDatum TxSkelOutDatum where toOutputDatum NoTxSkelOutDatum = Api.NoOutputDatum toOutputDatum (SomeTxSkelOutDatum datum Inline) = Api.OutputDatum $ Api.Datum $ Api.toBuiltinData datum toOutputDatum (SomeTxSkelOutDatum datum _) = Api.OutputDatumHash $ Script.datumHash $ Api.Datum $ Api.toBuiltinData datum + toOutputDatum (SomeTxSkelOutDatumHash hash) = Api.OutputDatumHash hash + +-- | An isomorphism betwean our 'TxSkelOutDatum' and Plutus +-- 'Api.OutputDatum'. The existence of this function does not mean that both +-- share the same expressiveness. In only means that there exists a sensible way +-- to convert one into the other, and vice versa. +txSkelOutDatumOutputDatumI :: Iso' TxSkelOutDatum Api.OutputDatum +txSkelOutDatumOutputDatumI = + iso + Script.toOutputDatum + ( \case + Api.OutputDatum (Api.Datum bData) -> SomeTxSkelOutDatum bData Inline + Api.NoOutputDatum -> NoTxSkelOutDatum + Api.OutputDatumHash dHash -> SomeTxSkelOutDatumHash dHash + ) diff --git a/src/Cooked/Skeleton/Label.hs b/src/Cooked/Skeleton/Label.hs index 71cb463a9..c4930a7e6 100644 --- a/src/Cooked/Skeleton/Label.hs +++ b/src/Cooked/Skeleton/Label.hs @@ -28,7 +28,7 @@ type LabelConstrs x = (PrettyCooked x, Show x, Typeable x, Eq x, Ord x) -- skeletons that have been modified by tweaks and automated attacks. -- -- The 'IsString' instance will add a 'Data.Text.Text' label, which can --- be used with 'Cooked.MockChain.Staged.labelled' to apply tweaks +-- be used with 'Cooked.Staged.labelled' to apply tweaks -- to arbitrary transactions annotated with a label. data TxSkelLabel where TxSkelLabel :: (LabelConstrs x) => x -> TxSkelLabel @@ -60,6 +60,6 @@ txSkelLabelTypedP = TxSkelLabel (\txSkelLabel@(TxSkelLabel lbl) -> maybe (Left txSkelLabel) Right (cast lbl)) --- | Turn a literal string into a 'Data.Text.Text' label, to be used with 'Cooked.MockChain.Staged.labelled'. +-- | Turn a literal string into a 'Data.Text.Text' label, to be used with 'Cooked.Staged.labelled'. instance IsString TxSkelLabel where fromString = TxSkelLabel . pack diff --git a/src/Cooked/Skeleton/Option.hs b/src/Cooked/Skeleton/Option.hs index 05d961e69..fc99f9608 100644 --- a/src/Cooked/Skeleton/Option.hs +++ b/src/Cooked/Skeleton/Option.hs @@ -13,24 +13,23 @@ module Cooked.Skeleton.Option -- * Optics txSkelOptModTxL, - txSkelOptAutoSlotIncreaseL, txSkelOptBalancingPolicyL, txSkelOptBalanceOutputPolicyL, txSkelOptFeePolicyL, txSkelOptBalancingUtxosL, - txSkelOptModParamsL, txSkelOptCollateralUtxosL, - txSkelOptDeferPhase2FailuresDuringBalancingL, txSkelOptMaxNbOfBalancingUtxosL, + txSkelOptHaltOnExUnitsFailuresL, + txSkelOptHaltOnSubmissionFailuresL, -- * Utilities txSkelOptAddModTx, - txSkelOptAddModParams, + txSkelOptsEmulatorTemplate, + txSkelOptsNodeTemplate, ) where -import Cardano.Api qualified as Cardano -import Cardano.Node.Emulator qualified as Emulator +import Cooked.Utilities.Aliases import Data.Default import Data.Set (Set) import Data.Typeable @@ -40,7 +39,12 @@ import Plutus.Script.Utils.Address qualified as Script import PlutusLedgerApi.V3 qualified as Api -- | Set of constraints that need to be satisfied by users in options -type UserConstraints pkh = (Script.ToPubKeyHash pkh, Show pkh, Eq pkh, Typeable pkh) +type UserConstraints pkh = + ( Script.ToPubKeyHash pkh, + Show pkh, + Eq pkh, + Typeable pkh + ) -- | What fee policy to use in the transaction. data FeePolicy @@ -133,29 +137,17 @@ instance Default CollateralUtxos where -- | Set of options to modify the behavior of generating and validating some -- transaction. data TxSkelOpts = TxSkelOpts - { -- | Whether to increase the slot counter automatically on transaction - -- submission. This is useful for modelling transactions that could be - -- submitted in parallel in reality, so there should be no explicit ordering - -- of what comes first. - -- - -- Default is @True@. - txSkelOptAutoSlotIncrease :: Bool, - -- | Applies an arbitrary modification to a transaction after it has been - -- potentially adjusted and balanced. The name of this option contains - -- /unsafe/ to draw attention to the fact that modifying a transaction at - -- that stage might make it invalid. Still, this offers a hook for being - -- able to alter a transaction in unforeseen ways. It is mostly used to test - -- contracts that have been written for custom PABs. + { -- | Applies an arbitrary modification to a transaction after it has been + -- adjusted, balanced and generated. This offers a hook for being able to + -- alter a transaction in unforeseen ways. -- -- One interesting use of this function is to observe a transaction just -- before it is being sent for validation, with -- - -- > txSkelOptModTx = [RawModTx Debug.Trace.traceShowId] + -- > txSkelOptModTx = Debug.Trace.traceShowId -- - -- The leftmost function in the list is applied first. - -- - -- Default is @[]@. - txSkelOptModTx :: Cardano.Tx Cardano.ConwayEra -> Cardano.Tx Cardano.ConwayEra, + -- Default is @id@. + txSkelOptModTx :: Transaction -> Transaction, -- | Whether to balance the transaction or not, and which user should -- provide/reclaim the missing and surplus value. -- @@ -178,44 +170,11 @@ data TxSkelOpts = TxSkelOpts -- -- Default is 'BalancingUtxosFromBalancingUser'. txSkelOptBalancingUtxos :: BalancingUtxos, - -- | Apply an arbitrary modification to the protocol parameters that are - -- used to balance and submit the transaction. This is obviously a very - -- unsafe thing to do if you want to preserve compatibility with the actual - -- chain. It is useful mainly for testing purposes, when you might want to - -- use extremely big transactions or transactions that exhaust the maximum - -- execution budget. Such a thing could be accomplished with - -- - -- > txSkelOptModParams = Just $ ModParams increaseTransactionLimits - -- - -- for example. - -- - -- Default is 'Nothing'. - txSkelOptModParams :: Emulator.Params -> Emulator.Params, -- | Which utxos to use as collaterals. They can be given manually, or -- computed automatically from a given, or the balancing, user. -- -- Default is 'CollateralUtxosFromBalancingUser' txSkelOptCollateralUtxos :: CollateralUtxos, - -- | Whether to defer validation failures occurring during balancing - -- (specifically during the computation of execution units) to the actual - -- later submission of the transaction. - -- - -- When set to @False@: the phase 2 validation failures will be caught as - -- early as possible, typically during balancing when the execution units - -- are computed. This will shortcut the whole balancing process which - -- iterates the body generation, and thus increase performances (by 40%). As - -- a result, the balanced `Cooked.Skeleton.TxSkel` will never be computed - -- and thus will be absent from the log, which is the only downside. - -- - -- When set to @True@: the phase 2 validation errors will be ignored during - -- the balancing process. This will result in a worst performance (40%), but - -- will allow the log to display a balanced version of the failing - -- `Cooked.Skeleton.TxSkel`, which might be useful. Only use this when - -- debugging complicated phase 2 failures which require a precise view of - -- the balanced `Cooked.Skeleton.TxSkel` sent for validation. - -- - -- Default is `False` - txSkelOptDeferPhase2FailuresDuringBalancing :: Bool, -- | The optional maximum number of Utxos that can be used during -- balancing. The algorithm which selects Utxos when permorming balancing is -- greedy. In the default use case where the are only a few wallets and @@ -229,31 +188,40 @@ data TxSkelOpts = TxSkelOpts -- added in the inputs of the transaction, if such a Utxo exist. -- -- Default is @Nothing@ - txSkelOptMaxNbOfBalancingUtxos :: Maybe Integer + txSkelOptMaxNbOfBalancingUtxos :: Maybe Integer, + -- | Whether to halt the mockchain run when a failure occurs while computing + -- execution units during balancing (typically a phase 2 script failure + -- uncovered early). When 'False', such failures are only logged. + -- + -- Default is 'True' + txSkelOptHaltOnExUnitsFailures :: Bool, + -- | Whether to halt the mockchain run when a failure occurs while submitting + -- the transaction for validation. When 'False', such failures are only + -- logged. + -- + -- Default is 'True' + txSkelOptHaltOnSubmissionFailures :: Bool } -- | Comparing 'TxSkelOpts' is possible as long as we ignore modifications to the -- generated transaction and the parameters. instance Eq TxSkelOpts where - (TxSkelOpts slotIncrease _ balancingPol feePol balOutputPol balUtxos _ colUtxos deferFailures maxNbBalUtxos) - == (TxSkelOpts slotIncrease' _ balancingPol' feePol' balOutputPol' balUtxos' _ colUtxos' deferFailures' maxNbBalUtxos') = - slotIncrease == slotIncrease' - && balancingPol == balancingPol' + (TxSkelOpts _ balancingPol feePol balOutputPol balUtxos colUtxos maxNbBalUtxos exUnitsPol subPol) + == (TxSkelOpts _ balancingPol' feePol' balOutputPol' balUtxos' colUtxos' maxNbBalUtxos' exUnitsPol' subPol') = + balancingPol == balancingPol' && feePol == feePol' && balOutputPol == balOutputPol' && balUtxos == balUtxos' && colUtxos == colUtxos' - && deferFailures == deferFailures' && maxNbBalUtxos == maxNbBalUtxos' + && exUnitsPol == exUnitsPol' + && subPol == subPol' -- | Showing 'TxSkelOpts' is possible as long as we ignore modifications to the -- generated transaction and the parameters. instance Show TxSkelOpts where - show (TxSkelOpts slotIncrease _ balancingPol feePol balOutputPol balUtxos _ colUtxos deferFailures maxNbBalUtxos) = - show [show slotIncrease, show balancingPol, show feePol, show balOutputPol, show balUtxos, show colUtxos, show deferFailures, show maxNbBalUtxos] - --- | Focuses on the automatic slot increase option of a 'TxSkelOpts' -makeLensesFor [("txSkelOptAutoSlotIncrease", "txSkelOptAutoSlotIncreaseL")] ''TxSkelOpts + show (TxSkelOpts _ balancingPol feePol balOutputPol balUtxos colUtxos maxNbBalUtxos exUnitsPol subPol) = + show [show balancingPol, show feePol, show balOutputPol, show balUtxos, show colUtxos, show maxNbBalUtxos, show exUnitsPol, show subPol] -- | Focuses on the Cardano transaction modifications option of a 'TxSkelOpts' makeLensesFor [("txSkelOptModTx", "txSkelOptModTxL")] ''TxSkelOpts @@ -270,37 +238,57 @@ makeLensesFor [("txSkelOptBalanceOutputPolicy", "txSkelOptBalanceOutputPolicyL") -- | Focuses on the balancing utxos option of a 'TxSkelOpts' makeLensesFor [("txSkelOptBalancingUtxos", "txSkelOptBalancingUtxosL")] ''TxSkelOpts --- | Focuses on the changes to protocol parameters option of a 'TxSkelOpts' -makeLensesFor [("txSkelOptModParams", "txSkelOptModParamsL")] ''TxSkelOpts - -- | Focuses on the collateral utxos option of a 'TxSkelOpts' makeLensesFor [("txSkelOptCollateralUtxos", "txSkelOptCollateralUtxosL")] ''TxSkelOpts --- | Focuses on the deferring of the failures option of a 'TxSkelOpts' -makeLensesFor [("txSkelOptDeferPhase2FailuresDuringBalancing", "txSkelOptDeferPhase2FailuresDuringBalancingL")] ''TxSkelOpts - -- | Focuses on the max nb of balancing Utxos option of a 'TxSkelOpts' makeLensesFor [("txSkelOptMaxNbOfBalancingUtxos", "txSkelOptMaxNbOfBalancingUtxosL")] ''TxSkelOpts -instance Default TxSkelOpts where - def = - TxSkelOpts - { txSkelOptAutoSlotIncrease = True, - txSkelOptModTx = id, - txSkelOptBalancingPolicy = def, - txSkelOptBalanceOutputPolicy = def, - txSkelOptFeePolicy = def, - txSkelOptBalancingUtxos = def, - txSkelOptModParams = id, - txSkelOptCollateralUtxos = def, - txSkelOptDeferPhase2FailuresDuringBalancing = False, - txSkelOptMaxNbOfBalancingUtxos = Nothing - } +-- | Focuses on the halt-on-execution-units-failures option of a 'TxSkelOpts' +makeLensesFor [("txSkelOptHaltOnExUnitsFailures", "txSkelOptHaltOnExUnitsFailuresL")] ''TxSkelOpts + +-- | Focuses on the halt-on-submission-failures option of a 'TxSkelOpts' +makeLensesFor [("txSkelOptHaltOnSubmissionFailures", "txSkelOptHaltOnSubmissionFailuresL")] ''TxSkelOpts -- | Appends a transaction modification to the given 'TxSkelOpts' -txSkelOptAddModTx :: (Cardano.Tx Cardano.ConwayEra -> Cardano.Tx Cardano.ConwayEra) -> TxSkelOpts -> TxSkelOpts +txSkelOptAddModTx :: (Transaction -> Transaction) -> TxSkelOpts -> TxSkelOpts txSkelOptAddModTx modTx = over txSkelOptModTxL (modTx .) --- | Appends a parameters modification to the given 'TxSkelOpts' -txSkelOptAddModParams :: (Emulator.Params -> Emulator.Params) -> TxSkelOpts -> TxSkelOpts -txSkelOptAddModParams modParams = over txSkelOptModParamsL (modParams .) +-- | A sensible set of options when running against the emulator backend. The +-- emulator is local and deterministic and reports complete failures, so we opt +-- to halt on both execution-units and submission failures to surface problems +-- as early and as loudly as possible. Balancing is kept unbounded since the +-- emulator typically deals with only a handful of Utxos. +txSkelOptsEmulatorTemplate :: TxSkelOpts +txSkelOptsEmulatorTemplate = + TxSkelOpts + { txSkelOptModTx = id, + txSkelOptBalancingPolicy = def, + txSkelOptBalanceOutputPolicy = def, + txSkelOptFeePolicy = def, + txSkelOptBalancingUtxos = def, + txSkelOptCollateralUtxos = def, + txSkelOptMaxNbOfBalancingUtxos = Nothing, + txSkelOptHaltOnExUnitsFailures = True, + txSkelOptHaltOnSubmissionFailures = True + } + +-- | A sensible set of options when running against a deployed node backend. A +-- real node may reject transactions for reasons outside of the caller's +-- control, so we do not halt after execution-units and submission failures, +-- letting the mockchain run continue and surface issues through the log rather +-- than aborting. Balancing is capped to keep Utxo selection tractable when the +-- node exposes many candidate Utxos. +txSkelOptsNodeTemplate :: TxSkelOpts +txSkelOptsNodeTemplate = + TxSkelOpts + { txSkelOptModTx = id, + txSkelOptBalancingPolicy = def, + txSkelOptBalanceOutputPolicy = def, + txSkelOptFeePolicy = def, + txSkelOptBalancingUtxos = def, + txSkelOptCollateralUtxos = def, + txSkelOptMaxNbOfBalancingUtxos = Just 10, + txSkelOptHaltOnExUnitsFailures = False, + txSkelOptHaltOnSubmissionFailures = False + } diff --git a/src/Cooked/Skeleton/Output.hs b/src/Cooked/Skeleton/Output.hs index 02e6487ec..91cebdde4 100644 --- a/src/Cooked/Skeleton/Output.hs +++ b/src/Cooked/Skeleton/Output.hs @@ -30,11 +30,11 @@ module Cooked.Skeleton.Output ) where -import Cooked.Families import Cooked.Skeleton.Datum import Cooked.Skeleton.User import Cooked.Skeleton.Value () -import Cooked.Wallet +import Cooked.Utilities.Families +import Cooked.Utilities.Wallet import Data.Kind import Data.Typeable import Optics.Core @@ -149,6 +149,9 @@ instance IsTxSkelOutAllowedOwner Wallet where instance IsTxSkelOutAllowedOwner VScript where toPKHOrVScript = UserScript +instance IsTxSkelOutAllowedOwner Api.ScriptHash where + toPKHOrVScript = UserScriptHash + instance (Typeable a) => IsTxSkelOutAllowedOwner (Script.TypedValidator a) where toPKHOrVScript = UserScript diff --git a/src/Cooked/Skeleton/Proposal.hs b/src/Cooked/Skeleton/Proposal.hs index 90f8a0edb..e76de3128 100644 --- a/src/Cooked/Skeleton/Proposal.hs +++ b/src/Cooked/Skeleton/Proposal.hs @@ -19,7 +19,7 @@ module Cooked.Skeleton.Proposal simpleProposal, -- * Utilities - fillConstitution, + fillConstitutionWhenEmpty, ) where @@ -223,10 +223,10 @@ makeLensesFor [("txSkelProposalAnchor", "txSkelProposalAnchorL")] ''TxSkelPropos simpleProposal :: (Script.ToCredential cred, Typeable kind) => cred -> GovernanceAction kind -> TxSkelProposal simpleProposal cred action = TxSkelProposal cred action Nothing Nothing --- | Sets the constitution script with an empty redeemer when empty. This will --- not tamper with an existing constitution script and redeemer. -fillConstitution :: (ToVScript script, Typeable script) => script -> TxSkelProposal -> TxSkelProposal -fillConstitution constitution = +-- | Sets the constitution script with an empty redeemer. This will not tamper +-- with an existing constitution script and redeemer. +fillConstitutionWhenEmpty :: (ToVScript script, Typeable script) => script -> TxSkelProposal -> TxSkelProposal +fillConstitutionWhenEmpty constitution = over (txSkelProposalMConstitutionAT @IsScript) (maybe (Just $ UserRedeemedScript constitution emptyTxSkelRedeemer) Just) diff --git a/src/Cooked/Skeleton/Signatory.hs b/src/Cooked/Skeleton/Signatory.hs index e3307651e..aa22e5fab 100644 --- a/src/Cooked/Skeleton/Signatory.hs +++ b/src/Cooked/Skeleton/Signatory.hs @@ -16,7 +16,7 @@ module Cooked.Skeleton.Signatory where import Cardano.Crypto.Wallet qualified as Crypto -import Cooked.Wallet +import Cooked.Utilities.Wallet import Optics.Core import Optics.TH import Plutus.Script.Utils.V3 qualified as Script diff --git a/src/Cooked/Skeleton/User.hs b/src/Cooked/Skeleton/User.hs index aa3280108..ec49993e9 100644 --- a/src/Cooked/Skeleton/User.hs +++ b/src/Cooked/Skeleton/User.hs @@ -19,6 +19,7 @@ module Cooked.Skeleton.User -- * Optics userHashG, userCredentialG, + userCredentialI, userRedeemerAT, userVScriptAT, userScriptHashAF, @@ -36,8 +37,8 @@ module Cooked.Skeleton.User ) where -import Cooked.Families import Cooked.Skeleton.Redeemer +import Cooked.Utilities.Families import Data.Kind import Data.Typeable import Optics.Core @@ -83,6 +84,11 @@ data User :: UserKind -> UserMode -> Type where -- | A script user. This can be used whenever a script is needed, but only for -- the allocation mode. UserScript :: forall script kind. (kind ∈ '[IsScript, IsEither], ToVScript script, Typeable script) => script -> User kind Allocation + -- | A script user known only by its hash. This can be used whenever a script + -- is needed for the allocation mode but the full script is not available. + -- Spending an output owned by such a user requires providing the full script + -- through a reference input. + UserScriptHash :: forall kind. (kind ∈ '[IsScript, IsEither]) => Api.ScriptHash -> User kind Allocation -- | A script user with an associated redeemer. This can be used whenever a -- script is needed for redemption mode. UserRedeemedScript :: forall script kind. (kind ∈ [IsScript, IsEither], ToVScript script, Typeable script) => script -> TxSkelRedeemer -> User kind Redemption @@ -93,6 +99,7 @@ type Peer = User IsPubKey Allocation instance Show (User kind mode) where show (UserPubKey (Script.toPubKeyHash -> pkh)) = "UserPubKey " <> show pkh show (UserScript (toVScript -> vScript)) = "UserScript " <> show (Script.toScriptHash vScript) + show (UserScriptHash sHash) = "UserScriptHash " <> show sHash show (UserRedeemedScript (toVScript -> vScript) red) = "UserRedeemedScript " <> show (Script.toScriptHash vScript) <> " " <> show red instance Eq (User kind mode) where @@ -100,17 +107,24 @@ instance Eq (User kind mode) where pkh == pkh' (UserScript (Script.toScriptHash . toVScript -> sHash)) == (UserScript (Script.toScriptHash . toVScript -> sHash')) = sHash == sHash' + (UserScriptHash sHash) == (UserScriptHash sHash') = + sHash == sHash' (UserRedeemedScript (Script.toScriptHash . toVScript -> sHash) red) == (UserRedeemedScript (Script.toScriptHash . toVScript -> sHash') red') = sHash == sHash' && red == red' _ == _ = False instance Ord (User kind mode) where compare (UserPubKey {}) (UserScript {}) = LT + compare (UserPubKey {}) (UserScriptHash {}) = LT compare (UserPubKey {}) (UserRedeemedScript {}) = LT compare (UserScript {}) (UserPubKey {}) = GT + compare (UserScript {}) (UserScriptHash {}) = LT + compare (UserScriptHash {}) (UserPubKey {}) = GT + compare (UserScriptHash {}) (UserScript {}) = GT compare (UserRedeemedScript {}) (UserPubKey {}) = GT compare (UserPubKey (Script.toPubKeyHash -> pkh)) (UserPubKey (Script.toPubKeyHash -> pkh')) = compare pkh pkh' compare (UserScript (Script.toScriptHash . toVScript -> sh)) (UserScript (Script.toScriptHash . toVScript -> sh')) = compare sh sh' + compare (UserScriptHash sh) (UserScriptHash sh') = compare sh sh' compare (UserRedeemedScript (Script.toScriptHash . toVScript -> sh) red) (UserRedeemedScript (Script.toScriptHash . toVScript -> sh') red') = compare (sh, red) (sh', red') @@ -120,6 +134,7 @@ instance Script.ToPubKeyHash (User IsPubKey mode) where instance Script.ToCredential (User kind mode) where toCredential (UserPubKey (Script.toPubKeyHash -> pkh)) = Script.toCredential pkh toCredential (UserScript (toVScript -> vScript)) = Script.toCredential vScript + toCredential (UserScriptHash sHash) = Script.toCredential sHash toCredential (UserRedeemedScript (toVScript -> vScript) _) = Script.toCredential vScript instance Script.ToAddress (User kind mode) where @@ -134,6 +149,7 @@ userHashG = ( \case UserPubKey (Script.toPubKeyHash -> Api.PubKeyHash hs) -> hs UserScript (Script.toScriptHash . toVScript -> Api.ScriptHash hs) -> hs + UserScriptHash (Api.ScriptHash hs) -> hs UserRedeemedScript (Script.toScriptHash . toVScript -> Api.ScriptHash hs) _ -> hs ) @@ -144,6 +160,7 @@ userTypedAF = ( \case UserPubKey @user' pkh | Just Refl <- eqT @user @user' -> Just pkh UserScript @user' script | Just Refl <- eqT @user @user' -> Just script + UserScriptHash sHash | Just Refl <- eqT @user @Api.ScriptHash -> Just sHash UserRedeemedScript @user' script _ | Just Refl <- eqT @user @user' -> Just script _ -> Nothing ) @@ -159,6 +176,7 @@ userTypedScriptAT = ) ( \case UserScript _ -> UserScript + UserScriptHash _ -> UserScript UserRedeemedScript _ red -> (`UserRedeemedScript` red) ) @@ -178,10 +196,12 @@ userEitherScriptP = prism ( \case UserScript script -> UserScript script + UserScriptHash sHash -> UserScriptHash sHash UserRedeemedScript script red -> UserRedeemedScript script red ) ( \case UserScript script -> Right (UserScript script) + UserScriptHash sHash -> Right (UserScriptHash sHash) UserRedeemedScript script red -> Right (UserRedeemedScript script red) user -> Left user ) @@ -200,6 +220,16 @@ userEitherPubKeyP = userCredentialG :: Getter (User kind mode) Api.Credential userCredentialG = to Script.toCredential +-- | An isomorphism between an 'Api.Credential' and an allocation user +userCredentialI :: Iso' (User IsEither Allocation) Api.Credential +userCredentialI = + iso + (view userCredentialG) + ( \case + Api.ScriptCredential sHash -> UserScriptHash sHash + Api.PubKeyCredential pkh -> UserPubKey pkh + ) + -- | Focuses on the optional 'TxSkelRedeemer' of a 'User' userRedeemerAT :: AffineTraversal' (User kind mode) TxSkelRedeemer userRedeemerAT = @@ -230,7 +260,14 @@ userVScriptAT = -- | Retrieves the optional 'Api.ScriptHash' of a 'User' userScriptHashAF :: AffineFold (User kind mode) Api.ScriptHash -userScriptHashAF = userVScriptAT % to Script.toScriptHash +userScriptHashAF = + afolding + ( \case + UserScript (Script.toScriptHash . toVScript -> sHash) -> Just sHash + UserScriptHash sHash -> Just sHash + UserRedeemedScript (Script.toScriptHash . toVScript -> sHash) _ -> Just sHash + _ -> Nothing + ) -- | Focuses on the optional 'Api.PubKeyHash' of a 'User' userPubKeyHashAT :: AffineTraversal' (User kind mode) Api.PubKeyHash @@ -252,29 +289,15 @@ userPubKeyHashI = (\(UserPubKey (Script.toPubKeyHash -> pkh)) -> pkh) UserPubKey --- | Focuses on the 'VScript' of a script -userVScriptL :: Lens' (User IsScript mode) VScript -userVScriptL = - lens - ( \case - UserScript (toVScript -> vScript) -> vScript - UserRedeemedScript (toVScript -> vScript) _ -> vScript - ) - ( \case - UserScript _ -> UserScript - UserRedeemedScript _ red -> (`UserRedeemedScript` red) - ) - -- | Retrieves the 'Api.ScriptHash' of a script userScriptHashG :: Getter (User IsScript mode) Api.ScriptHash -userScriptHashG = userVScriptL % to Script.toScriptHash - --- | Focuses on the 'TxSkelRedeemer' of a script being redeemed -userRedeemerL :: Lens' (User IsScript Redemption) TxSkelRedeemer -userRedeemerL = - lens - (\(UserRedeemedScript _ red) -> red) - (\(UserRedeemedScript script _) -> UserRedeemedScript script) +userScriptHashG = + to + ( \case + UserScript (Script.toScriptHash . toVScript -> sHash) -> sHash + UserScriptHash sHash -> sHash + UserRedeemedScript (Script.toScriptHash . toVScript -> sHash) _ -> sHash + ) -- | An isomorphism between a @User IsScript Redemption@ and a pair of 'VScript' -- and 'TxSkelRedeemer' @@ -283,3 +306,11 @@ userScriptRedeemerI = iso (\(UserRedeemedScript (toVScript -> vScript) red) -> (vScript, red)) (uncurry UserRedeemedScript) + +-- | Focuses on the 'TxSkelRedeemer' of a script being redeemed +userRedeemerL :: Lens' (User IsScript Redemption) TxSkelRedeemer +userRedeemerL = userScriptRedeemerI % _2 + +-- | Focuses on the 'VScript' of a redeemed script +userVScriptL :: Lens' (User IsScript Redemption) VScript +userVScriptL = userScriptRedeemerI % _1 diff --git a/src/Cooked/Tweak.hs b/src/Cooked/Tweak.hs index 916c4b384..7c7a083be 100644 --- a/src/Cooked/Tweak.hs +++ b/src/Cooked/Tweak.hs @@ -1,6 +1,6 @@ -- | This module centralizes Tweaks, that is state-aware skeleton -- modifications. These tweaks can be used on specific skeletons, or deployed in --- time using `Cooked.Ltl` +-- time using `Cooked.MockChain.Ltl` module Cooked.Tweak (module X) where import Cooked.Tweak.Common as X diff --git a/src/Cooked/Tweak/Guard.hs b/src/Cooked/Tweak/Guard.hs index 8d3e70eca..5cd73df4f 100644 --- a/src/Cooked/Tweak/Guard.hs +++ b/src/Cooked/Tweak/Guard.hs @@ -61,7 +61,7 @@ condTweak optic = (guardTweak optic >>) -- > -- > someEndpoint = do -- > ... --- > validateTxSkel' txSkelTemplate +-- > validateTxSkel' txSkelEmulatorTemplate -- > { txSkelLabels = -- > [ TxSkelLabel "InitialMinting" -- > , TxSkelLabel "AuctionWorkflow" @@ -84,7 +84,7 @@ labelled lbl = condTweak $ txSkelLabelsL % at (TxSkelLabel lbl) % _Just -- > -- > someEndpoint = do -- > ... --- > validateTxSkel' txSkelTemplate +-- > validateTxSkel' txSkelEmulatorTemplate -- > { txSkelLabels = -- > [ TxSkelLabel "InitialMinting" -- > , TxSkelLabel "AuctionWorkflow" diff --git a/src/Cooked/Utilities.hs b/src/Cooked/Utilities.hs new file mode 100644 index 000000000..963f04f5b --- /dev/null +++ b/src/Cooked/Utilities.hs @@ -0,0 +1,9 @@ +-- | This module centralizes the transverse utilities used throughout the +-- library (common type aliases, type-family helpers, wallets and a Plutus-level +-- 'ShowBS'). It is an umbrella re-exporting all the utility submodules. +module Cooked.Utilities (module X) where + +import Cooked.Utilities.Aliases as X +import Cooked.Utilities.Families as X +import Cooked.Utilities.ShowBS as X +import Cooked.Utilities.Wallet as X diff --git a/src/Cooked/Utilities/Aliases.hs b/src/Cooked/Utilities/Aliases.hs new file mode 100644 index 000000000..391eeede2 --- /dev/null +++ b/src/Cooked/Utilities/Aliases.hs @@ -0,0 +1,59 @@ +-- | This module exposes some type aliases common to our library +module Cooked.Utilities.Aliases + ( -- * Type aliases + Fee, + CollateralIns, + Collaterals, + Utxo, + Utxos, + BodyContent, + Body, + Transaction, + SubmissionFailures, + ExUnitsFailures, + ) +where + +import Cardano.Api qualified as Cardano +import Cardano.Ledger.Alonzo.Plutus.Evaluate qualified as Alonzo +import Cardano.Ledger.Conway qualified as Conway +import Cardano.Ledger.Conway.Rules qualified as Conway +import Cooked.Skeleton.Output +import Data.Map (Map) +import Data.Set (Set) +import PlutusLedgerApi.V3 qualified as Api + +-- * Type aliases + +-- | An alias for Integers used as fees +type Fee = Integer + +-- | An alias for sets of utxos used as collateral inputs +type CollateralIns = Set Api.TxOutRef + +-- | An alias for optional pairs of collateral inputs and optional return +-- collateral output +type Collaterals = (CollateralIns, Maybe TxSkelOut) + +-- | An alias for an output and its reference +type Utxo = (Api.TxOutRef, TxSkelOut) + +-- | An alias for lists of `Utxo` +type Utxos = Map Api.TxOutRef TxSkelOut + +-- | An alias for a transaction body content +type BodyContent = Cardano.TxBodyContent Cardano.BuildTx Cardano.ConwayEra + +-- | An alias for a transaction body +type Body = Cardano.TxBody Cardano.ConwayEra + +-- | An alias for errors occurring when computing execution units. These contain +-- Phase2 failures, but also errors uncovered when building a proper context to +-- execute the scripts. +type ExUnitsFailures = Map Cardano.ScriptWitnessIndex (Alonzo.TransactionScriptFailure Conway.ConwayEra) + +-- | An alias for errors occurring at submission +type SubmissionFailures = [Conway.ConwayLedgerPredFailure Conway.ConwayEra] + +-- | An alias for a Cardano transaction +type Transaction = Cardano.Tx Cardano.ConwayEra diff --git a/src/Cooked/Families.hs b/src/Cooked/Utilities/Families.hs similarity index 95% rename from src/Cooked/Families.hs rename to src/Cooked/Utilities/Families.hs index 4cb56f853..d416ee41b 100644 --- a/src/Cooked/Families.hs +++ b/src/Cooked/Utilities/Families.hs @@ -3,7 +3,7 @@ -- | This module exposes some type families used to either directly constraint -- values within our skeletons, or constraint inputs of smart constructors for -- components of these skeletons. -module Cooked.Families +module Cooked.Utilities.Families ( -- * Type-level constraints type (∈), type (∉), @@ -23,6 +23,7 @@ module Cooked.Families HList (..), hHead, hTail, + hSingleton, ) where @@ -89,6 +90,10 @@ hHead (HCons a _) = a hTail :: HList (a ': l) -> HList l hTail (HCons _ l) = l +-- | A singleton wrapped in an 'HList' +hSingleton :: a -> HList '[a] +hSingleton = (`HCons` HEmpty) + instance Eq (HList '[]) where _ == _ = True diff --git a/src/Cooked/ShowBS.hs b/src/Cooked/Utilities/ShowBS.hs similarity index 99% rename from src/Cooked/ShowBS.hs rename to src/Cooked/Utilities/ShowBS.hs index e5cd50844..88990e565 100644 --- a/src/Cooked/ShowBS.hs +++ b/src/Cooked/Utilities/ShowBS.hs @@ -10,7 +10,7 @@ -- module, consider using -- 'Cooked.Skeleton.Option.txOptEmulatorParamsModification' to temporarily -- loosen the limits (at the cost of breaking compatibility with mainnet) -module Cooked.ShowBS (ShowBS (..)) where +module Cooked.Utilities.ShowBS (ShowBS (..)) where import PlutusLedgerApi.V3 qualified as Api import PlutusTx.AssocMap qualified as Map diff --git a/src/Cooked/Wallet.hs b/src/Cooked/Utilities/Wallet.hs similarity index 99% rename from src/Cooked/Wallet.hs rename to src/Cooked/Utilities/Wallet.hs index e471f40f2..ff19cc3d6 100644 --- a/src/Cooked/Wallet.hs +++ b/src/Cooked/Utilities/Wallet.hs @@ -3,7 +3,7 @@ -- | This module defines convenient wrappers for mock chain wallets (around -- Plutus mock wallets) with an associate API to construct them, manipulate -- them, and fetch information (such as public/private and staking keys). -module Cooked.Wallet +module Cooked.Utilities.Wallet ( knownWallets, wallet, walletPKHashToId, diff --git a/tests/Plutus/Withdrawals.hs b/tests/Plutus/Withdrawals.hs index ccc2c95cd..cbe93d5e8 100644 --- a/tests/Plutus/Withdrawals.hs +++ b/tests/Plutus/Withdrawals.hs @@ -2,7 +2,7 @@ module Plutus.Withdrawals where -import Cooked.ShowBS +import Cooked.Utilities.ShowBS import Plutus.Script.Utils.V3 qualified as Script import PlutusLedgerApi.V3 qualified as Api import PlutusTx diff --git a/tests/Spec/Attack/DatumHijacking.hs b/tests/Spec/Attack/DatumHijacking.hs index 25630b36e..dedfdac40 100644 --- a/tests/Spec/Attack/DatumHijacking.hs +++ b/tests/Spec/Attack/DatumHijacking.hs @@ -4,6 +4,7 @@ module Spec.Attack.DatumHijacking (tests) where import Cooked import Data.Map qualified as Map +import Data.Set qualified as Set import Optics.Core import Plutus.Attack.DatumHijacking import Plutus.Script.Utils.V3 qualified as Script @@ -22,7 +23,7 @@ instance PrettyCooked LockDatum where lockTxSkel :: Api.TxOutRef -> Script.MultiPurposeScript DHContract -> TxSkel lockTxSkel o v = - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton o emptyTxSkelRedeemer, txSkelOutputs = [v `receives` InlineDatum FirstLock <&&> Value lockValue], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] @@ -30,12 +31,16 @@ lockTxSkel o v = txLock :: Script.MultiPurposeScript DHContract -> StagedMockChain Api.TxOutRef txLock v = do - oref : _ <- getTxOutRefs $ utxosAtSearch (wallet 1) $ ensureAFoldIs (txSkelOutValueL % filtered (`Api.geq` lockValue)) - fst . head <$> validateTxSkel' (lockTxSkel oref v) + utxosAt (wallet 1) + >>= ensureAFoldIs (txSkelOutValueL % filtered (`Api.geq` lockValue)) + >>= retrieveTxOutRefs + >>= retrieve ((`lockTxSkel` v) . Set.elemAt 0) + >>= validateTxSkelL + >>= retrieve head relockTxSkel :: Script.MultiPurposeScript DHContract -> Api.TxOutRef -> TxSkel relockTxSkel v o = - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton o $ someTxSkelRedeemer (), txSkelOutputs = [v `receives` InlineDatum SecondLock <&&> Value lockValue], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] @@ -65,7 +70,7 @@ tests = value_10_000 = Script.lovelace 10000 value_9_999 = Script.lovelace 9999 inSkel = - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [ carelessValidator `receives` InlineDatum SecondLock <&&> Value value_10_001, carelessValidator `receives` InlineDatum SecondLock <&&> Value value_9_999, diff --git a/tests/Spec/Attack/DatumTampering.hs b/tests/Spec/Attack/DatumTampering.hs index 8bd5d2dea..2ad99a7a2 100644 --- a/tests/Spec/Attack/DatumTampering.hs +++ b/tests/Spec/Attack/DatumTampering.hs @@ -17,7 +17,7 @@ alice = wallet 1 datumTamperingAttackTest :: TestTree datumTamperingAttackTest = testCase "datumTamperingAttack" $ - [ txSkelTemplate + [ txSkelEmulatorTemplate { txSkelLabels = Set.singleton $ TxSkelLabel $ DatumTamperingLabel [(52 :: Integer, 53 :: Integer)], txSkelOutputs = [ alice `receives` VisibleHashedDatum (52 :: Integer, 54 :: Integer), @@ -28,7 +28,7 @@ datumTamperingAttackTest = ] @=? (run . runNonDet) ( execTweak - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [ alice `receives` VisibleHashedDatum (52 :: Integer, 53 :: Integer), alice `receives` Value (Script.lovelace 234), @@ -62,7 +62,7 @@ malformDatumAttackTest = ] ( (fmap allBuiltinData . run . runNonDet) ( execTweak - ( txSkelTemplate + ( txSkelEmulatorTemplate { txSkelOutputs = [ alice `receives` VisibleHashedDatum (52 :: Integer, 53 :: Integer), alice `receives` Value (Script.lovelace 234), diff --git a/tests/Spec/Attack/OutputsReordering.hs b/tests/Spec/Attack/OutputsReordering.hs index a496accdf..33a2e413d 100644 --- a/tests/Spec/Attack/OutputsReordering.hs +++ b/tests/Spec/Attack/OutputsReordering.hs @@ -10,7 +10,7 @@ import Test.Tasty.HUnit manyOutputsSkeleton :: TxSkel manyOutputsSkeleton = - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = (\n -> wallet n `receives` Value (Script.ada 10)) <$> [1 .. 5] } diff --git a/tests/Spec/Attack/PeerTampering.hs b/tests/Spec/Attack/PeerTampering.hs index 294f094d5..d30b4a1cc 100644 --- a/tests/Spec/Attack/PeerTampering.hs +++ b/tests/Spec/Attack/PeerTampering.hs @@ -20,7 +20,7 @@ pkh = Script.toPubKeyHash . wallet -- exercises both branches of 'txSkelAllocatedPeersT'. baseSkel :: TxSkel baseSkel = - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [wallet 1 `receives` Value (Script.lovelace 3_000)], txSkelSignatories = txSkelSignatoriesFromList [wallet 1, wallet 2] } diff --git a/tests/Spec/Attack/RedeemerTampering.hs b/tests/Spec/Attack/RedeemerTampering.hs index a1c5af889..39ab6b947 100644 --- a/tests/Spec/Attack/RedeemerTampering.hs +++ b/tests/Spec/Attack/RedeemerTampering.hs @@ -23,7 +23,7 @@ oref = Api.TxOutRef (Api.TxId "") -- must leave untouched. baseSkel :: TxSkel baseSkel = - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.fromList [ (oref 0, someTxSkelRedeemer (10 :: Integer)), @@ -41,7 +41,7 @@ integerRedeemers = toListOf (txSkelSpendingRedeemersT % txSkelRedeemerTypedAT) -- to make their redeemers invisible to the redeemer traversals. certificateSkel :: TxSkelRedeemer -> TxSkel certificateSkel red = - txSkelTemplate + txSkelEmulatorTemplate { txSkelCertificates = [TxSkelCertificate (UserRedeemedScript (toVScript $ Script.trueMPScript @()) red) StakingRegister] } diff --git a/tests/Spec/Attack/TokenDuplication.hs b/tests/Spec/Attack/TokenDuplication.hs index 8a0551993..e92ec731b 100644 --- a/tests/Spec/Attack/TokenDuplication.hs +++ b/tests/Spec/Attack/TokenDuplication.hs @@ -19,7 +19,7 @@ dupTokenTrace pol tName amount recipient = do skel = let mints = review txSkelMintsListI [mint pol () tName amount] mintedValue = Script.toValue mints - in txSkelTemplate + in txSkelEmulatorTemplate { txSkelMints = mints, txSkelOutputs = [recipient `receives` Value mintedValue], txSkelSignatories = txSkelSignatoriesFromList [wallet 3] @@ -38,7 +38,7 @@ tests = ac1 = Api.assetClass (Script.toCurrencySymbol pol1) tName1 ac2 = Api.assetClass (Script.toCurrencySymbol pol2) tName2 skelIn = - txSkelTemplate + txSkelEmulatorTemplate { txSkelMints = review txSkelMintsListI @@ -59,7 +59,7 @@ tests = [ (Script.toCurrencySymbol pol1, tName1, v1 - 5), (Script.toCurrencySymbol pol2, tName2, v2 - 7) ] - in [ ( txSkelTemplate + in [ ( txSkelEmulatorTemplate { txSkelLabels = Set.singleton $ TxSkelLabel $ TokenDuplicationLabel increment, txSkelMints = review @@ -103,13 +103,13 @@ tests = ac1 = Api.assetClass (Script.toCurrencySymbol pol) tName1 ac2 = Api.assetClass (Script.toCurrencySymbol Script.trueMintingMPScript) (Api.TokenName "preExistingToken") skelIn = - txSkelTemplate + txSkelEmulatorTemplate { txSkelMints = review txSkelMintsListI [mint pol () tName1 1], txSkelOutputs = [wallet 1 `receives` Value (Api.assetClassValue ac1 1 <> Api.assetClassValue ac2 2)], txSkelSignatories = txSkelSignatoriesFromList [wallet 2] } skelExpected = - [ ( txSkelTemplate + [ ( txSkelEmulatorTemplate { txSkelLabels = Set.singleton $ TxSkelLabel $ TokenDuplicationLabel $ review (valueAssetClassAmountP pol tName1) 1, txSkelMints = review txSkelMintsListI [mint pol () tName1 2], txSkelOutputs = diff --git a/tests/Spec/Attack/ValidityTampering.hs b/tests/Spec/Attack/ValidityTampering.hs index b7764529f..d5b44c338 100644 --- a/tests/Spec/Attack/ValidityTampering.hs +++ b/tests/Spec/Attack/ValidityTampering.hs @@ -31,7 +31,7 @@ runValidityTampering initialRange params = fmap (view txSkelValidityRangeL) . run . runNonDet - . execTweak (txSkelTemplate {txSkelValidityRange = initialRange}) + . execTweak (txSkelEmulatorTemplate {txSkelValidityRange = initialRange}) $ validityTamperingAttack params tests :: TestTree diff --git a/tests/Spec/Balancing.hs b/tests/Spec/Balancing.hs index 0457f4740..77948d69a 100644 --- a/tests/Spec/Balancing.hs +++ b/tests/Spec/Balancing.hs @@ -1,13 +1,12 @@ module Spec.Balancing where import Cooked -import Data.Default +import Data.List (isInfixOf) import Data.List qualified as List import Data.Map (Map) import Data.Map qualified as Map +import Data.Set (Set) import Data.Set qualified as Set -import Data.Text (isInfixOf) -import Ledger.Index qualified as P.Ledger import Optics.Core import Optics.Core.Extras import Plutus.Script.Utils.V3 qualified as Script @@ -37,13 +36,11 @@ initialDistributionBalancing = alice `receives` FixedValue (Script.ada 105 <> banana 2) <&&> VisibleHashedDatum () ] -type TestBalancingOutcome = (TxSkel, TxSkel, Fee, Maybe Collaterals, [Api.TxOutRef]) +type TestBalancingOutcome = (TxSkel, TxSkel, Fee, Maybe Collaterals, Set Api.TxOutRef) spendsScriptUtxo :: Bool -> FullMockChain (Map Api.TxOutRef TxSkelRedeemer) spendsScriptUtxo False = return Map.empty -spendsScriptUtxo True = do - (scriptOutRef, _) : _ <- utxosAt $ Script.trueSpendingMPScript @() - return $ Map.singleton scriptOutRef emptyTxSkelRedeemerNoAutoFill +spendsScriptUtxo True = fmap (const emptyTxSkelRedeemerNoAutoFill) <$> utxosAt (Script.trueSpendingMPScript @()) testingBalancingTemplate :: -- Value to pay to bob @@ -51,11 +48,11 @@ testingBalancingTemplate :: -- Value to pay back to alice Api.Value -> -- utxos to be spent - FullMockChain [Api.TxOutRef] -> + FullMockChain (Set Api.TxOutRef) -> -- utxos to be used for balancing - FullMockChain [Api.TxOutRef] -> + FullMockChain (Set Api.TxOutRef) -> -- utxos to be used for collaterals - FullMockChain [Api.TxOutRef] -> + FullMockChain (Set Api.TxOutRef) -> -- Whether to consum the script utxo Bool -> -- Option modifications @@ -70,55 +67,52 @@ testingBalancingTemplate toBobValue toAliceValue spendSearch balanceSearch colla additionalSpend <- spendsScriptUtxo consumeScriptUtxo let valueConstr = if adjust then Value else FixedValue skel = - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = List.filter ((/= mempty) . (^. txSkelOutValueL)) [ bob `receives` valueConstr toBobValue, alice `receives` valueConstr toAliceValue ], - txSkelInputs = additionalSpend <> Map.fromList ((,emptyTxSkelRedeemer) <$> toSpendUtxos), + txSkelInputs = additionalSpend <> Map.fromSet (const emptyTxSkelRedeemer) toSpendUtxos, txSkelOpts = optionsMod - def + txSkelOptsEmulatorTemplate { txSkelOptBalancingUtxos = if List.null toBalanceUtxos then BalancingUtxosFromBalancingUser - else BalancingUtxosFromSet $ Set.fromList toBalanceUtxos, + else BalancingUtxosFromSet toBalanceUtxos, txSkelOptCollateralUtxos = if List.null toCollateralUtxos then CollateralUtxosFromBalancingUser - else CollateralUtxosFromSet (Set.fromList toCollateralUtxos) alice + else CollateralUtxosFromSet toCollateralUtxos alice }, txSkelSignatories = txSkelSignatoriesFromList [alice] } - ExtendedTxSkel skel' fee mCols _ <- balanceTxSkel skel - validateTxSkel_ skel + (ExtendedTxSkel skel' fee mCols _ _, _, _, _) <- validateTxSkel skel nonOnlyValueUtxos <- aliceNonOnlyValueUtxos return (skel, skel', fee, mCols, nonOnlyValueUtxos) -aliceNonOnlyValueUtxos :: FullMockChain [Api.TxOutRef] +aliceNonOnlyValueUtxos :: FullMockChain (Set Api.TxOutRef) aliceNonOnlyValueUtxos = - getTxOutRefs $ - utxosAtSearch alice $ - ensurePure $ \skel -> - is txSkelOutReferenceScriptAT skel - || is (txSkelOutDatumL % txSkelOutDatumKindAT) skel + utxosAt alice + >>= ensurePure (\skel -> is txSkelOutReferenceScriptAT skel || is (txSkelOutDatumL % txSkelOutDatumKindAT) skel) + >>= retrieveTxOutRefs -aliceNAdaUtxos :: Integer -> FullMockChain [Api.TxOutRef] +aliceNAdaUtxos :: Integer -> FullMockChain (Set Api.TxOutRef) aliceNAdaUtxos n = - getTxOutRefs $ - utxosAtSearch alice $ - ensureAFoldIs (txSkelOutValueL % valueLovelaceL % filtered (== Api.Lovelace (n * 1_000_000))) + utxosAt alice + >>= ensureAFoldIs (txSkelOutValueL % valueLovelaceL % filtered (== Api.Lovelace (n * 1_000_000))) + >>= retrieveTxOutRefs -aliceRefScriptUtxos :: FullMockChain [Api.TxOutRef] +aliceRefScriptUtxos :: FullMockChain (Set Api.TxOutRef) aliceRefScriptUtxos = - getTxOutRefs $ - utxosAtSearch alice $ - ensureAFoldIs txSkelOutReferenceScriptAT + utxosAt alice + >>= ensureAFoldIs txSkelOutReferenceScriptAT + >>= retrieveTxOutRefs -emptySearch :: FullMockChain [Api.TxOutRef] -emptySearch = return [] +emptySearch :: FullMockChain (Set Api.TxOutRef) +emptySearch = return Set.empty simplePaymentToBob :: Integer -> Integer -> Integer -> Integer -> Bool -> (TxSkelOpts -> TxSkelOpts) -> Bool -> FullMockChain TestBalancingOutcome simplePaymentToBob lv apples oranges bananas = @@ -141,13 +135,13 @@ bothPaymentsToBobAndAlice val = noBalanceMaxFee :: FullMockChain () noBalanceMaxFee = do maxFee <- snd <$> getMinAndMaxFee 0 - (txOutRef : _) <- aliceNAdaUtxos 30 + aliceORefs30Ada <- aliceNAdaUtxos 30 validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [bob `receives` Value (Script.lovelace (30_000_000 - maxFee))], - txSkelInputs = Map.singleton txOutRef emptyTxSkelRedeemer, + txSkelInputs = Map.fromSet (const emptyTxSkelRedeemer) aliceORefs30Ada, txSkelOpts = - def + txSkelOptsEmulatorTemplate { txSkelOptBalancingPolicy = DoNotBalance, txSkelOptFeePolicy = AutoFeeComputation }, @@ -157,33 +151,36 @@ noBalanceMaxFee = do balanceReduceFee :: FullMockChain (Integer, Integer, Integer, Integer) balanceReduceFee = do let skelAutoFee = - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [bob `receives` Value (Script.ada 50)], txSkelSignatories = txSkelSignatoriesFromList [alice] } - ExtendedTxSkel skelBalanced feeBalanced mCols _ <- balanceTxSkel skelAutoFee - (feeBalanced', _) <- estimateTxSkelFee skelBalanced feeBalanced mCols + ExtendedTxSkel skelBalanced feeBalanced mCols _ _ <- balanceTxSkel skelAutoFee + (feeBalanced', _, _) <- estimateTxSkelFee skelBalanced feeBalanced mCols let skelManualFee = skelAutoFee { txSkelOpts = - def + txSkelOptsEmulatorTemplate { txSkelOptFeePolicy = ManualFee (feeBalanced - 1) } } - ExtendedTxSkel skelBalancedManual feeBalancedManual mColsManual _ <- balanceTxSkel skelManualFee - (feeBalancedManual', _) <- estimateTxSkelFee skelBalancedManual feeBalancedManual mColsManual + ExtendedTxSkel skelBalancedManual feeBalancedManual mColsManual _ _ <- balanceTxSkel skelManualFee + (feeBalancedManual', _, _) <- estimateTxSkelFee skelBalancedManual feeBalancedManual mColsManual return (feeBalanced, feeBalanced', feeBalancedManual, feeBalancedManual') reachingMagic :: FullMockChain () reachingMagic = do - bananaOutRefs <- getTxOutRefs $ utxosAtSearch alice $ ensureAFoldIs (txSkelOutValueL % filtered (banana 1 `Api.leq`)) + bananaOutRefs <- + utxosAt alice + >>= ensureAFoldIs (txSkelOutValueL % filtered (banana 1 `Api.leq`)) + >>= retrieveTxOutRefs validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [bob `receives` Value (Script.ada 106 <> banana 12)], txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelOpts = - def - { txSkelOptBalancingUtxos = BalancingUtxosFromSet (Set.fromList bananaOutRefs) + txSkelOptsEmulatorTemplate + { txSkelOptBalancingUtxos = BalancingUtxosFromSet bananaOutRefs } } @@ -212,40 +209,40 @@ testBalancingSucceedsWith msg props run = `withInitDist` initialDistributionBalancing `withResultProp` \res -> testConjoin (($ res) <$> props) -failsAtBalancingWith :: Api.Value -> Wallet -> MockChainError -> Assertion -failsAtBalancingWith val' wal' (MCEBalancingError (NotEnoughFund wal val)) = testBool $ val' == val && Script.toPubKeyHash wal' == Script.toPubKeyHash wal +failsAtBalancingWith :: Api.Value -> Wallet -> ChainError -> Assertion +failsAtBalancingWith val' wal' (CEBalancingError (NotEnoughFund wal val)) = testBool $ val' == val && Script.toPubKeyHash wal' == Script.toPubKeyHash wal failsAtBalancingWith _ _ _ = testBool False -failsAtBalancing :: MockChainError -> Assertion -failsAtBalancing (MCEBalancingError (NotEnoughFund {})) = testBool True -failsAtBalancing (MCEBalancingError (NotEnoughFundForExtraMinAda {})) = testBool True +failsAtBalancing :: ChainError -> Assertion +failsAtBalancing (CEBalancingError (NotEnoughFund {})) = testBool True +failsAtBalancing (CEBalancingError (NotEnoughFundForExtraMinAda {})) = testBool True failsAtBalancing _ = testBool False -failsWithTooLittleFee :: MockChainError -> Assertion -failsWithTooLittleFee (MCEValidationError P.Ledger.Phase1 (P.Ledger.CardanoLedgerValidationError text)) = testBool $ isInfixOf "FeeTooSmallUTxO" text +failsWithTooLittleFee :: ChainError -> Assertion +failsWithTooLittleFee (CESubmissionFailures failures) = testBool $ any (isInfixOf "FeeTooSmallUTxO" . show) failures failsWithTooLittleFee _ = testBool False -failsWithValueNotConserved :: MockChainError -> Assertion -failsWithValueNotConserved (MCEValidationError P.Ledger.Phase1 (P.Ledger.CardanoLedgerValidationError text)) = testBool $ isInfixOf "ValueNotConserved" text +failsWithValueNotConserved :: ChainError -> Assertion +failsWithValueNotConserved (CESubmissionFailures failures) = testBool $ any (isInfixOf "ValueNotConserved" . show) failures failsWithValueNotConserved _ = testBool False -failsWithEmptyTxIns :: MockChainError -> Assertion -failsWithEmptyTxIns (MCEValidationError P.Ledger.Phase1 (P.Ledger.CardanoLedgerValidationError text)) = testBool $ isInfixOf "InputSetEmptyUTxO" text +failsWithEmptyTxIns :: ChainError -> Assertion +failsWithEmptyTxIns (CESubmissionFailures failures) = testBool $ any (isInfixOf "InputSetEmptyUTxO" . show) failures failsWithEmptyTxIns _ = testBool False -failsAtCollateralsWith :: Integer -> MockChainError -> Assertion -failsAtCollateralsWith fee' (MCEBalancingError (NoSuitableCollateral fee percentage val)) = testBool $ fee == fee' && val == Script.lovelace (1 + (fee * percentage) `div` 100) +failsAtCollateralsWith :: Integer -> ChainError -> Assertion +failsAtCollateralsWith fee' (CEBalancingError (NoSuitableCollateral fee percentage val)) = testBool $ fee == fee' && val == Script.lovelace (1 + (fee * percentage) `div` 100) failsAtCollateralsWith _ _ = testBool False -failsAtCollaterals :: MockChainError -> Assertion -failsAtCollaterals (MCEBalancingError (NoSuitableCollateral {})) = testBool True +failsAtCollaterals :: ChainError -> Assertion +failsAtCollaterals (CEBalancingError (NoSuitableCollateral {})) = testBool True failsAtCollaterals _ = testBool False -failsLackOfCollateralWallet :: MockChainError -> Assertion -failsLackOfCollateralWallet (MCEBalancingError MissingBalancingUser) = testBool True +failsLackOfCollateralWallet :: ChainError -> Assertion +failsLackOfCollateralWallet (CEBalancingError MissingBalancingUser) = testBool True failsLackOfCollateralWallet _ = testBool False -testBalancingFailsWith :: (Show a) => String -> (MockChainError -> Assertion) -> FullMockChain a -> TestTree +testBalancingFailsWith :: (Show a) => String -> (ChainError -> Assertion) -> FullMockChain a -> TestTree testBalancingFailsWith msg p smc = testCooked msg $ mustFailTest smc @@ -457,7 +454,7 @@ tests = ( testingBalancingTemplate (Script.ada 142) mempty - ((fst <$>) <$> utxosAt alice) + (Map.keysSet <$> utxosAt alice) emptySearch (aliceNAdaUtxos 1) True @@ -641,7 +638,7 @@ tests = (apple 2 <> orange 5 <> banana 4) mempty emptySearch - ((fst <$>) <$> utxosAt alice) + (Map.keysSet <$> utxosAt alice) emptySearch False (setFixedFee 1_000_000) @@ -653,7 +650,12 @@ tests = ( testingBalancingTemplate mempty mempty - (getTxOutRefs $ utxosAtSearch alice ensureOnlyValueOutputs) + ( utxosAt alice + >>= ensureAFoldIsn't txSkelOutReferenceScriptAT + >>= ensureAFoldIsn't txSkelOutStakingCredentialAT + >>= ensureAFoldIsn't (txSkelOutDatumL % txSkelOutDatumKindAT) + >>= retrieveTxOutRefs + ) emptySearch emptySearch False diff --git a/tests/Spec/BasicUsage.hs b/tests/Spec/BasicUsage.hs index 812ccf001..ed7a6c0ff 100644 --- a/tests/Spec/BasicUsage.hs +++ b/tests/Spec/BasicUsage.hs @@ -15,7 +15,7 @@ carrie = wallet 3 pkToPk :: Wallet -> Wallet -> Integer -> StagedMockChain () pkToPk sender recipient amount = validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [recipient `receives` Value (Script.ada amount)], txSkelSignatories = txSkelSignatoriesFromList [sender] } @@ -30,7 +30,7 @@ multiplePksToPks = mintingQuickValue :: StagedMockChain () mintingQuickValue = validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelMints = review txSkelMintsListI [mint (Script.trueMintingMPScript @()) () (Api.TokenName "banana") 10], txSkelOutputs = [alice `receives` Value (Script.multiPurposeScriptValue (Script.trueMintingMPScript @()) (Api.TokenName "banana") 10)], txSkelSignatories = txSkelSignatoriesFromList [alice] @@ -38,9 +38,9 @@ mintingQuickValue = payToAlwaysTrueValidator :: StagedMockChain Api.TxOutRef payToAlwaysTrueValidator = - fst . head - <$> ( validateTxSkel' $ - txSkelTemplate + head + <$> ( validateTxSkelL $ + txSkelEmulatorTemplate { txSkelOutputs = [Script.trueSpendingMPScript @() `receives` Value (Script.ada 10)], txSkelSignatories = txSkelSignatoriesFromList [alice] } @@ -50,7 +50,7 @@ consumeAlwaysTrueValidator :: StagedMockChain () consumeAlwaysTrueValidator = do outref <- payToAlwaysTrueValidator validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.fromList [(outref, someTxSkelRedeemer ())], txSkelOutputs = [alice `receives` Value (Script.ada 10)], txSkelSignatories = txSkelSignatoriesFromList [alice] diff --git a/tests/Spec/Certificates.hs b/tests/Spec/Certificates.hs index 29022d8b9..0ca602721 100644 --- a/tests/Spec/Certificates.hs +++ b/tests/Spec/Certificates.hs @@ -16,7 +16,7 @@ publishCertificate :: TxSkelCertificate -> DirectMockChain () publishCertificate cert = do forceOutputs_ [alice `receives` Value (Script.ada 100)] validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelCertificates = [cert] } @@ -24,7 +24,7 @@ publishCertificate cert = do withdraw :: User IsEither Redemption -> DirectMockChain () withdraw user = validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelWithdrawals = review txSkelWithdrawalsListI [Withdrawal user Nothing] } diff --git a/tests/Spec/InitialDistribution.hs b/tests/Spec/InitialDistribution.hs index f67421d97..d65843b48 100644 --- a/tests/Spec/InitialDistribution.hs +++ b/tests/Spec/InitialDistribution.hs @@ -24,20 +24,22 @@ initialDistributionWithReferenceScript = : replicate 2 (bob `receives` Value (Script.ada 100)) getValueFromInitialDatum :: DirectMockChain [Integer] -getValueFromInitialDatum = do - fmap hHead <$> getExtracts (utxosAtSearch alice (extractAFold (txSkelOutDatumL % txSkelOutDatumTypedAT @Integer))) +getValueFromInitialDatum = + utxosAt alice + >>= extractAFold (txSkelOutDatumL % txSkelOutDatumTypedAT @Integer) + >>= retrieveExtractedHeads spendReferenceAlwaysTrueValidator :: DirectMockChain () spendReferenceAlwaysTrueValidator = do - [(referenceScriptTxOutRef, _)] <- utxosAt alice - ((scriptTxOutRef, _) : _) <- - validateTxSkel' $ - txSkelTemplate + (fst . Map.elemAt 0 -> referenceScriptTxOutRef) <- utxosAt alice + (scriptTxOutRef : _) <- + validateTxSkelL $ + txSkelEmulatorTemplate { txSkelOutputs = [Script.trueSpendingMPScript @() `receives` Value (Script.ada 2)], txSkelSignatories = txSkelSignatoriesFromList [bob] } validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [alice `receives` Value (Script.ada 2)], txSkelInputs = Map.singleton scriptTxOutRef $ TxSkelRedeemer () (Just referenceScriptTxOutRef) False, txSkelSignatories = txSkelSignatoriesFromList [bob] diff --git a/tests/Spec/InlineDatums.hs b/tests/Spec/InlineDatums.hs index 72af6fcd7..2227ba4b9 100644 --- a/tests/Spec/InlineDatums.hs +++ b/tests/Spec/InlineDatums.hs @@ -23,9 +23,9 @@ listUtxosTestTrace :: Script.Versioned Script.Validator -> DirectMockChain (Api.TxOutRef, TxSkelOut) listUtxosTestTrace useInlineDatum validator = - head + Map.elemAt 0 <$> validateTxSkel' - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [validator `receives` (if useInlineDatum then InlineDatum else VisibleHashedDatum) FirstPaymentDatum], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } @@ -44,7 +44,7 @@ spendOutputTestTrace :: spendOutputTestTrace useInlineDatum validator = do (theTxOutRef, _) <- listUtxosTestTrace useInlineDatum validator validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton theTxOutRef $ someTxSkelRedeemer (), txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } @@ -66,7 +66,7 @@ continuingOutputTestTrace :: continuingOutputTestTrace datumKindOnSecondPayment validator = do (theTxOutRef, theOutput) <- listUtxosTestTrace True validator validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton theTxOutRef $ someTxSkelRedeemer (), txSkelOutputs = [ validator diff --git a/tests/Spec/Ltl.hs b/tests/Spec/Ltl.hs index 7aa73b9b6..af38ee0b6 100644 --- a/tests/Spec/Ltl.hs +++ b/tests/Spec/Ltl.hs @@ -3,7 +3,7 @@ module Spec.Ltl where import Control.Monad (MonadPlus (..), guard, replicateM, void) -import Cooked.Ltl +import Cooked.MockChain.Ltl import Cooked.MockChain.Testing import Data.Maybe import Polysemy diff --git a/tests/Spec/MinAda.hs b/tests/Spec/MinAda.hs index 5b0b31e85..0c4346425 100644 --- a/tests/Spec/MinAda.hs +++ b/tests/Spec/MinAda.hs @@ -1,6 +1,7 @@ module Spec.MinAda where import Cooked +import Data.Map qualified as Map import Optics.Core import Plutus.Script.Utils.Value qualified as Script import PlutusTx qualified @@ -24,9 +25,9 @@ instance PrettyCooked HeavyDatum where paymentWithMinAda :: DirectMockChain Integer paymentWithMinAda = do forceOutputs_ initialDistributionTemplate - view (txSkelOutValueL % valueLovelaceL % lovelaceIntegerI) . snd . (!! 0) + view (txSkelOutValueL % valueLovelaceL % lovelaceIntegerI) . snd . Map.elemAt 0 <$> validateTxSkel' - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [wallet 2 `receives` VisibleHashedDatum heavyDatum], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } @@ -34,7 +35,7 @@ paymentWithMinAda = do paymentWithoutMinAda :: Integer -> DirectMockChain () paymentWithoutMinAda paidLovelaces = do validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [wallet 2 `receives` FixedValue (Script.lovelace paidLovelaces) <&&> VisibleHashedDatum heavyDatum], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } diff --git a/tests/Spec/MultiPurpose.hs b/tests/Spec/MultiPurpose.hs index 80b6116d5..969c3aa0b 100644 --- a/tests/Spec/MultiPurpose.hs +++ b/tests/Spec/MultiPurpose.hs @@ -25,9 +25,9 @@ bob = wallet 2 runScript :: StagedMockChain () runScript = do forceOutputs_ initialDistributionTemplate - [(oRef@(Api.TxOutRef txId _), _), (oRef', _), (oRef'', _)] <- - validateTxSkel' $ - txSkelTemplate + [oRef@(Api.TxOutRef txId _), oRef', oRef''] <- + validateTxSkelL $ + txSkelEmulatorTemplate { txSkelOutputs = [ alice `receives` Value (Script.ada 3), alice `receives` Value (Script.ada 5) @@ -40,13 +40,13 @@ runScript = do (mintSkel2, mintValue2, tn2) = mkMintSkel alice oRef' script (mintSkel3, mintValue3, tn3) = mkMintSkel bob oRef'' script - ((oRefScript, _) : _) <- validateTxSkel' mintSkel1 - ((oRefScript1, _) : _) <- validateTxSkel' mintSkel2 - ((oRefScript2, _) : _) <- validateTxSkel' mintSkel3 + (oRefScript : _) <- validateTxSkelL mintSkel1 + (oRefScript1 : _) <- validateTxSkelL mintSkel2 + (oRefScript2 : _) <- validateTxSkelL mintSkel3 - ((oRefScript1', _) : (oRefScript2', _) : _) <- - validateTxSkel' $ - txSkelTemplate + (oRefScript1' : oRefScript2' : _) <- + validateTxSkelL $ + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelInputs = HMap.fromList @@ -61,9 +61,9 @@ runScript = do txSkelMints = review txSkelMintsListI [burn script BurnToken tn1 1] } - ((oRefScript2'', _) : _) <- - validateTxSkel' $ - txSkelTemplate + (oRefScript2'' : _) <- + validateTxSkelL $ + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [bob], txSkelInputs = HMap.fromList @@ -77,7 +77,7 @@ runScript = do } validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelInputs = HMap.singleton oRefScript2'' (someTxSkelRedeemer Close), txSkelMints = review txSkelMintsListI [burn script BurnToken tn3 1] @@ -88,7 +88,7 @@ runScript = do let tn = txOutRefToToken oRef mints = review txSkelMintsListI [mint script (MintToken oRef) tn 1] mintValue = Script.toValue mints - in ( txSkelTemplate + in ( txSkelEmulatorTemplate { txSkelInputs = HMap.singleton oRef emptyTxSkelRedeemer, txSkelMints = mints, txSkelOutputs = [script `receives` InlineDatum index <&&> Value mintValue], diff --git a/tests/Spec/ProposingScript.hs b/tests/Spec/ProposingScript.hs index 842e659ea..5213612c1 100644 --- a/tests/Spec/ProposingScript.hs +++ b/tests/Spec/ProposingScript.hs @@ -24,17 +24,17 @@ testProposingScript autoRefScript autoConstitution constitution mScript govActio forceOutputs_ initialDistributionTemplate setConstitutionScript constitution validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [alice `receives` ReferenceScript constitution], txSkelSignatories = txSkelSignatoriesFromList [alice] } validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelCertificates = [pubKeyCertificate alice $ StakingRegisterDelegate (Api.DelegVote Api.DRepAlwaysAbstain)] } validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelProposals = [ TxSkelProposal @@ -69,37 +69,37 @@ tests = mustFailTest (testProposingScript False False checkProposingScript (Just alwaysTrueProposingValidator) (ParameterChange [FeePerByte 100])) `withFailureProp` isPhase1FailureWithMsg "InvalidPolicyHash" - `withLogProp` didNotHappen "MCLogAutoFilledConstitution", + `withLogProp` didNotHappen "CLogAutoFilledConstitution", testCooked "Success when executing the right constitution script" $ mustSucceedTest (testProposingScript False False alwaysTrueProposingValidator (Just alwaysTrueProposingValidator) (ParameterChange [FeePerByte 100])) - `withLogProp` didNotHappen "MCLogAutoFilledConstitution", + `withLogProp` didNotHappen "CLogAutoFilledConstitution", testCooked "Success when executing a more complex constitution script" $ mustSucceedTest (testProposingScript False False checkProposingScript (Just checkProposingScript) (ParameterChange [FeePerByte 100])) - `withLogProp` didNotHappen "MCLogAutoFilledConstitution", + `withLogProp` didNotHappen "CLogAutoFilledConstitution", testCooked "Failure when executing a more complex constitution script with the wrong proposal" $ mustFailInPhase2Test (testProposingScript False False checkProposingScript (Just checkProposingScript) (ParameterChange [FeePerByte 50])) - `withLogProp` didNotHappen "MCLogAutoFilledConstitution", + `withLogProp` didNotHappen "CLogAutoFilledConstitution", testCooked "Success when executing a more complex constitution script as a reference script" $ mustSucceedTest (testProposingScript True False checkProposingScript (Just checkProposingScript) (ParameterChange [FeePerByte 100])) - `withLogProp` happened "MCLogAddedReferenceScript" - `withLogProp` didNotHappen "MCLogAutoFilledConstitution" + `withLogProp` happened "CLogAddedReferenceScript" + `withLogProp` didNotHappen "CLogAutoFilledConstitution" ], testGroup "Automated constitution attachment" [ testCooked "Success when auto assigning the constitution script" $ mustSucceedTest (testProposingScript False True checkProposingScript Nothing (ParameterChange [FeePerByte 100])) - `withLogProp` happened "MCLogAutoFilledConstitution", + `withLogProp` happened "CLogAutoFilledConstitution", testCooked "Success when auto assigning the constitution script and using it as a reference script" $ mustSucceedTest (testProposingScript True True checkProposingScript Nothing (ParameterChange [FeePerByte 100])) - `withLogProp` happened "MCLogAddedReferenceScript" - `withLogProp` happened "MCLogAutoFilledConstitution", + `withLogProp` happened "CLogAddedReferenceScript" + `withLogProp` happened "CLogAutoFilledConstitution", testCooked "Success when auto assigning the constitution script while overriding an existing one" $ mustSucceedTest (testProposingScript False True checkProposingScript (Just alwaysFalseProposingValidator) (ParameterChange [FeePerByte 100])) - `withLogProp` happened "MCLogAutoFilledConstitution" + `withLogProp` happened "CLogAutoFilledConstitution" ] ] diff --git a/tests/Spec/ReferenceInputs.hs b/tests/Spec/ReferenceInputs.hs index 37aec79b1..c8111479d 100644 --- a/tests/Spec/ReferenceInputs.hs +++ b/tests/Spec/ReferenceInputs.hs @@ -15,9 +15,9 @@ instance PrettyCooked FooDatum where trace1 :: DirectMockChain () trace1 = do - (txOutRefFoo, _) : (txOutRefBar, _) : _ <- - validateTxSkel' - txSkelTemplate + txOutRefFoo : txOutRefBar : _ <- + validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [ fooTypedValidator `receives` Value (Script.ada 4) <&&> InlineDatum (FooDatum $ Script.toPubKeyHash $ wallet 3), barTypedValidator `receives` Value (Script.ada 5) @@ -25,7 +25,7 @@ trace1 = do txSkelSignatories = txSkelSignatoriesFromList [wallet 2] } validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton txOutRefBar $ someTxSkelRedeemer (), txSkelReferenceInputs = Set.singleton txOutRefFoo, txSkelOutputs = [wallet 4 `receives` Value (Script.ada 5)], @@ -34,9 +34,9 @@ trace1 = do trace2 :: DirectMockChain () trace2 = do - (refORef, _) : (scriptORef, _) : _ <- - validateTxSkel' - ( txSkelTemplate + refORef : scriptORef : _ <- + validateTxSkelL + ( txSkelEmulatorTemplate { txSkelOutputs = [ wallet 1 `receives` Value (Script.ada 2) <&&> VisibleHashedDatum (10 :: Integer), bazTypedValidator `receives` Value (Script.ada 10) @@ -45,7 +45,7 @@ trace2 = do } ) validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [wallet 1], txSkelInputs = Map.singleton scriptORef (someTxSkelRedeemer ()), txSkelReferenceInputs = Set.singleton refORef diff --git a/tests/Spec/ReferenceScripts.hs b/tests/Spec/ReferenceScripts.hs index b92a46a51..9f437cb44 100644 --- a/tests/Spec/ReferenceScripts.hs +++ b/tests/Spec/ReferenceScripts.hs @@ -18,9 +18,9 @@ putRefScriptOnWalletOutput :: Script.Versioned Script.Validator -> DirectMockChain V3.TxOutRef putRefScriptOnWalletOutput recipient referenceScript = - fst . head - <$> validateTxSkel' - txSkelTemplate + head + <$> validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [recipient `receives` ReferenceScript referenceScript], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } @@ -30,9 +30,9 @@ putRefScriptOnScriptOutput :: Script.Versioned Script.Validator -> DirectMockChain V3.TxOutRef putRefScriptOnScriptOutput recipient referenceScript = - fst . head - <$> validateTxSkel' - txSkelTemplate + head + <$> validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [recipient `receives` ReferenceScript referenceScript], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } @@ -42,14 +42,14 @@ checkReferenceScriptOnOref :: V3.TxOutRef -> DirectMockChain () checkReferenceScriptOnOref expectedScriptHash refScriptOref = do - (oref, _) : _ <- - validateTxSkel' - txSkelTemplate + oref : _ <- + validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [requireRefScriptValidator expectedScriptHash `receives` Value (Script.ada 42)], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton oref emptyTxSkelRedeemer, txSkelReferenceInputs = Set.singleton refScriptOref, txSkelSignatories = txSkelSignatoriesFromList [wallet 1] @@ -62,15 +62,15 @@ checkReferenceScriptOnOref expectedScriptHash refScriptOref = do useReferenceScript :: Wallet -> Bool -> Script.Versioned Script.Validator -> DirectMockChain P.Ledger.CardanoTx useReferenceScript spendingSubmitter consumeScriptOref theScript = do scriptOref <- putRefScriptOnWalletOutput (wallet 3) theScript - (oref, _) : _ <- - validateTxSkel' - txSkelTemplate + oref : _ <- + validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [theScript `receives` Value (Script.ada 42)], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } - fst + (\(_, _, tx, _) -> P.Ledger.CardanoEmulatorEraTx tx) <$> validateTxSkel - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.fromList $ (oref, TxSkelRedeemer () (Just scriptOref) False) @@ -81,23 +81,23 @@ useReferenceScript spendingSubmitter consumeScriptOref theScript = do useReferenceScriptInInputs :: Wallet -> Script.Versioned Script.Validator -> DirectMockChain () useReferenceScriptInInputs spendingSubmitter theScript = do scriptOref <- putRefScriptOnWalletOutput (wallet 1) theScript - (oref, _) : _ <- - validateTxSkel' - txSkelTemplate + oref : _ <- + validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [theScript `receives` Value (Script.ada 42)], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.fromList [(oref, TxSkelRedeemer () (Just scriptOref) False), (scriptOref, emptyTxSkelRedeemer)], txSkelSignatories = txSkelSignatoriesFromList [spendingSubmitter] } referenceMint :: Script.Versioned Script.MintingPolicy -> Script.Versioned Script.MintingPolicy -> Int -> Bool -> DirectMockChain () referenceMint mp1 mp2 n autoRefScript = do - ((!! n) -> (mpOutRef, _)) <- + (Map.elemAt n -> (mpOutRef, _)) <- validateTxSkel' $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [ wallet 1 `receives` Value (Script.ada 2) <&&> ReferenceScript mp1, wallet 1 `receives` Value (Script.ada 10) @@ -105,7 +105,7 @@ referenceMint mp1 mp2 n autoRefScript = do txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelMints = review txSkelMintsListI @@ -145,56 +145,60 @@ tests = ], testGroup "using reference scripts" - [ testCookedFromInitDistTemplate @DirectEffs "fail from transaction generation for missing reference scripts" $ + [ testCookedFromInitDistTemplate @DirectMockChainEffs "fail from transaction generation for missing reference scripts" $ mustFailTest ( do - consumedOref : _ <- getTxOutRefs $ utxosAtSearch (wallet 1) $ ensureAFoldIs (txSkelOutValueL % filtered (`Api.geq` Script.lovelace 42_000_000)) - (oref, _) : _ <- - validateTxSkel' - txSkelTemplate + consumedOref <- + utxosAt (wallet 1) + >>= ensureAFoldIs (txSkelOutValueL % filtered (`Api.geq` Script.lovelace 42_000_000)) + >>= retrieveTxOutRefs + >>= retrieve (Set.elemAt 0) + oref : _ <- + validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [Script.alwaysSucceedValidatorVersioned `receives` Value (Script.ada 42)], txSkelInputs = Map.singleton consumedOref emptyTxSkelRedeemer, txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton oref (TxSkelRedeemer () (Just consumedOref) False), txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } ) `withErrorProp` \case - MCEUnknownOutRef _ -> testSuccess + CEUnknownOutRef _ -> testSuccess _ -> testFailure, testCookedFromInitDistTemplate "fail from transaction generation for mismatching reference scripts" $ mustFailTest ( do scriptOref <- putRefScriptOnWalletOutput (wallet 3) Script.alwaysFailValidatorVersioned - (oref, _) : _ <- - validateTxSkel' - txSkelTemplate + oref : _ <- + validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [Script.alwaysSucceedValidatorVersioned `receives` Value (Script.ada 42)], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton oref (TxSkelRedeemer () (Just scriptOref) False), txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } ) `withErrorProp` \case - MCEWrongReferenceScriptError {} -> testSuccess + CEWrongReferenceScriptError {} -> testSuccess _ -> testFailure, testCookedFromInitDistTemplate "phase 1 - fail if using a reference script with 'someRedeemer'" $ mustFailInPhase1Test $ do scriptOref <- putRefScriptOnWalletOutput (wallet 3) Script.alwaysSucceedValidatorVersioned - (oref, _) : _ <- - validateTxSkel' - txSkelTemplate + oref : _ <- + validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [Script.alwaysSucceedValidatorVersioned `receives` Value (Script.ada 42)], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton oref emptyTxSkelRedeemerNoAutoFill, txSkelReferenceInputs = Set.singleton scriptOref, txSkelSignatories = txSkelSignatoriesFromList [wallet 1] @@ -239,16 +243,16 @@ tests = referenceMint Script.alwaysSucceedPolicyVersioned Script.alwaysSucceedPolicyVersioned 0 False, testCookedFromInitDistTemplate "succeed if relying on automated finding of reference minting policy" $ mustSucceedTest (referenceMint Script.alwaysSucceedPolicyVersioned Script.alwaysSucceedPolicyVersioned 0 True) - `withLogProp` happened "MCLogAddedReferenceScript", + `withLogProp` happened "CLogAddedReferenceScript", testCookedFromInitDistTemplate "fail if given the wrong reference minting policy" $ mustFailTest (referenceMint Script.alwaysFailPolicyVersioned Script.alwaysSucceedPolicyVersioned 0 False) `withErrorProp` \case - MCEWrongReferenceScriptError {} -> testSuccess + CEWrongReferenceScriptError {} -> testSuccess _ -> testFailure, testCookedFromInitDistTemplate "fail if referencing the wrong utxo" $ mustFailTest (referenceMint Script.alwaysSucceedPolicyVersioned Script.alwaysSucceedPolicyVersioned 1 False) `withErrorProp` \case - MCEWrongReferenceScriptError {} -> testSuccess + CEWrongReferenceScriptError {} -> testSuccess _ -> testFailure ] ] diff --git a/tests/Spec/Slot.hs b/tests/Spec/Slot.hs index f16b38306..409af0907 100644 --- a/tests/Spec/Slot.hs +++ b/tests/Spec/Slot.hs @@ -1,8 +1,8 @@ module Spec.Slot (tests) where -import Cooked.MockChain.Effect.Read -import Cooked.MockChain.Runtime.Error -import Cooked.MockChain.Runtime.State +import Cooked.Effect.Time +import Cooked.Runtime.Error +import Cooked.Runtime.State import Data.Default import Ledger.Slot qualified as P.Ledger import Ledger.Tx qualified as P.Ledger @@ -16,21 +16,23 @@ import Test.Tasty.QuickCheck runSlot :: Sem - '[ MockChainRead, - State MockChainState, + '[ Time, + State EmulatorState, + State ChainIndex, Fail, Error P.Ledger.ToCardanoError, - Error MockChainError + Error ChainError ] a -> - Either MockChainError a + Either ChainError a runSlot = run . runError - . runToCardanoErrorInMockChainError - . runFailInMockChainError + . mapError CEToCardanoError + . failToError CEFailure . evalState def - . runMockChainRead + . evalState def + . runMockChainTime tests :: TestTree tests = diff --git a/tests/Spec/Tweak/Common.hs b/tests/Spec/Tweak/Common.hs index 49e96ccaa..afe3b491d 100644 --- a/tests/Spec/Tweak/Common.hs +++ b/tests/Spec/Tweak/Common.hs @@ -13,7 +13,7 @@ alice :: Wallet alice = wallet 1 mkSkel :: [Integer] -> TxSkel -mkSkel l = set txSkelOutputsL (receives alice . Value . Script.lovelace <$> l) txSkelTemplate +mkSkel l = set txSkelOutputsL (receives alice . Value . Script.lovelace <$> l) txSkelEmulatorTemplate tests :: TestTree tests = diff --git a/tests/Spec/Tweak/Labels.hs b/tests/Spec/Tweak/Labels.hs index 45b0bdaf7..ff5970795 100644 --- a/tests/Spec/Tweak/Labels.hs +++ b/tests/Spec/Tweak/Labels.hs @@ -17,7 +17,7 @@ carrie = wallet 3 payTo :: Wallet -> Integer -> StagedMockChain () payTo target amount = do validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelOutputs = [target `receives` Value (Script.ada amount)] } diff --git a/tests/Spec/Withdrawals.hs b/tests/Spec/Withdrawals.hs index 8ee6340e2..91a2114af 100644 --- a/tests/Spec/Withdrawals.hs +++ b/tests/Spec/Withdrawals.hs @@ -21,7 +21,7 @@ testWithdrawingScript userCertifying userRewarding mAmount = do forceOutputs_ [alice `receives` Value (Script.ada 100)] when (isJust userCertifying) $ validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelCertificates = [ TxSkelCertificate (fromJust userCertifying) $ @@ -31,7 +31,7 @@ testWithdrawingScript userCertifying userRewarding mAmount = do ] } validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelWithdrawals = txSkelWithdrawalsFromList [Withdrawal userRewarding (Api.Lovelace . (1_000_000 *) <$> mAmount)] } @@ -64,7 +64,7 @@ tests = (scriptUserWithdrawing 0) Nothing ) - `withLogProp` happened "MCLogAutoFilledWithdrawalAmount", + `withLogProp` happened "CLogAutoFilledWithdrawalAmount", testCooked ".. but the script's logic might say No" $ mustFailTest ( testWithdrawingScript @@ -73,7 +73,7 @@ tests = Nothing ) `withFailureProp` isPhase2FailureWithMsg "Wrong quantity: 0 instead of 2000000" - `withLogProp` happened "MCLogAutoFilledWithdrawalAmount", + `withLogProp` happened "CLogAutoFilledWithdrawalAmount", testCooked "We cannot withdraw more than our rewards (0)" $ mustFailTest ( testWithdrawingScript @@ -82,7 +82,7 @@ tests = (Just 2) ) `withFailureProp` isPhase1FailureWithMsg "WithdrawalsNotInRewardsCERTS" - `withLogProp` didNotHappen "MCLogAutoFilledWithdrawalAmount", + `withLogProp` didNotHappen "CLogAutoFilledWithdrawalAmount", testCooked "A peer can also make a withdrawal" $ mustSucceedTest ( testWithdrawingScript @@ -90,5 +90,5 @@ tests = aliceUser Nothing ) - `withLogProp` happened "MCLogAutoFilledWithdrawalAmount" + `withLogProp` happened "CLogAutoFilledWithdrawalAmount" ]