I have written a script that basically keeps a counter and every time it is spent increases said counter, the way it guarantees that the counter was increased is by checking the current datum of the output and making sure the new datum is current datum + 1 (The script has some additional preconditions which are not important right now):
{-|
Module : Horrocubes.Counter.
Description : Plutus script that keeps track of an internal counter.
License : Apache-2.0
Maintainer : [email protected]
Stability : experimental
This script keeps a counter and increases it everytime the eUTXO is spent.
-}
-- LANGUAGE EXTENSIONS --------------------------------------------------------
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ViewPatterns #-}
-- MODULE DEFINITION ----------------------------------------------------------
module Horrocubes.Counter
(
counterScript,
counterScriptShortBs,
CounterParameter(..)
) where
-- IMPORTS --------------------------------------------------------------------
import Cardano.Api.Shelley (PlutusScript (..), PlutusScriptV1)
import Codec.Serialise
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Short as SBS
import Ledger hiding (singleton)
import qualified Ledger.Typed.Scripts as Scripts
import Ledger.Value as Value
import qualified PlutusTx
import PlutusTx.Prelude as P hiding (Semigroup (..), unless)
import Data.Aeson (FromJSON, ToJSON)
import GHC.Generics (Generic)
import qualified Ledger.Contexts as Validation
import Text.Show
-- DATA TYPES -----------------------------------------------------------------
-- | The parameters for the counter contract.
data CounterParameter = CounterParameter {
ownerPkh :: !PubKeyHash, -- ^ The transaction that spends this output must be signed by the private key
identityNft :: !AssetClass -- ^ The NFT that identifies the correct eUTXO.
} deriving (Show, Generic, FromJSON, ToJSON)
PlutusTx.makeLift ''CounterParameter
-- | This Datum represents the state of the counter.
data CounterDatum = CounterDatum {
counter :: !Integer -- ^ The current counter value.
}
deriving Show
PlutusTx.unstableMakeIsData ''CounterDatum
-- | The Counter script type. Sets the Redeemer and Datum types for this script.
data Counter
instance Scripts.ValidatorTypes Counter where
type instance DatumType Counter = CounterDatum
type instance RedeemerType Counter = ()
-- DEFINITIONS ----------------------------------------------------------------
-- | Maybe gets the datum from the transatcion output.
{-# INLINABLE counterDatum #-}
counterDatum :: TxOut -> (DatumHash -> Maybe Datum) -> Maybe CounterDatum
counterDatum o f = do
dh <- txOutDatum o
Datum d <- f dh
PlutusTx.fromBuiltinData d
-- | Checks that the identity NFT is locked again in the contract.
{-# INLINABLE isIdentityNftRelocked #-}
isIdentityNftRelocked:: CounterParameter -> Value -> Bool
isIdentityNftRelocked params valueLockedByScript = assetClassValueOf valueLockedByScript (identityNft params) == 1
-- | Creates the validator script for the outputs on this contract.
{-# INLINABLE mkCounterValidator #-}
mkCounterValidator :: CounterParameter -> CounterDatum -> () -> ScriptContext -> Bool
mkCounterValidator parameters oldDatum _ ctx =
let oldCounterValue = counter oldDatum
isRightNexCounterValue = (newDatumValue == (oldCounterValue + 1))
isIdentityLocked = isIdentityNftRelocked parameters valueLockedByScript
in traceIfFalse "Wrong counter value" isRightNexCounterValue &&
traceIfFalse "Wrong balance" isIdentityLocked &&
traceIfFalse "Missing signature" isTransactionSignedByOwner
where
info :: TxInfo
info = scriptContextTxInfo ctx
ownOutput :: TxOut
ownOutput = case getContinuingOutputs ctx of
[o] -> o
_ -> traceError "Expected exactly one output"
newDatumValue :: Integer
newDatumValue = case counterDatum ownOutput (`findDatum` info) of
Nothing -> traceError "Counter output datum not found"
Just datum -> counter datum
valueLockedByScript :: Value
valueLockedByScript = Validation.valueLockedBy info (Validation.ownHash ctx)
isTransactionSignedByOwner :: Bool
isTransactionSignedByOwner = txSignedBy info (ownerPkh parameters)
-- | The script instance of the counter. It contains the mkCounterValidator function
-- compiled to a Plutus core validator script.
counterInstance :: CounterParameter -> Scripts.TypedValidator Counter
counterInstance counter = Scripts.mkTypedValidator @Counter
($$(PlutusTx.compile [|| mkCounterValidator ||]) `PlutusTx.applyCode` PlutusTx.liftCode counter) $$(PlutusTx.compile [|| wrap ||])
where
wrap = Scripts.wrapValidator @CounterDatum @()
-- | Gets the counter validator script that matches the given parameters.
counterValidator :: CounterParameter -> Validator
counterValidator params = Scripts.validatorScript . counterInstance $ params
-- | Generates the plutus script.
counterPlutusScript :: CounterParameter -> Script
counterPlutusScript params = unValidatorScript $ counterValidator params
-- | Serializes the contract in CBOR format.
counterScriptShortBs :: CounterParameter -> SBS.ShortByteString
counterScriptShortBs params = SBS.toShort . LBS.toStrict $ serialise $ counterPlutusScript params
-- | Gets a serizlized plutus script from the given parameters.
counterScript :: PubKeyHash -> AssetClass -> PlutusScript PlutusScriptV1
counterScript pkh ac = PlutusScriptSerialised $ counterScriptShortBs $ CounterParameter { ownerPkh = pkh, identityNft = ac }
I already locked some funds in the script and I am trying to spent the eUTXO using the CLI and I am having a conceptual issue here, there is no way to specify the new output datum using the CLI, it seems I can only specify the datum hash of the new datum.
cardano-cli transaction build-raw --alonzo-era --fee 500000 --tx-in b2997baf426caa94762e4baeed051ac13bad7994f2f3a43f8c43299d2ba8f050#0 --tx-in b2997baf426caa94762e4baeed051ac13bad7994f2f3a43f8c43299d2ba8f050#1 --tx-in-script-file ./counter/out2.plutus --tx-in-execution-units "(491845099,1197950)" --tx-in-datum-value 0 --tx-in-redeemer-value 0 --tx-in-collateral b2997baf426caa94762e4baeed051ac13bad7994f2f3a43f8c43299d2ba8f050#0 --tx-out "addr_test1wqesjavxsh7g2q8lf92ptyt7rnrhh07ghnyjq65ra50uwwqsssy2q+2000000+1 a1c6cefca22b4527acdf17a1d44674b6d7cf17c3e7e35cbd1a57d8b5.Horrocube09997" --tx-out-datum-hash ee155ace9c40292074cb6aff8c9ccdd273c81648ff1149ef36bcea6ebb8a3e25 --protocol-params-file protocol.json --out-file tx-script2.build
Using the CLI to construct the transaction you need to:
Specify the actual datum value matching the datum hash in the current eUTXO you want to spend. (--tx-in-datum-value)
Specify the new datum hash for the new eUTXO we are creating
(--tx-out-datum-hash)
However, the script needs both values in the validator as it verifies that the new datum value in the output is equal to the current datum value + 1 but it only has the current datum value and hash AND the new datum hash (not the value).
Using the plutus offchain code I can pass the new datum no problem:
-- | Tries to solve the puzzle at the given index.
solve :: forall w s. HasBlockchainActions s => SolveParams -> Contract w s Text ()
solve solveParams = do
let cube = CubeParameter {
cubeId = spCubeId solveParams,
stateMachineNft = spStateMachineNft solveParams
}
pkh <- pubKeyHash <$> Contract.ownPubKey
utxos <- utxoAt $ cubeAddress cube
addressUtxos <- utxoAt $ pubKeyHashAddress pkh
let constriants = Constraints.unspentOutputs utxos <>
Constraints.unspentOutputs addressUtxos <>
Constraints.otherScript (Scripts.validatorScript (cubeInstance cube)) <>
Constraints.scriptInstanceLookups (cubeInstance cube) <>
Constraints.ownPubKeyHash pkh
m <- findCubeOutput cube
case m of
Nothing -> logInfo @String "Cube output not found for solve parameters "
Just (_, _, dat) -> do
let datum = dat { currentPuzzleIndex = spPuzzleIndex solveParams + 1 }
redeemmer = CubeRedeemer (spPuzzleIndex solveParams) (spAnswer solveParams)
totalValue = Prelude.foldMap (Tx.txOutValue . Tx.txOutTxOut) utxos
orefs = fst <$> Map.toList utxos
payToSelf = assetClassValue (cubeId cube) 1 -- We must pay to outselves the cube so we can prove ownership of the cube.
payToScript = (buildValue cube datum (spPuzzleIndex solveParams) totalValue)
tx = mconcat [Constraints.mustSpendScriptOutput oref (Redeemer (PlutusTx.toData redeemmer)) | oref <- orefs] <>
Constraints.mustPayToTheScript datum payToScript <>
Constraints.mustPayToPubKey pkh payToSelf
ledgerTx <- submitTxConstraintsWith @Cube constriants tx
void $ awaitTxConfirmed $ txId ledgerTx
In fact, using the Plutus code I only pass the new datum value to "submitTxConstraintsWith" which now that I think I also don't understand, where is the Plutus off chain code getting the current datum value from? since the output only store the datum hash? Anyways the Plutus API works. But I am trying to use the CLI to use the contract and I don't see a way to pass the actual datum value (not just the hash) to the script so the validator can run.