Looking to contribute to Plutarch? Looking for functionalities that are not currently provided by Plutarch from a safe interface? You've come to the right place!
Table of Contents
Note: If you spot any mistakes/have any related questions that this guide lacks the answer to, please don't hesitate to raise an issue. The goal is to have high quality documentation for Plutarch developers!
You should generally follow the MLabs style guide, credit to @Koz Ross.
Discouraged Extensions
ImportQualifiedPost
RecordWildCards
Remember to run ./bin/format
to format your code and cabal test
, alongside cabal test -f development
, to make sure all the tests pass prior to making a PR!
If your PR makes a change to some user facing functionality - please summarize the change(s) and add it to CHANGELOG.md
.
More often than not, you'll be making PRs directly to master
.
However, sometimes, there is a release cycle going on and the state of the repository is in flux. Everything is unstable as rapid changes are occuring. In this case, all development take place in the staging
branch - all the PRs are based on top off staging
, and they all target staging
. This is the workflow that took place during the Plutarch 1.1 release cycle.
Even if certain functionalities are absent from the public facing API - you can always implement them using functions like punsafeConstant
and punsafeBuiltin
- these allow you to walk the lines between Plutus core and Plutarch.
A general familiarity with Plutus core is important. You can learn all of that through the following documents-
Parts of the Pluto guide may also prove useful.
NOTE: The following information is almost never necessary with the existence of
pconstant
. Refer to constant building andPConstant
/PLift
section of the Plutarch user guide.
Often, you will need to build a Plutus core constant. You can do this using Some
and ValueOf
. Here's how pcon PTrue
creates a Plutarch term that actually evaluates to a Plutus core constant representing a boolean-
import qualified PlutusCore as PLC
pcon' PTrue = punsafeConstant . PLC.Some $ PLC.ValueOf PLC.DefaultUniBool True
pcon' PFalse = punsafeConstant . PLC.Some $ PLC.ValueOf PLC.DefaultUniBool False
There's a lot to unpack here - but the general pattern is always the same. First step is to construct the Plutus core constant-
PLC.Some $ PLC.ValueOf PLC.DefaultUniBool True
The only parts that you will need to change when creating other constants, are the type and the value. Here the type is DefaultUniBool
. This means the next argument must be a Bool
. Ensured by the type system - don't you worry :)
You can glance at the other types in the default universe (what you will be working with). Can you guess how to make a Plutus core string from a Haskell string, and represent it as a Plutarch term?
import qualified Data.Text as Txt
import qualified PlutusCore as PLC
punsafeConstant . PLC.Some . PLC.ValueOf PLC.DefaultUniString . Txt.pack
(it's even pointfree!)
And that's essentially what the IsString
implementation of Term s PString
does. That is how your string literals end up as plutus core built in strings.
One more, how about something complex - DefaultUniProtoList
. This is a builtin list. But what is the element type? Well, you'll have to specify that yourself! You use DefaultUniApply
to "apply" a type (from the default universe) over DefaultUniProtoList
-
import qualified PlutusCore as PLC
PLC.Some . PLC.ValueOf (PLC.DefaultUniProtoList `PLC.DefaultUniApply` PLC.DefaultUniInteger)
That right there converts a [Integer]
into a Plutus core builtin list of builtin integers. Convenient!
Actually, there's a convenient pattern
synonym for DefaultUniProtoList `DefaultUniApply` a
- DefaultUniList a
. Using that, you can simplify the above to-
PLC.Some . PLC.ValueOf (PLC.DefaultUniList PLC.DefaultUniInteger)
Note that you will have to provide the correct type annotations yourself, as punsafeConstant
just infers to a Term s a
. That's why it's unsafe! Make sure to provide the correct annotations when using this unsafe function-
foo :: Bool -> Term s PBool
foo = punsafeConstant . PLC.Some . PLC.ValueOf PLC.DefaultUniBool
Of course, we represent Plutus core booleans as Term s PBool
in Plutarch - so that's its type!
This is what you will be wrangling with the most. Builtin functions are going to be the foundation of everything you do. And the documentation on them is….. sparse.
You create Plutarch synonyms to Plutus core builtin functions using punsafeBuiltin
. It creates a Plutarch level function from a Plutus core builtin functions.
Let's try making one, how about AddInteger
?
import qualified PlutusCore as PLC
addI :: Term s (PInteger :--> PInteger :--> PInteger)
addI = punsafeBuiltin PLC.AddInteger
Just like punsafeConstant
, you have to provide the right annotation yourself. We know that AddInteger
takes two Plutus core builtin integers and returns another one. We represent these integers in Plutarch using PInteger
terms - so there we go!
You can use and apply this Plutarch function just like any other.
Now here's where this goes off the rails, some builtin functions require forces to be used. These builtin functions have inherent polymorphic type variables. The number of times you need to force them, depends on the number of type variables they have.
Let's look at an example- HeadList
. It's type can be thought of as - forall a. [a] → a
. It has one type variable, so it needs to be forced once-
pheadBuiltin :: Term s (PBuiltinList a :--> a)
pheadBuiltin = pforce $ punsafeBuiltin PLC.HeadList
We force a Plutarch term using pforce
, recall that punsafeBuiltin
returns a term. You need to type it all yourself of course. pforce
doesn't mean you need to get rid of the type variable in your Plutarch level type. It'll still work with any a
- the forcing just has to happen at call site.
You can sort of do this blindly, HeadList
takes 1 force, so just pforce
once. TailList
also takes 1 force. ChooseList
takes 2 forces (forall a b. [a] → b → b → b
). Here's how you would implement a Plutarch synonym for it-
pchooseList :: Term s (PBuiltinList a :--> b -> b -> b)
pchooseList = pforce $ pforce $ punsafeBuiltin PLC.ChooseList
Aside: You should also hoist the synonyms here that take one or more forces!
We have a Plutus Core builtin functions reference for everything you need to know about them. Including types, usage, and forcing.
Most of the time, you'll be working with BuiltinData
/Data
- this is the type of the arguments that will be passed onto your script from the outside. This is the type of the datum, the redeemer and the script context. This is also the type of arguments you will be able to pass to a Script
.
Plutarch aims to hide these low level details from the user. Ideally, you will be using PDataSum
/PDataList
and PAsData
- these are essentially just BuiltinData
, but it is typed at the Plutarch level.
If you want to work with BuiltinData
directly however, which you may have to do during developing Plutarch, you can find all that you need to know at Plutonomicon.
TODO
Here's a quick refresher on what ScriptContext
looks like-
data ScriptContext = ScriptContext
{ scriptContextTxInfo :: TxInfo
, scriptContextPurpose :: ScriptPurpose
}
We are interested in txInfoInputs
, which has type TxInInfo
. It is the first field within TxInfo
. If you have read Working with BuiltinData
already - you know that a ScriptContext
translates to a Data
value similar to-
Constr 0 [PlutusTx.toData txInfo, PlutusTx.toData txPurpose]
Where txInfo
and txPurpose
are values of type TxInfo
and ScriptPurpose
respectively.
We are interested in that first field. That's easy, we do the following actions in sequence-
pasConstr
- yields aPBuiltinPair PInteger (PBuiltinList PData)
. We know the constructor id is0
. It doesn't matter, there's only one constructor.psndBuiltin
- yieldsPBuiltinList PData
, the second element of the pair. These are the fields withinScriptContext
.phead
- yieldsPData
, the first field. We know this is ourTxInfo
.
Combining that all up would give you-
import Plutarch.Prelude
import Plutarch.Builtin
f :: Term s (PData :--> PData)
f = plam $ \x -> phead #$ psndBuiltin #$ pasConstr # x
And if you test it with a mock context value, it does work-
mockCtx :: ScriptContext
mockCtx =
ScriptContext
(TxInfo
[ TxInInfo
(TxOutRef "" 1)
(TxOut (Address (PubKeyCredential "0123") Nothing) mempty Nothing)
]
mempty
mempty
mempty
mempty
mempty
(interval (POSIXTime 1) (POSIXTime 2))
mempty
mempty
""
)
(Minting (CurrencySymbol ""))
> f `evalWithArgsT` [PlutusTx.toData mockCtx]
Right (Program () (Version () 1 0 0) (Constant () (Some (ValueOf data (Constr 0 [List [Constr 0 [Constr 0 [Constr 0 [B ""],I 1],Constr 0 [Constr 0 [Constr 0 [B "\SOH#"],Constr 1 []],Map [],Constr 1 []]]],List [],Map [],Map [],List [],List [],Constr 0 [Constr 0 [Constr 1 [I 1],Constr 1 []],Constr 0 [Constr 1 [I 2],Constr 1 []]],List [],List [],Constr 0 [B ""]])))))
Aside: You can find the definition of
evalWithArgsT
above - Compiling and Running.
But we're not done yet! We want txInfoInputs
. You may have noticed where exactly it is located on the above output. See that List …
? Inside the outermost Constr
's fields? That's our txInfoInputs
!
Aside: Recall that
List
data values are simply wrappers around lists. Also recall that the fields in aConstr
value must be all of typeData
. So any of your list fields get translated toList
data. Just remember not to confuse these with builtin lists (PBuiltinList
)! Functions likepheadBuiltin
don't work onList
data values.
To obtain txInfoInputs
from here, we do the following actions in sequence-
pasConstr
- unpacks theTxInfo
. There's only one constructor,TxInfo
- we don't care about that. We need the fields.psndBuiltin
- extracts the second member of the pair, the fields ofTxInfo
.phead
- extracts the first element of the list. This is our field,txInfoInputs
.- (optional)
pasList
- takes out the builtin list from theList
data value.
And that's it! Putting it all together-
f :: Term s (PData :--> PBuiltinList PData)
f = plam $ \x ->
let txInfo = phead #$ psndBuiltin #$ pasConstr # x
in pasList #$ phead #$ psndBuiltin #$ pasConstr # txInfo
Trying it on the same mockCtx
yields-
> f `evalWithArgsT` [PlutusTx.toData mockCtx]
Right (Program () (Version () 1 0 0) (Constant () (Some (ValueOf list (data) [Constr 0 [Constr 0 [Constr 0 [B ""],I 1],Constr 0 [Constr 0 [Constr 0 [B "\SOH#"],Constr 1 []],Map [],Constr 1 []]]]))))
Getting some of the boilerplate out of the way, this is what the value looks like-
Some
(ValueOf list (data)
[Constr 0
[Constr 0 [Constr 0 [B ""],I 1],Constr 0 [Constr 0 [Constr 0 [B "\SOH#"],Constr 1 []],Map [],Constr 1 []]]
]
)
There's just one element in txInfoInputs
in this example, and there it is. Of course TxInInfo
, the element type of this list, also gets translated to a Constr
data with further fields. And that's what you see above.