Skip to content

Latest commit

 

History

History
250 lines (183 loc) · 10.6 KB

interactive-plutip.md

File metadata and controls

250 lines (183 loc) · 10.6 KB

Running contracts in an executable and interactive environment with Plutip

by Chase

This article will showcase how you can utilize Plutip to effortlessly run contracts in an executable environment. Which may be helpful for presenting/recording a demo for your project, or for debugging a contract flow, testing edge cases manually, and even completely interactively in the repl!

The basic executable environment

Firstly, we'll need a few functions that are exported from Plutip's internal modules. These will allow us to spin up the local node and set up everything - the same way it works for your Plutip tests!

import Test.Plutip.Internal.LocalCluster (startCluster, stopCluster)

Here's the types for those functions:

startCluster :: PlutipConfig -> ReaderT ClusterEnv IO a -> IO (TVar (ClusterStatus a), a)

stopCluster :: TVar (ClusterStatus a) -> IO ()

Let's declutter it a bit by monomorphizing the a. Since we're using Plutip with BPI (bot-plutus-interface) (i.e Haskell written contracts), and we'll be using a single wallet to run all contracts in this executable environment, a should be: (ClusterEnv, BpiWallet)

So, given a config and a setup function (we'll get to this shortly), startCluster yields a TVar which you can use to gracefully stop the local node and related services. It also returns a pair containing the ClusterEnv and a BpiWallet. The ClusterEnv is necessary configuration to run contracts, and BpiWallet is the newly generated own wallet. That is, this is the wallet that you should use to run the transactions.

Now, we need to get to the aforementioned setup function. Simply put, this function allows you to do run some setup transactions after the node is setup and return any value that will be of use later.

Of course, in this case, we'll just use the setup function to generate a wallet and fund it with some ada. Then we'll return the ClusterEnv and the wallet that you'll be able to use later.

import Test.Plutip.Internal.BotPlutusInterface.Wallet (BpiWallet, addSomeWallet)
import Test.Plutip.LocalCluster (waitSeconds)
import Test.Plutip.Internal.Types

setup :: ReaderT ClusterEnv IO (ClusterEnv, BpiWallet)
setup = do
  env <- ask
  -- Gotta have all those utxos for the collaterals.
  ownWallet <- addWalletWithAdas $ 100 : replicate 20 10
  -- Wait for faucet funds to be added.
  waitSeconds 2
  pure (env, ownWallet)

addWalletWithAdas :: [Ada] -> ReaderT ClusterEnv IO BpiWallet
addWalletWithAdas = addSomeWallet . map (fromInteger . Ada.toLovelace)

Aside: Feel free to choose the amount of ada you want to fund your wallet with. Just remember: addSomeWallet takes a list of lovelace amounts. Here, I've actually made my custom Ada type as well some helper utilities (not the same as Plutus.V1.Ledger.Ada as that is removed in newer plutus-ledger-api versions).

As promised: just creating one wallet and funding it with ada, that's all!

Now, you can choose the PlutipConfig as you prefer, we'll just be using def from Data.Default (the default config) in this example:

main :: IO ()
main = do
  -- Start the node.
  (clusterStat, (cEnv, ownWallet)) <- startCluster def setup

  -- Do stuff.

  -- Stop the node.
  stopCluster clusterStat

That's what the whole setup and teardown dance looks like! But what about actually running contracts?

That's just as simple! First, you need a contract of course:

import qualified Plutus.Contract as Contract
import qualified Ledger.Constraints as Constraints

-- | Pay 5 ada to yourself ...very useful thing to do
payMyself :: AsContractError e => Contract w s e ()
payMyself = do
  ownPkh <- Contract.ownPaymentPubKeyHash
  let tx = Constraints.mustPayToPubKey ownPkh $ Ada.toValue 5
  ledgerTx <- Contract.submitTxConstraintsWith @Void mempty tx
  void $ Contract.awaitTxConfirmed $ getCardanoTxId ledgerTx

Aside: Remember that your Contract may be comprised of several transactions. In fact, if you're using this to present a demo, for example, I recommend having a single function: demoFlow, that yields a Contract monad and does all the transactions necessary for the whole flow!

Once you have that, you can simply use runContract from import Test.Plutip.Internal.BotPlutusInterface.Run:

runContract ::
  (ToJSON w, Monoid w, MonadIO m) =>
  ClusterEnv ->
  BpiWallet ->
  Contract w s e a ->
  m (ExecutionResult w e a)

There it is! It simply takes a Contract. But what about the other two arguments? Well, the ClusterEnv is simply the one you obtained before from startCluster, and so is BpiWallet! runContract needs to know what wallet is running the contract and therefore submitting the transactions - so we use the wallet we created exactly for this purpose.

Aside: runContract may raise ambiguous type variable errors if your Contract also uses type variables in place of its type parameters (w, s, e, a). You can use type applications on your runContract to specify any valid types. I often use () for w, EmptySchema for s, and Text for e.

In the end, it yields the ExecutionResult, which is defined like so:

data ExecutionResult w e a = ExecutionResult
  { -- | outcome of running contract.
    outcome :: Either (FailureReason e) a
  , -- | stats returned by bot interface after contract being run
    txStats :: ContractStats
  , -- | `Contract` observable state after execution (or up to the point where it failed)
    contractState :: w
  }
  deriving stock (Show)

More often than not, you'll only be interested in the outcome field. This simply contains the result returned by your Contract in case of success, and a reason for failure in case of failure:

data FailureReason e
  = -- | error thrown by `Contract` (via `throwError`)
    ContractExecutionError e
  | -- | exception caught during contract execution
    CaughtException SomeException
  deriving stock (Show)

The e here will be same as the e used by your Contract. In our example, we haven't chosen a concrete e, so we'll just use a type application to set it to Text.

Now, let's run that contract from above!

ExecutionResult exOutcome _ _ <- runContract @() @EmptySchema @Text cEnv ownWallet payMyself

As mentioned before, you'll usually be interested in the outcome only; so we bind it to exOutcome and handle it as necessary:

case exOutcome of
  Left (ContractExecutionError e) -> putStrLn "Contract failed" >> print e
  Left (CaughtException e) -> putStrLn "Unexpected exception" >> print e
  Right _ -> pure ()

Feel free to handle the outcome as it makes sense for your contract!

Assembling all of that together, your main should look like:

main :: IO ()
main = do
  -- Start the node.
  (clusterStat, (cEnv, ownWallet)) <- startCluster def setup

  -- Do stuff.
  ExecutionResult exOutcome _ _ <- runContract @() @EmptySchema @Text cEnv ownWallet payMyself
  case exOutcome of
    Left (ContractExecutionError e) -> putStrLn "Contract failed" >> print e
    Left (CaughtException e) -> putStrLn "Unexpected exception" >> print e
    Right _ -> pure ()

  -- Stop the node.
  stopCluster clusterStat

The interactive executable environment

Alright, you now know how to utilize Plutip outside of just tests. Can we take this further? Could we do all of this in ghci? That'd facilitate easy debugging of contracts because you can simply write, modify and run contracts on demand - all in the repl!

As a matter of fact, that's not too different from what you've seen above. All you really have to do is:

  • Run startCluster with the necessary arguments in the repl and bind its results.
  • Run whatever contracts you want using runContract and play with the results in the repl.
  • When you're done, use stopCluster.

I like to short circuit some of this work with utility functions:

import Test.Plutip.Contract.Types (TestContractConstraints)

newtype ContractRunner = ContrRunner
  { runContr ::
      forall w e a.
      TestContractConstraints w e a =>
      Contract w EmptySchema e a ->
      IO (Either (FailureReason e) a)
  }

begin :: IO (ContractRunner, IO ())
begin = do
  (clusterStat, (cEnv, ownWallet)) <- startCluster def setup
  pure (ContrRunner $ fmap outcome . runContract cEnv ownWallet, stopCluster clusterStat)
  where
    setup = do
      env <- ask
      -- Gotta have all those utxos for the collaterals.
      ownWallet <- addWalletWithAdas $ 300 : replicate 50 10
      -- Wait for faucet funds to be added.
      waitSeconds 2
      pure (env, ownWallet)

This function starts the cluster, and yields 2 functions for you to use in the repl. One of them is just runContract with the cEnv and ownWallet pre-set, and the ExOutcome result is mapped to just the outcome field. The second function returned by begin is just stopCluster with the clusterStat pre-set.

This means you can effectively use it like so in your cabal repl:

> (ContrRunner{runContr}, end) <- begin

Now, you can use runContr to run contracts on demand in the repl however you want and whenever you want:

> runContr @() @EmptySchema @Text payMyself
Right ()

And once you're all done, you can simply type in end to stop the cluster:

> end

That's it!

Troubleshooting

No UTxO to be used as collateral

This isn't directly related to this guide, however - you might encounter this issue while running your contracts either in the executable environment, or, more likely: when playing around in the interactive environment.

As a workaround, you can simply run a separate distributeAda transaction in between your larger transactions, which simply creates a bunch of small Ada only UTxOs.

import qualified Ledger.Constraints as Constraints
import Plutus.Contract
import qualified Plutus.Contract as Contract
import Plutus.V1.Ledger.Api
import qualified Plutus.V1.Ledger.Value as Value

adaToValue :: Integer -> Value
adaToValue x = Value.singleton adaSymbol adaToken lovelaceAmount
  where
    lovelaceAmount = x * 1_000_000

distributeAda :: AsContractError e => [Integer] -> Contract w s e ()
distributeAda amounts = do
  ownPkh <- Contract.ownPaymentPubKeyHash
  let tx = foldMap (Constraints.mustPayToPubKey ownPkh . adaToValue) amounts
  ledgerTx <- Contract.submitTxConstraintsWith @Void mempty tx
  void $ Contract.awaitTxConfirmed $ getCardanoTxId ledgerTx