This page describes what a Plinth contract project looks like — its shape, not the steps to build it. For a hands-on walk-through, follow Your first smart contract with Plinth instead.
The short version: a Plinth contract is an ordinary Haskell project whose one special move is a Template Haskell splice that turns the on-chain logic into a plain Haskell value. That single idea is what lets a project hold both the on-chain code and the off-chain code that deploys and interacts with it.
An ordinary Haskell project
Because Plinth is a subset of Haskell, a
Plinth project is just a cabal package. The entire cabal-install workflow
applies unchanged: dependencies are resolved from Hackage and the Cardano
package repository (CHaP), code is organised into modules, a library, and
executables or test suites, and cabal build builds it.
The single tweak is the compiler. Instead of stock GHC you build with
plinthc, most conveniently by setting it once in cabal.project:
with-compiler: /path/to/plinthc
packages: .
This is the persistent equivalent of passing cabal -w /path/to/plinthc on the
command line.
On-chain and off-chain in one project
The defining structural feature of a Plinth project is that the same package, built by the same compiler, holds two kinds of code bound for two different places:
- On-chain code — the validator and minting-policy logic (the scripts that guard spending an output and minting a token), written in the Plinth subset, that ends up as UPLC on the ledger.
- Off-chain code — ordinary Haskell that prepares, inspects, serialises, and deploys that on-chain code: blueprint generators, tests, and transaction-building.
These are not separate projects that share a data format by convention; they are the same Haskell program. What joins them is a Template Haskell splice.
The bridge: PlutusTx.compile
The on-chain logic starts life as an ordinary Haskell function. You turn it into on-chain code by quoting it and compiling it at compile time:
validatorCode :: CompiledCode (BuiltinData -> BuiltinUnit)
validatorCode = $$(PlutusTx.compile [|| validator ||])
Reading this:
[|| validator ||]is a typed Template Haskell quote of the function.$$(PlutusTx.compile ...)runs during compilation. This splice is exactly where the GHC plugin intercepts the quoted code and runs the Plinth-to-UPLC pipeline.- the result is a
CompiledCode a: an ordinary Haskell value holding the compiled UPLC program. The phantom typea(hereBuiltinData -> BuiltinUnit) records the Haskell type the program was compiled from.
The key idea is that the splice freezes the on-chain program into the host
program as a value. Once validatorCode is just a value, the rest of the
project — the off-chain half — can pick it up and work with it like
any other data. That is what lets one project hold both worlds.
(A handful of other Template Haskell helpers — makeLift,
makeIsDataIndexed — derive the glue that moves Haskell datatypes across
the boundary as on-chain Data. Writing the Plinth code itself, and these
helpers, are covered in
The Plinth contract language; here
we only need the splice.)
Working with CompiledCode off-chain
Everything past the splice is plain Haskell operating on a value — no
plugin involved. The plugin was needed only to produce the CompiledCode; what
the off-chain code does with it is where most of a project’s non-validator code
lives. Typically it:
-
Applies parameters. A parameterised script is a
CompiledCode (P -> V); before deployment you bake in its compile-time parameter withunsafeApplyCode(or the checkedapplyCode) together withliftCode, producing aCompiledCode V. This happens off-chain and changes the resulting script — and therefore its hash. The template does it right at the splice site:auctionValidatorScript params = $$(PlutusTx.compile [|| auctionUntypedValidator ||]) `PlutusTx.unsafeApplyCode` PlutusTx.liftCode plcVersion110 params -
Serialises it.
serialiseCompiledCode :: CompiledCode a -> SerialisedScriptproduces the flat-in-CBOR bytes the ledger actually runs (SerialisedScriptis aShortByteString). From those bytes come the script hash and the script address.import PlutusLedgerApi.Common (serialiseCompiledCode) script = serialiseCompiledCode validatorCode -
Describes it as a blueprint. A CIP-57 blueprint can be generated for one or more scripts directly in the off-chain code. Parts of it are derived from the scripts themselves — the argument schemas, the compiled code, its hash — while the rest of the metadata is filled in explicitly. See Generate a blueprint.
-
Builds transactions and tests. The serialised script can be handed to
cardano-api(or a test harness) to attach it to a transaction as an inline or reference script, and to exercise the validator off-chain.
A typical layout
Put together, a Plinth project is usually a library of on-chain validators
— each exposed as a CompiledCode, often as a function of its parameters
— plus executables or test suites for the off-chain side. The
project template is laid out
exactly this way: the plinth-validators library under src/, and the
gen-auction-validator-blueprint and gen-minting-policy-blueprint executables
under app/. The library is the on-chain half frozen into values; the
executables are the off-chain half consuming them.
Further reading
- From Plinth to the chain
— the pipeline the
compilesplice triggers, and how the serialised script reaches a transaction. - Contract blueprints (CIP-57) — what the off-chain blueprint generators emit.
- plinthc
— why building with
plinthcneeds no plugin configuration. - Your first smart contract with Plinth — the same template, built step by step.
- The Plinth contract language — writing the on-chain code itself.
- Plinth user guide — the complete reference for writing Plinth.