3. Off-Chain

The off-chain side of the server implementation is in charge of defining the interface for interacting with the client and performing the necessary actions for each operation. We divide the OffChain module in two: Interface and Operations.

Inside Interface we define the necessary types for specifying the endpoints that the PAB service will provide to the client, and a type for storing blockchain information that we want to communicate to the client. In plutus-apps jargon it corresponds to the Schema and the Observable State.

Inside Operations we define the main functions that will be called from the PAB service module for providing the endpoints, and functions corresponding to each operation, that can query the blockchain and build transactions.

3.1. Interface

First thing we define is the Schema, which basically specifies the API that the PAB service will provide.

type EscrowSchema = Endpoint "start"   StartParams
                .\/ Endpoint "cancel"  CancelParams
                .\/ Endpoint "resolve" ResolveParams
                .\/ Endpoint "reload"  Integer

We define an endpoint for each operation: start, cancel and resolve. Each one has parameters with the necessary input information. In addition, we define a reload operation used for querying blockchain information that is needed in the client side.

As we will see later, when an end-user connects to the PAB service, the corresponding wallet address is set, so the parameter corresponding to the address that is performing the operation is implicit. In other words, once the frontend connects with the PAB service, it provides an API where the user address is set as a context.

For starting an escrow, in addition to the address of the user performing the operation, we need to define the receiver address and the payments information:

data StartParams = StartParams
                   { receiverAddress   :: ReceiverAddress
                   , sendAmount        :: Integer
                   , sendAssetClass    :: AssetClass
                   , receiveAmount     :: Integer
                   , receiveAssetClass :: AssetClass
                   }
  deriving (Generic)
  deriving anyclass (FromJSON, ToJSON)

For canceling or resolving, it is required to have the reference to the script UTxO corresponding to the specific escrow instance. The validation script code must be included in the transaction and therefore, as it is parameterized on the receiver address, the receiver address is also required.

In the case of canceling, as it is done by the sender, both requirements are included as parameters:

data CancelParams = CancelParams
                    { cpTxOutRef        :: TxOutRef
                    , cpReceiverAddress :: ReceiverAddress
                    }
  deriving (Generic)
  deriving anyclass (FromJSON, ToJSON)

For resolving, as it is done by the receiver, the receiver address is in the context, so we don’t need to provide it as an endpoint parameter:

newtype ResolveParams = ResolveParams { rpTxOutRef :: TxOutRef }
  deriving (Generic)
  deriving anyclass (FromJSON, ToJSON)

In addition to the Schema, we define the Observable State. It corresponds to information that we want to make available to the client side through the PAB’s status endpoint. In this case we want to provide the list of escrows waiting to be resolved by the connected user. This way, the frontend can display the information in the UI and provide the option to resolve them. For each escrow we include the UTxO reference, the Escrow Info (see section 2.1), and the asset class and amount paid by the sender:

data UtxoEscrowInfo = UtxoEscrowInfo
                      { escrowUtxo    :: TxOutRef
                      , escrowInfo    :: EscrowInfo
                      , escrowPayment :: (AssetClass,Integer)
                      }
    deriving (Show, Generic)
    deriving anyclass (FromJSON, ToJSON)

In addition to the escrows information, the Observable State includes a special integer called reloadFlag:

data ObservableState = ObservableState
                       { info       :: [UtxoEscrowInfo]
                       , reloadFlag :: Integer
                       }
    deriving (Show, Generic)
    deriving anyclass (FromJSON, ToJSON)

The reloadFlag is used as a way to signal to the client side that the Observable State has been updated in the PAB status after a call to the reload endpoint (see section 3.2 below).

The types defined here are the interface for communicating the client with the PAB service. The client will send the endpoints parameters as JSON objects that are converted to Haskell types. Vice versa, the Observable State is converted to JSON for sending to the client. For the conversions, we currently use derived instances of FromJSON and ToJSON. To have more control over the interface, developers can define their own instantiations of the JSON conversion.

3.2. Operations

Now that we have defined the interface of our off-chain code, it’s turn to implement the core functionality for each operation. First, we define the function that connects each endpoint with the corresponding off-chain function. This function is called endpoints and will be called from the PAB service module. It receives a WalletAddress corresponding to the connected user that is calling the endpoint:

endpoints
    :: WalletAddress
    -> Contract (Last [UtxoEscrowInfo]) EscrowSchema Text ()
endpoints raddr = forever $ handleError logError $ awaitPromise $
                  startEp `select` cancelEp `select` resolveEp `select` reloadEp
  where
    startEp :: Promise (Last [UtxoEscrowInfo]) EscrowSchema Text ()
    startEp = endpoint @"start" $ startOp raddr

    cancelEp :: Promise (Last [UtxoEscrowInfo]) EscrowSchema Text ()
    cancelEp = endpoint @"cancel" $ cancelOp raddr

    resolveEp :: Promise (Last [UtxoEscrowInfo]) EscrowSchema Text ()
    resolveEp = endpoint @"resolve" $ resolveOp raddr

    reloadEp :: Promise (Last [UtxoEscrowInfo]) EscrowSchema Text ()
    reloadEp = endpoint @"reload" $ const $ reloadOp raddr

Then we define functions for each operation. Let’s review the startOp, resolveOp and reloadOp functions. We will show just some relevant code snippets here.

First startOp, that has the following header:

startOp
    :: forall w s
    .  WalletAddress
    -> StartParams
    -> Contract w s Text ()
startOp addr StartParams{..} = do

Starting an escrow consists of paying to a script the value that the sender wants to pay to the receiver, including in the datum the corresponding escrow information. So for specifying the transaction, we need to define the value and datum that will be part of the script UTxO:

senderVal = assetClassValue sendAssetClass sendAmount
val       = minAda <> cTokenVal <> senderVal
datum     = mkEscrowDatum (mkSenderAddress addr)
                          receiveAmount
                          receiveAssetClass
                          cTokenAsset

The value consists of a minimum amount of ADA, the control token that will be minted and the tokens that should be paid to the receiver. In the datum we include the sender’s address, the payment expected and the control token asset class that will be burned at resolving or canceling.

Then we specify the transaction by defining lookups and constraints:

lkp = mconcat
      [ typedValidatorLookups (escrowInst receiverAddress)
      , plutusV1OtherScript validator
      , plutusV1MintingPolicy (controlTokenMP contractAddress)
      ]
tx  = mconcat
      [ mustPayToTheScriptWithDatumInTx datum val
      , mustMintValue cTokenVal
      , mustBeSignedBy senderPpkh
      ]

In lkp we define the lookups. In this case we are not spending any script UTxO, but we are generating a new one and minting a token, so we declare the validator and minting policy. We will review their implementation in the next section.

In tx we define the constraints. We declare that we pay to the script the defined datum and value, we mint the control token, and we require that the transaction must be signed with the sender’s public key.

Now we just need to yield the specified unbalanced transaction making it accessible to the client side:

mkTxConstraints @Escrowing lkp tx >>= yieldUnbalancedTx

The following diagram illustrates the unbalanced transaction that is yielded to the client for balancing, signing and submitting:

../_images/unbalancedStart.png

Let’s review now the resolve operation:

resolveOp
    :: forall w s
    .  WalletAddress
    -> ResolveParams
    -> Contract w s Text ()
resolveOp addr ResolveParams{..} = do
    ....
    ....

We have to build a transaction that spends the script UTxO, pays to the sender the tokens specified in the Escrow Info, and burns the control token. We also have to specify that the receiver gets the payment in the corresponding address. First, we get the UTxO and extract from there the Escrow Info:

utxo  <- findValidUtxoFromRef rpTxOutRef contractAddress cTokenAsset
eInfo <- getEscrowInfo utxo

We use the following auxiliary functions for it: findValidUtxoFromRef gets the UTxO content from a given UTxO reference if the address is the given one, and the value contains a token of the given asset class. The function getEscrowInfo reads the datum of a given UTxO and returns the Escrow Info inside it.

For defining the transaction, we need to specify the payments to sender and receiver:

let cTokenVal      = assetClassValue cTokenAsset (-1)
    senderWallAddr = eInfoSenderWallAddr eInfo
    senderPayment  = valueToSender eInfo <> minAda
    escrowVal      = utxo ^. decoratedTxOutValue
    receivePayment = escrowVal <> cTokenVal

The sender address is defined in the Escrow Info, and for defining the payment we use the function valueToSender, implemented in the Business module. This function will be also used in the on-chain validator for checking that the payment received by the sender is correct. Regarding the receiver’s payment, it is basically the entire value contained in the script UTxO, without the control token that must be burned.

Now we define the lookups and constraints:

lkp = mconcat
    [ plutusV1OtherScript validator
    , unspentOutputs (singleton rpTxOutRef utxo)
    , plutusV1MintingPolicy (controlTokenMP contractAddress)
    ]
tx = mconcat
    [ mustSpendScriptOutput rpTxOutRef resolveRedeemer
    , mustMintValue cTokenVal
    , mustBeSignedBy receiverPpkh
    , mustPayToWalletAddress senderWallAddr senderPayment
    , mustPayToWalletAddress addr receivePayment
    ]

In addition to the validator and control token minting policy, we include in the lookups the UTxO that is spent in this transaction. The constraints specify that we spend the script UTxO using the redeemer resolveRedeemer, we burn the control token, the transaction must be signed by the receiver, pays to the sender the corresponding tokens specified in the Escrow Info, and pays to the receiver the corresponding value.

Finally, we build the unbalanced transaction and yield it:

mkTxConstraints @Escrowing lkp tx >>= yieldUnbalancedTx

The following diagram illustrates the yielded transaction:

../_images/unbalancedResolve.png

Let’s finally review the reload operation, which doesn’t generate any transaction, but it’s in charge of reading the blockchain and writing the updated obervable state. It corresponds to a list containing the information of every escrow waiting to be resolved by the corresponding user address:

reloadOp
    :: forall s
    .  WalletAddress
    -> Integer
    -> Contract (Last ObservableState) s Text ()
reloadOp addr rFlag = do
    let contractAddress = escrowAddress $ mkReceiverAddress addr
        cTokenCurrency  = controlTokenCurrency contractAddress
        cTokenAsset     = assetClass cTokenCurrency cTokenName

    utxos      <- lookupScriptUtxos contractAddress cTokenAsset
    utxosEInfo <- mapM (mkUtxoEscrowInfoFromTxOut cTokenAsset) utxos

    tell $ Last $ Just $ mkObservableState rFlag utxosEInfo

For updating the observable state we need to look for the list of UTxOs belonging to the script address. Function lookupScriptUtxos is used for this, looking for UTxOs in the given address and containing the given token, in our case the control token. Then we have to read the datum inside each UTxO, using the auxiliary function mkUtxoEscrowInfoFromTxOut. Finally, we write the updated observable state by calling the monadic function tell.