Skip to content

Commit

Permalink
Howto: exporting a Plutus contract blueprint
Browse files Browse the repository at this point in the history
  • Loading branch information
Unisay committed Mar 12, 2024
1 parent 1f9b73f commit d230b84
Show file tree
Hide file tree
Showing 2 changed files with 244 additions and 0 deletions.
243 changes: 243 additions & 0 deletions doc/read-the-docs-site/howtos/exporting-a-blueprint.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
.. highlight:: haskell
.. _exporting_a_blueprint:

How to produce a Plutus Contract Blueprint (CIP-57)
===================================================

`CIP-0057`_ Plutus Contract Blueprints are used to document the binary interface of a
Plutus contract in a machine-readable format (JSON schema).

A contract Blueprint can be produced by using the
`writeBlueprint` function exported by the `PlutusTx.Blueprint` module::

writeBlueprint
:: FilePath
-- ^ The file path where the blueprint will be written to,
-- e.g. '/tmp/plutus.json'
-> Blueprint '[MyParams, MyRedeemer, MyDatum]
-- ^ Contains all the necessary information to generate
-- a blueprint for a Plutus contract.
-> IO ()

In order to demonstrate the usage of the `writeBlueprint` function,
Let's consider the following example validator function and its interface::

data MyParams = MyParams
{ myBool :: Bool
, myInteger :: Integer
}
deriving anyclass (AsDefinitionId)

$(PlutusTx.makeLift ''Params)
$(makeIsDataSchemaIndexed ''Params [('MkParams, 0)])
type MyDatum = Integer

type MyRedeemer = ()

typedValidator :: MyParams -> MyDatum -> MyRedeemer -> ScriptContext -> Bool
typedValidator datum redeemer scriptContext = ...

untypedValidator :: MyParams -> BuiltinData -> BuiltinData -> BuiltinData -> ()
untypedValidator params datum redeemer _scriptContext =
check $ typedValidator
params
(unsafeFromBuiltinData datum)
(unsafeFromBuiltinData redeemer)

First of all we need to import required functionality::

import PlutusTx.Blueprint
( ContractBlueprint (..)
, Preamble (..)
, Purpose (..)
, ValidatorBlueprint (..)
, ArgumentBlueprint (..)
, ParameterBlueprint (..)
, AsDefinitionId (..)
, definitionRef
, deriveSchemaDefinitions
)
import PlutusTx.Blueprint.TH (makeIsDataSchemaIndexed)

Then we define a list of data types used at the top level of the typed interface
(excluding `ScriptContext`)::

type ContractTypes = '[MyParams, MyDatum, MyRedeemer]

Next we define a contract blueprint value of the following type::

data ContractBlueprint (referencedTypes :: [Type]) = MkContractBlueprint
{ contractId :: Maybe Text
-- ^ An optional identifier for the contract.
, contractPreamble :: Preamble
-- ^ An object with meta-information about the contract.
, contractValidators :: Set (ValidatorBlueprint referencedTypes)
-- ^ A set of validator blueprints that are part of the contract.
, contractDefinitions :: Map DefinitionId (Schema referencedTypes)
-- ^ A registry of schema definitions used across the blueprint.
}

.. note::
The 'referencedTypes' phantom type parameter is used to track the types used in the contract
making sure their schemas are included in the blueprint and that they are referenced
in a type-safe way.

The blueprint will contain JSON schema definitions for all the types used in the contract,
including the types **nested** within the top-level types (`MyParams`, `MyDatum`, `MyRedeemer`):
* `Integer` - nested within `MyDatum` and `MyParams`.
* `Bool` - nested within `MyParams`.
* `()` - nested within `MyRedeemer`.

This way, the `referencedTypes` type variable should be specialized to::

'[ MyParams -- top-level type
, MyDatum -- top-level type
, MyRedeemer -- top-level type
, Integer -- nested type
, Bool -- nested type
, () -- nested type
]

A blueprint author can always write a full list of types manually but it is tedious and error-prone.
Instead, it is recommended to use the ``Blueprint topLevelTypes`` type alias instead, which when given
a list of top-level types, automatically extends this list to also include all the nested types::

Blueprint '[MyParams, MyDatum, MyRedeemer]

is equivalent to::

ContractBlueprint '[MyParams, MyDatum, MyRedeemer, Integer, Bool, ()]'


We can construct a value of this type like in this::
myContractBlueprint :: Blueprint ContractTypes
myContractBlueprint = MkContractBlueprint
{ contractId = Just "my-contract"
, contractPreamble = myPreamble -- defined below
, contractValidators = Set.singleton myValidator -- defined below
, contractDefinitions = deriveSchemaDefinitions
}

The `contractId` field is optional and can be used to give a unique identifier to the contract.

The `contractPreamble` field is a value of type `PlutusTx.Blueprint.Preamble`
contains a meta-information about the contract::

data Preamble = MkPreamble
{ preambleTitle :: Text
-- ^ A short and descriptive title of the contract application
, preambleDescription :: Maybe Text
-- ^ A more elaborate description
, preambleVersion :: Text
-- ^ A version number for the project.
, preamblePlutusVersion :: PlutusVersion
-- ^ The Plutus version assumed for all validators
, preambleLicense :: Maybe Text
-- ^ A license under which the specification
-- and contract code is distributed
}

Here is an example construction::

myPreamble :: Preamble
myPreamble = MkPreamble
{ preambleTitle = "My Contract"
, preambleDescription = Just "A simple contract"
, preambleVersion = "1.0.0"
, preamblePlutusVersion = PlutusV2
, preambleLicense = Just "MIT"
}

The `contractDefinitions` field is a map of schema definitions used across the blueprint.
It can be constructed using the `deriveSchemaDefinitions` function which automatically
constructs all the schema definitions used in the contract (it does so by using the `ContractTypes`
list we defined earlier, discovering it via the type-inference mechanism).

Since every type in the `ContractTypes` list is going to have its derived JSON-schema in the
`contractDefinitions` map under a certain unique `DefinitionId` key, we need to make sure that it
has:
* an instance of the `AsDefinitionId` type class. Most of the times it could be derived using
the the `anyclass` strategy, for example::
{-# LANGUAGE DerivingStrategies, StandaloneDeriving #-}
deriving instance anyclass AsDefinitionId MyParams
* an instance of the `HasSchema` type class. If your validator exposes standard supported types
like `Integer` or `Bool` you don't need to define this instance. If your validator uses custom types
then you should be deriving it using the `makeIsDataSchemaIndexed` Template Haskell function,
which derives it alongside with the corresponding `ToBuiltinData`/`FromBuiltinData` instances,
for example::
{-# LANGUAGE TemplateHaskell #-}
$(makeIsDataSchemaIndexed ''MyDatum [('MkMyDatum, 0)])

.. note::

If we forget to include a type in the `ContractTypes` list, the `deriveSchemaDefinitions` function
will fail to compile with a meaningful message, thus ensuring that JSON schema definitions of
all the types used in the contract are included in the blueprint.

Finally, we need to define a validator blueprint for each validator used in the contract.

Our contract can contain one or more validators and for each one we need to provide
a description as a value of the following type::

data ValidatorBlueprint (referencedTypes :: [Type]) = MkValidatorBlueprint
{ validatorTitle :: Text
-- ^ A short and descriptive name for the validator.
, validatorDescription :: Maybe Text
-- ^ An informative description of the validator.
, validatorRedeemer :: ArgumentBlueprint referencedTypes
-- ^ A description of the redeemer format expected by this validator.
, validatorDatum :: Maybe (ArgumentBlueprint referencedTypes)
-- ^ A description of the datum format expected by this validator.
, validatorParameters :: Maybe (NonEmpty (ParameterBlueprint referencedTypes))
-- ^ A list of parameters required by the script.
, validatorCompiledCode :: Maybe ByteString
-- ^ A full compiled and CBOR-encoded serialized flat script.
}

In our example this would be::

myValidator :: ValidatorBlueprint ContractTypes
myValidator = MkValidatorBlueprint
{ validatorTitle = "My Validator"
, validatorDescription = Just "An example validator"
, validatorRedeemer = definitionRef @MyRedeemer
, validatorDatum = Just (definitionRef @MyDatum)
, validatorParameters = Just $ pure $
MkParameterBlueprint
{ parameterTitle = Just "My Validator Parameters"
, parameterDescription = Just "Parameters configure the validator in compile-time"
, parameterPurpose = Set.singleton Spend
, parameterSchema = definitionRef @MyParams
}
, validatorRedeemer =
MkArgumentBlueprint
{ argumentTitle = Just "My Redeemer"
, argumentDescription = Just "A redeemer that does something awesome"
, argumentPurpose = Set.fromList [Spend, Mint]
, argumentSchema = definitionRef @MyRedeemer
}
, validatorDatum = Just
MkArgumentBlueprint
{ argumentTitle = Just "My Datum"
, argumentDescription = Just "A datum that contains something awesome"
, argumentPurpose = Set.singleton Spend
, argumentSchema = definitionRef @MyDatum
}
, validatorCompiledCode = Nothing -- you can optionally provide the compiled code here
}

The `definitionRef` function is used to reference a schema definition of a given type. It is
smart enough to discover the schema definition of the given type from the `ContractTypes` list and
fails to compile if the referenced type is not included in the list.

With all the pieces in place, we can now write the blueprint to a file::

writeBlueprint "/tmp/plutus.json" myContractBlueprint

.. note::
You can find a more elaborate example of a contract blueprint in the `Blueprint.Tests`
module of the plutus repository.

.. _CIP-0057: https://cips.cardano.org/cip/CIP-0057
1 change: 1 addition & 0 deletions doc/read-the-docs-site/howtos/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ How-to guides
:titlesonly:

exporting-a-script
exporting-a-blueprint
profiling-scripts

0 comments on commit d230b84

Please sign in to comment.