Operations over Contract
Having understood the contract. Now is the time we actually start using our framework to build the transactions for it.
The main principle to understand here is that - we only need to give the essentials, i.e. we only specify what we want and it is the job of the framework to do the rest.
For instance, we may tell that we want to consume a specific input belonging to the script's address and generate a specific output. Given that, it becomes framework's job to do the rest, say:
- Select available UTxO's in user's wallet and generate suitable change output to balance the transaction, considering fees.
- Make sure all generated UTxO's satisfy minimum ada requirement.
- Handle collateral.
- etc, etc.
Thus, we only specify at high-level what we want. This would become clear as we actually start writing operations for our contract.
Entire code for these operations is available here (opens in a new tab).
When working with Atlas inside your project, since Atlas isn't on Hackage, you'll need to specify (opens in a new tab) it as a remote package inside your cabal.project
. Moreover, since Atlas itself relies on dependencies which are outside Hackage, those would need to be specified too. To streamline this, it's best to use the cabal.project
(opens in a new tab) mentioned in atlas-examples
repository where you would just need to modify packages:
stanza depending upon your project.
Operation 1: Generating address for our Smart Contract
Generating Validator for our Smart Contract
Following the usual drill, we generate the Validator given contract parameters (following is written in file Compiled.hs
(opens in a new tab)):
-- | Generates validator given params.
betRefValidator :: BetRefParams -> PlutusTx.CompiledCode (PlutusTx.BuiltinData -> PlutusTx.BuiltinData -> PlutusTx.BuiltinData -> ())
betRefValidator betRefParams =
$$(PlutusTx.compile [|| mkBetRefValidator||]) `PlutusTx.unsafeApplyCode` PlutusTx.liftCode plcVersion100 betRefParams
Note that since spending validator takes in a datum, redeemer and a script context and thus it's type signature, PlutusTx.CompiledCode (PlutusTx.BuiltinData -> PlutusTx.BuiltinData -> ())
takes three PlutusTx.BuiltinData
. We encapsulate it with GYValidator
inside framework.
Likewise minting policies and stake validators take in only redeemer and script context, thus having type PlutusTx.CompiledCode (PlutusTx.BuiltinData -> PlutusTx.BuiltinData -> PlutusTx.BuiltinData -> ())
which is represented in Atlas as GYMintingPolicy
and GYStakeValidator
respectively.
GYValidator
, GYMintingPolicy
and GYStakeValidator
are nothing but newtype
wrapper around GYScript
where GYScript
holds information of raw serialized Plutus script, version of Plutus ledger language (opens in a new tab) and script's hash.
Thus encapsulating GYScript
under say GYValidator
gives us additional context regarding script's purpose.
Checkout GeniusYield.Types.Script
(opens in a new tab) module for definitions of these types and corresponding helper utilities.
The mentioned GeniusYield.Types.Script
module contains a lot of helper utilities such as validatorFromPlutus
which takes in Plutus's PlutusTx.CompiledCode (PlutusTx.BuiltinData -> PlutusTx.BuiltinData -> PlutusTx.BuiltinData -> ())
type to give out GYValidator
. Though there has been slight abuse in mentioning type here as what is actually given out is GYValidator v
where type variable v
is of kind PlutusVersion
which is defined in GeniusYield.Types.PlutusVersion
(opens in a new tab) module and is used to tag plutus ledger version of our validator script1.
If we look at the type signature of validatorFromPlutus
, we see: validatorFromPlutus :: forall v. SingPlutusVersionI v => PlutusTx.CompiledCode (PlutusTx.BuiltinData -> PlutusTx.BuiltinData -> PlutusTx.BuiltinData -> ()) -> GYValidator v
where for the time being we can ignore the description of the typeclass SingPlutusVersionI
1 besides noting the fact that only types (currently 'PlutusV1
& 'PlutusV2
) of kind PlutusVersion
have an instance for it. So here, our function validatorFromPlutus
works for all type variable v
which have an instance of SingPlutusVersionI
but there is no way to learn what this v
is based solely on the input PlutusTx.CompiledCode (PlutusTx.BuiltinData -> PlutusTx.BuiltinData -> PlutusTx.BuiltinData -> ())
and therefore, caller must specify it, either by providing type signature (of callee or caller due to type inference) or by using visible type application (opens in a new tab). Our first operation does make use of it but before looking at it, we need to understand about GYTxQueryMonad
.
Interlude - GYTxQueryMonad
When we want to obtain the address of the script from its hash, besides the hash, we also need to know the network we are currently operating at. Is it some testnet or mainnet?
Similarly, transaction building involves querying the ledger for various information like say querying UTxO's present at one's address, similarly it might need help of some chain indexer to query datum in case output contains only the datum's hash.
All of this is captured by typeclass GYTxQueryMonad
documented here (opens in a new tab). It is strongly advised to see methods made available by it.
So, if we are working inside a monad which happens to also provide an instance for it, we would happily be able to query such an information.
Generating address
In this operation, we only need to obtain network details with the help of this monad. Here is the code to obtain address (notice that we have provided multiple versions of the same code here):
Type of scriptAddress
(opens in a new tab) used below is scriptAddress :: forall (m :: * -> *) (v :: PlutusVersion). GYTxQueryMonad m => GYValidator v -> m GYAddress
. Thus with respect to type application, the first parameter is for monad and second one is PlutusVersion
kinded.
Internally this function queries for network details.
-- A. Type is given by `scriptAddress`.
-- | Validator in question, obtained after giving required parameters.
betRefValidator' :: SingPlutusVersionI v => BetRefParams -> GYValidator v
betRefValidator' brp = validatorFromPlutus $ betRefValidator brp
-- | Address of the validator, given params.
betRefAddress :: (HasCallStack, GYTxQueryMonad m) => BetRefParams -> m GYAddress
betRefAddress brp = scriptAddress @_ @'PlutusV2 $ betRefValidator' brp
Well what is this monad m
being used here? Well any! As long as it has an instance for GYTxQueryMonad
. When we will start writing tests, then we'll use all of these operations and most likely how to use them would become clear then.
Operation 2: Adding Input to refer later (Reference Input)
Interlude - GYTxSkeleton
As mentioned before, we just mention at high level what we want in a transaction. This is captured by GYTxSkeleton
(opens in a new tab) datatype and its description is mentioned below.
Fields | Represented by | Additional details |
---|---|---|
Inputs | gytxIns | It is a list of inputs where for each input, we have its UTxO reference (the "TxIn" as the cardano ledger specification (opens in a new tab) calls it) and a witness. In case this UTxO doesn't belong to a script, we just need spending key witness, otherwise we need the associated script, its datum and input redeemer where the associated script could be provided as part of this transaction body or could be obtained from reference input. See GeniusYield.Types.TxIn (opens in a new tab). |
Outputs | gytxOuts | List of outputs produced by this transaction where for each output we can mention whether the datum is to be inlined or not and whether this output stores any script. See GeniusYield.Types.TxOut (opens in a new tab). |
Reference Inputs | gytxRefIns | Set of reference to UTxOs corresponding to reference inputs. |
Mints | gytxMint | Map of minting policy to pair of redeemer and another map for token name to mint amount for that token. |
Withdrawals | gytxWdrls | It is a list of withdrawals. Each withdrawal is specified by the concerned stake address with it's associated available rewards and witness. Witness could either be a key witness or a script witness. |
Signatories | gytxSigs | Set of Public Key Hash of Signatories. |
Certificates | gytxCerts | List of transaction certificates. |
Valid after | gytxInvalidBefore | Just the corresponding node slot. |
Valid before | gytxInvalidAfter | Same as above. |
Metadata | gytxMetadata | Transaction metadata. |
When constructing the transaction, we just need to specify what we want in this skeleton.
This skeleton naturally has a monoid instance where two skeletons are combined by running mappend
over each of their fields. We have utility functions defined in the same (GeniusYield.TxBuilder.Class
(opens in a new tab)) module like:
mustHaveOutput :: GYTxOut v -> GYTxSkeleton v
mustHaveOutput o = emptyGYTxSkeleton {gytxOuts = [o]}
mustHaveInput :: GYTxIn v -> GYTxSkeleton v
mustHaveInput i = emptyGYTxSkeleton {gytxIns = [i]}
mustHaveRefInput :: VersionIsGreaterOrEqual v PlutusV2 => GYTxOutRef -> GYTxSkeleton v
mustHaveRefInput i = emptyGYTxSkeleton { gytxRefIns = GYTxSkeletonRefIns (Set.singleton i) }
mustMint :: GYMintingPolicy u -> GYRedeemer -> GYTokenName -> Integer -> GYTxSkeleton v
mustMint p r tn n = emptyGYTxSkeleton {gytxMint = Map.singleton (Some p) (Map.singleton tn n, r)}
mustBeSignedBy :: GYPubKeyHash -> GYTxSkeleton v
mustBeSignedBy pkh = emptyGYTxSkeleton {gytxSigs = Set.singleton pkh}
isInvalidBefore :: GYSlot -> GYTxSkeleton v
isInvalidBefore s = emptyGYTxSkeleton {gytxInvalidBefore = Just s}
isInvalidAfter :: GYSlot -> GYTxSkeleton v
isInvalidAfter s = emptyGYTxSkeleton {gytxInvalidAfter = Just s}
mustHaveTxMetadata :: Maybe GYTxMetadata -> GYTxSkeleton v
mustHaveTxMetadata m = emptyGYTxSkeleton {gytxMetadata = m}
mustHaveWithdrawal :: GYTxWdrl v -> GYTxSkeleton v
mustHaveWithdrawal w = mempty {gytxWdrls = [w]}
mustHaveCertificate :: GYTxCert v -> GYTxSkeleton v
mustHaveCertificate c = mempty {gytxCerts = [c]}
Thus we can specify that our transaction must have this output (using mustHaveOutput
) and that output and must have this input (using mustHaveInput
) and so on... and combine them all into a single skeleton using mappend
.
Skeleton for adding reference input
Here we want to create an output at a given address (Oracle's address) with the given datum. This UTxO is to be later used as a reference input by script where the script would refer to its datum. Here we have decided to keep this datum inline.
-- | Add UTxO to be used as reference input at a given address with given datum.
addRefInput :: GYAddress -> OracleAnswerDatum -> GYTxSkeleton 'PlutusV2
addRefInput addr dat =
mustHaveOutput $ GYTxOut addr mempty (Just (datumFromPlutusData dat, GYTxOutUseInlineDatum)) Nothing
-- Note that the value can be empty as tx building logic would add the needed minimum UTxO ada.
Note that we have mentioned the value as empty for this UTxO and this is one of the beauties of our framework that it will itself manage adding lovelaces to satisfy minimum ada requirement.
Q: Can you create a skeleton for adding reference script?
Toggle Answer
Given the output address addr :: GYAddress
and the Plutus V2 validator script :: GYValidator 'PlutusV2
, we can write mustHaveOutput $ GYTxOut addr mempty (Just (datumFromPlutusData (), GYTxOutDontUseInlineDatum)) (Just $ validatorToScript script)
Operation 3: Placing a bet
Placing the first bet
In case this is a first bet (a program handling the bets can decide whether the bet being placed by the user is first or not by querying the UTxOs at the script address), then we just need to produce an output at the script address with the bet value and our guess.
-- | Operation to place bet.
placeBet :: (HasCallStack, GYTxMonad m)
=> GYTxOutRef -- ^ Reference Script.
-> BetRefParams -- ^ Validator Params.
-> OracleAnswerDatum -- ^ Guess.
-> GYValue -- ^ Bet amount to place.
-> GYAddress -- ^ Own address.
-> Maybe GYTxOutRef -- ^ Reference to previous bets UTxO (if any).
-> m (GYTxSkeleton PlutusV2)
placeBet refScript brp guess bet ownAddr mPreviousBetsUtxoRef = do
pkh <- addressToPubKeyHash' ownAddr
betAddr <- betRefAddress brp
case mPreviousBetsUtxoRef of
-- This is the first bet.
Nothing -> do
return $ mustHaveOutput $ GYTxOut
{ gyTxOutAddress = betAddr
, gyTxOutValue = bet
, gyTxOutDatum = Just (datumFromPlutusData $ BetRefDatum [(pubKeyHashToPlutus pkh, guess)] (valueToPlutus bet), GYTxOutDontUseInlineDatum)
, gyTxOutRefS = Nothing
}
At this point, it should be clear what is happening in the above code block. This function is somewhat overloaded and is handling both the cases whether the bet is first or not and it determines this using the presence of reference to a UTxO (representing previous bets) at validator script. In case there isn't one, i.e., Nothing
for our Maybe
value, we are placing the first bet. Notice that we mention that our datum shouldn't be inlined to output using GYTxOutDontUseInlineDatum
.
Placing subsequent bets
Here we would be exercising script's logic for the first time. We would be consuming the UTxO present at script address. We have defined a function, viz. input
which would take in the following parameters:
BetRefParams
: to generate the validator script or else we can read the script from the UTxO pertaining to reference script.- Reference to reference script UTxO.
- Reference of script input to consume.
- The datum present at this input. Recall that our datum was not inlined for this particular output, we therefore would need lookup the datum using
lookupDatum
to pass the actual datum to this function. - Redeemer action.
Thus, we have its definition as:
-- | Utility function to consume script UTxO.
input :: BetRefParams -> GYTxOutRef -> GYTxOutRef -> BetRefDatum -> BetRefAction -> GYTxSkeleton 'PlutusV2
input brp refScript inputRef dat red =
mustHaveInput GYTxIn
{ gyTxInTxOutRef = inputRef
, gyTxInWitness = GYTxInWitnessScript
(GYInReference refScript $ validatorToScript $ betRefValidator' brp)
(datumFromPlutusData dat)
(redeemerFromPlutusData red)
}
In case we didn't want to use reference script, we would write gyTxInWitness
as:
gyTxInWitness = GYTxInWitnessScript
(GYInScript (validatorToScript $ betRefValidator' brp))
(datumFromPlutusData dat)
(redeemerFromPlutusData red)
Following is the complete code for handling this case. Few comments to facilitate its understanding:
-
We first query the UTxO corresponding to previous bets at script address and we then query for its datum using
utxoDatum'
which tries its best to retrieve the datum and raises an exception in case it fails. This is its signature:utxoDatum' :: (GYTxQueryMonad m, Plutus.FromData a) => GYUTxO -> m (GYAddress, GYValue, a)
.- Note:
utxoAtTxOutRef'
(opens in a new tab) is a wrapper aroundutxoAtTxOutRef
(opens in a new tab) which raises an exception in case the result wasNothing
.
- Note:
-
We then see the use of
gyLogDebug'
which as you would expect is for logging purposes. The first argument that it takes correspond to namespace as used byKatip
(opens in a new tab). This is where integration of off-chain and on-chain code really begins to shine ✨, having theShow
instance defined for some of our on-chain types allows us to log them. -
timeFromPlutus
is as you'll expect - gives us the framework's representation of time from that of plutus. AndenclosingSlotFromTime'
uses ledger's information to determine the corresponding slot for the given time. We need this as cardano's node work in slots. We mention that our transaction is to be invalid after this slot usingisInvalidAfter
. -
We mention that our transaction must have our public key hash as signatories when plutus smart contract asks for it using
mustBeSignedBy
. -
Lastly, this transaction must generate an output to the script's address with the updated datum and added value.
valueToPlutus
gives the corresponding value type used by plutus from what we have in our framework (viz.,GYValue
).
All these skeletons are combined together using mappend
defined for GYTxSkeleton
.
-- Need to append to previous.
Just previousBetsUtxoRef -> do
previousUtxo <- utxoAtTxOutRef' previousBetsUtxoRef
(_addr, previousValue, dat@(BetRefDatum previousGuesses _previousBet)) <- utxoDatum' previousUtxo
gyLogDebug' "" $ printf "previous guesses %s" (show previousGuesses)
betUntilSlot <- enclosingSlotFromTime' (timeFromPlutus $ brpBetUntil brp)
gyLogDebug' "" $ printf "bet until slot %s" (show betUntilSlot)
return $
input brp refScript previousBetsUtxoRef dat (Bet guess)
<> mustHaveOutput GYTxOut
{ gyTxOutAddress = betAddr
, gyTxOutValue = bet <> previousValue
, gyTxOutDatum = Just (datumFromPlutusData $ BetRefDatum ((pubKeyHashToPlutus pkh, guess) : previousGuesses) (valueToPlutus bet), GYTxOutDontUseInlineDatum)
, gyTxOutRefS = Nothing
}
<> isInvalidAfter betUntilSlot
<> mustBeSignedBy pkh
Operation 4: Taking the bet pot
At this point, reading following code snippet should make sense as it is similar to what we have done before. Here note that we are using mustHaveRefInput
to tell that the transaction must have the following UTxO reference as a reference input.
Observe that we don't need to specify that the value we successfully consume from the script's UTxO must reach us because transaction balancer would add the change output to us.
-- | Operation to take UTxO corresponding to previous bets.
takeBets :: (HasCallStack, GYTxMonad m)
=> GYTxOutRef -- ^ Reference Script.
-> BetRefParams -- ^ Validator params.
-> GYTxOutRef -- ^ Script UTxO to consume.
-> GYAddress -- ^ Own address.
-> GYTxOutRef -- ^ Oracle reference input.
-> m (GYTxSkeleton PlutusV2)
takeBets refScript brp previousBetsUtxoRef ownAddr oracleRefInput = do
pkh <- addressToPubKeyHash' ownAddr
previousUtxo <- utxoAtTxOutRef' previousBetsUtxoRef
(_addr, _previousValue, dat) <- utxoDatum' previousUtxo
betRevealSlot <- enclosingSlotFromTime' (timeFromPlutus $ brpBetReveal brp)
return $
input brp refScript previousBetsUtxoRef dat Take
<> isInvalidBefore betRevealSlot
<> mustHaveRefInput oracleRefInput
<> mustBeSignedBy pkh
Additional Useful Features
utxosDatums
Sometimes we want to see all valid UTxOs at our script address. In Cardano, anyone can form UTxO at any address and such a UTxO need not have valid datum as required by our script. utxosDatums
can be used wither out invalid ones. See it's usage in Vesting example.
mustMint
We weren't minting any tokens in our example here and thus didn't make use of mustMint
skeleton function. It's sample usage is given in this (opens in a new tab) example. Which also illustrates how one can mint NFT and shows usage of someUTxO
function which essentially gives some random UTxO belonging to wallet.
Withdrawals, Stake Validator & Stake Certificates
We haven't made use of withdrawals, stake certificates and stake validators in our example. A sample illustration is provided in this (opens in a new tab) privnet test.
Footnotes
-
This is making use of "singletons" and one can read about it from the "Dependent Types" chapter (the last one) in Thinking with Types (opens in a new tab) book. ↩ ↩2