Viết một ứng dụng Plutus cơ bản trong Plutus Playground

Plutus apps alà các chương trình chạy ngoài chuỗi và quản lý các phiên bản hợp đồng đang hoạt động. Họ giám sát chuỗi khối, yêu cầu thông tin đầu vào của người dùng và gửi các giao dịch lên chuỗi khối. Nếu bạn là tác giả hợp đồng, xây dựng ứng dụng Plutus là cách dễ nhất để tạo và sử dụng các đầu ra của tập lệnh Plutus. Trong hướng dẫn này, bạn sẽ viết một ứng dụng Plutus để khóa một số ada trong đầu ra tập lệnh và chia chúng đồng đều giữa hai người nhận.

Xác định các loại

Bạn bắt đầu bằng cách xác định một số kiểu dữ liệu mà bạn sẽ cần cho ứng dụng Split .

SplitData mô tả hai người nhận tiền và tổng số tiền được biểu thị bằng ada.

Bạn đang sử dụng``PubKeyHash`` oại để xác định người nhận. Khi thanh toán, bạn có thể sử dụng hàm băm để tạo hai đầu ra khóa công khai.

Phiên bản cho các loại dữ liệu

SplitData``này các phiên bản cho một số kiểu chữ. Những trường hợp này cho phép tuần tự hóa. ``SplitData``các định dạng khác nhau. ``ToJSON and FromJSON are needed for JSON serialization. JSON objects are passed between the frontend (for example, the Playground) and the app instance. PlutusTx.FromData and PlutusTx.ToData are used for values that are attached to transactions, for example as the <redeemer> of a script output. This class is used by the Plutus app at runtime to construct Data values. Finally, PlutusTx.makeLift is a Template Haskell statement that generates an instance of the PlutusTx.Lift.Class.Lift class for SplitData. This class is used by the Plutus compiler at compile-time to construct Plutus core programs.

Defining the validator script

The validator script is the on-chain part of our Plutus app. The job of the validator is to look at individual transactions in isolation and decide whether they are valid. Plutus validators have the following type signature:

d -> r -> ValidatorCtx -> Bool

where d is the type of the <datum> and r is the type of the redeemer.

You are going to use the validator script to lock a script output that contains the amount specified in the SplitData.

Note

There is an n-to-n relationship between Plutus apps and validator scripts. Apps can deal with multiple validators, and validators can be used by different apps.

In this tutorial you only need a single validator. Its datum type is SplitData and its redeemer type is () (the unit type). The validator looks at the ValidatorCtx value to see if the conditions for making the payment are met:

The validator checks that the transaction, represented by scriptContextTxInfo, pays half the specified amount to each recipient.

You then need some boilerplate to compile the validator to a Plutus script (see Viết các tập lệnh xác thực cơ bản).

The Ledger.Typed.Scripts.Validators.ValidatorTypes class defines the types of the validator, and splitValidator contains the compiled Plutus core code of validateSplit.

Asking for input

When you start the app, you want to ask the sender for a SplitData object. In Plutus apps, the mechanism for requesting inputs is called endpoints.

All endpoints that an app wants to use must be declared as part of the type of the app. The set of all endpoints of an app is called the schema of the app. The schema is defined as a Haskell type. You can build a schema using the Endpoint type family to construct individual endpoint types, and the .\/ operator to combine them.

The SplitSchema defines two endpoints, lock and unlock. Each endpoint declaration contains the endpoint’s name and its type. Note that SplitData has two PubKeyHash fields for the recipients, whereas the endpoint has two Wallet fields (the wallet type is used in the Playground as the identity of a simulated agent). You are going to convert the wallet values to their corresponding public key hashes in the Split app. That way, the user can simply identify the recipient by a number and doesn’t have to enter a public key into a text box. This type of conversion from a nickname to a unique identifier is a common task for Plutus apps.

To use the lock endpoint in our app, you call the Plutus.Contract.Request.endpoint function:

endpoint has a single argument, the name of the endpoint. The name of the endpoint is a Haskell type, not a value, and you have to supply this argument using the type application operator @. This operator is provided by the TypeApplications GHC extension.

Next you need to turn the two Wallet values into their public key hashes so that you can get the SplitData value from the input that was supplied by the user.

Note that the Wallet.Emulator.Wallet.mockWalletPaymentPubKeyHash function and the Wallet.Emulator.Wallet.Wallet type are only available in the simulated environment used by the Plutus playground and by Plutus tests. A real Plutus app would use the metadata server or a custom lookup function for such conversions.

Locking the funds

With the SplitData that you got from the user you can now write a transaction that locks the requested amount of ada in a script output.

Using the constraints library that comes with the Plutus SDK you specify a transaction tx in a single line.

tx = Constraints.mustPayToTheScript s (Ada.toValue amount)

After calling submitTxConstraints in the next line, the Plutus app runtime examines the transaction constraints tx and builds a transaction that fulfills them. The runtime then sends the transaction to the wallet, which adds enough to cover the required funds (in this case, the ada amount specified in amount).

Unlocking the funds

All that’s missing now is the code for retrieving the funds, and some glue to put it all together.

In unlockFunds you use the constraints library to build the spending transaction. Here, tx combines three different constraints. collectFromScript takes the script outputs in unspentOutputs and adds them as input to the transaction, using the unit () as the redeemer. The other two constraints use mustPayToPubKey to add payments for the recipients.

Deploying the app on the Playground

You have all the functions you need for the on-chain and off-chain parts of the app. Every contract in the Playground must define its public interface like this:

The Playground server uses the endpoints definition to populate the UI (via the schema, in our case SplitSchema) and to start the simulation. endpoints is the high-level definition of our app:

The select function acts like a choice between two branches. The left branch starts with lock and the right branch starts with unlock. The app exposes both endpoints and proceeds with the branch that receives an answer first. So, if you call the lock endpoint in one of the simulated wallets, it will call lockFunds and ignore the unlock side of the contract.

You also need a couple of declarations that generate glue code for the Playground.

mkSchemaDefinitions ''SplitSchema

$(mkKnownCurrencies [])

You also need an additional import at the top of the file.

import Playground.Contract

After that, you can compile the contract and create a simulation. The following action sequence results in two transactions that lock the funds and then distribute them to the two recipients.

plutus/tutorials/images/playground-basic-app-simulation.png

Simulation for the split app.

Exercise

  1. Extract the function that assigns funds to each recipient from unlockFunds and validateSplit to reduce redundancy in the code

  2. Extend the contract to deal with a list of recipients instead of a fixed number of 2.