Bài 3: Script Context
- Đây là phiên bản đã viết của Bài giảng số 3, Bài làm lại số 2 .
- Trong bài giảng này, chúng ta tìm hiểu về ngữ cảnh script (đối số xác thực thứ ba), thời gian xử lý và các hợp đồng được tham số hóa.
- Đoạn mã trong bài giảng này sử dụng Plutus commit
81ba78edb1d634a13371397d8c8b19829345ce0d
.
3.1. Trước khi chúng ta bắt đầu
Kể từ bài giảng cuối cùng đã có bản cập nhật cho sân chơi, có trong bản cam kết Plutus mà chúng tôi đang sử dụng cho bài giảng này (xem ghi chú ở trên).
Đã xảy ra sự cố khi thời gian chờ được mã hóa cứng vào sân chơi quá ngắn. Điều này sẽ khiến mô phỏng không thành công nếu chúng mất nhiều thời gian hơn thời gian chờ mã hóa cứng.
Bây giờ có một tùy chọn khi bạn khởi động Máy chủ sân chơi Plutus cho phép bạn chỉ định thời gian chờ. Ví dụ sau đặt thời gian chờ thành 120 giây.
plutus\-playground\-server \-i 120s
3.2. Tóm tắt lại
Khi chúng tôi giải thích mô hình (E) UTxO trong bài giảng đầu tiên, chúng tôi đã đề cập rằng để mở khóa địa chỉ tập lệnh, tập lệnh được đính kèm với địa chỉ sẽ được chạy và tập lệnh đó nhận được ba phần thông tin - dữ liệu , trình đổi và bối cảnh .
Trong bài giảng thứ hai, chúng ta đã xem các ví dụ về điều đó và chúng ta đã thấy nó thực sự hoạt động như thế nào trong Haskell.
Chúng tôi đã thấy việc triển khai cấp thấp, trong đó cả ba đối số đều được đại diện bởi Data
kiểu. Chúng tôi cũng thấy rằng trong thực tế, điều này không được sử dụng.
Thay vào đó, chúng tôi sử dụng phiên bản đã nhập, trong đó dữ liệu và công cụ đổi có thể là kiểu tùy chỉnh (miễn là chúng triển khai IsData
lớp kiểu) và trong đó đối số thứ ba là kiểu ScriptContext
.
Trong các ví dụ mà chúng tôi đã thấy cho đến nay, chúng tôi đã xem xét dữ liệu và công cụ đổi, nhưng chúng tôi luôn bỏ qua ngữ cảnh. Nhưng bối cảnh, tất nhiên, rất quan trọng. Vì vậy, trong bài giảng này, chúng ta sẽ bắt đầu xem xét bối cảnh.
3.3. ScriptContext
Kiểu ScriptContext
được định nghĩa trong gói plutus-ledger-api
, là một gói mà cho đến nay, chúng ta vẫn chưa cần đến. Nhưng bây giờ chúng tôi cần nó, và nó đã được đưa vào .cabal
hồ sơ của tuần này. Nó được định nghĩa trong mô-đun Plutus.V1.Ledger.Contexts
.
**data** ScriptContext **\=** ScriptContext {
scriptContextTxInfo **::** TxInfo,
scriptContextPurpose **::** ScriptPurpose
}
Nó là một loại bản ghi có hai trường.
Trường thứ hai thuộc loại ScriptPurpose
, được xác định trong cùng một mô-đun. Nó xác định mục đích mà một tập lệnh đang được chạy.
**data** ScriptPurpose
**\=** Minting CurrencySymbol
| Spending TxOutRef
| Rewarding StakingCredential
| Certifying DCert
Đối với chúng tôi, quan trọng nhất là Spending
. Đây là những gì chúng ta đã nói cho đến nay trong bối cảnh của mô hình (E) UTxO. Đây là khi một tập lệnh được chạy để xác thực đầu vào chi tiêu cho một giao dịch.
Mục Minting
đích có tác dụng khi bạn muốn xác định mã thông báo gốc. Mục đích của nó là chúng tôi mô tả trong những trường hợp nào mã thông báo gốc có thể được đúc hoặc đốt.
Ngoài ra còn có hai mục đích hoàn toàn mới - Rewarding
- liên quan đến đặt cược và Certifying
- liên quan đến ủy quyền cổ phần.
Trường thú vị nhất, trường có chứa ngữ cảnh thực tế, scriptContextTxInfo
thuộc loại TxInfo
, cũng được xác định trong cùng một mô-đun.
**data** TxInfo **\=** TxInfo
{ txInfoInputs **::** \[TxInInfo\] *\-- ^ Transaction inputs*
, txInfoOutputs **::** \[TxOut\] *\-- ^ Transaction outputs*
, txInfoFee **::** Value *\-- ^ The fee paid by this transaction.*
, txInfoForge **::** Value *\-- ^ The 'Value' forged by this transaction.*
, txInfoDCert **::** \[DCert\] *\-- ^ Digests of certificates included in this transaction*
, txInfoWdrl **::** \[(StakingCredential, Integer)\] *\-- ^ Withdrawals*
, txInfoValidRange **::** SlotRange *\-- ^ The valid range for the transaction.*
, txInfoSignatories **::** \[PubKeyHash\] *\-- ^ Signatures provided with the transaction, attested that they all signed the tx*
, txInfoData **::** \[(DatumHash, Datum)\]
, txInfoId **::** TxId
*\-- ^ Hash of the pending transaction (excluding witnesses)*
} **deriving** (Generic)
Nó mô tả giao dịch chi tiêu. Trong mô hình (E) UTxO, bối cảnh xác thực là giao dịch chi tiêu và các đầu vào và đầu ra của nó. Bối cảnh này được thể hiện trong TxInfo
loại.
Có một số trường là toàn cầu cho toàn bộ giao dịch và đặc biệt là chúng tôi có danh sách tất cả các đầu vào txInfoInputs
và danh sách tất cả các đầu ra txInfoOutputs
. Mỗi người trong số họ có nhiều lĩnh vực khác nhau để đi sâu vào từng đầu vào hoặc đầu ra riêng lẻ.
Chúng tôi cũng thấy các trường về phí txFee
, giá trị giả mạo txInfoForge
, được sử dụng khi đúc hoặc đốt các mã thông báo gốc.
Sau đó, chúng tôi có một danh sách các chứng chỉ ủy quyền trong txInfoDCert
và một trường txInfoWdrl
để nắm giữ thông tin về việc rút tiền đặt cược.
Trường txInfoValidRange
mà chúng ta sẽ xem xét chi tiết hơn trong giây lát, xác định phạm vi vị trí mà giao dịch này hợp lệ.
txInfoSignatories
là danh sách các khóa công khai đã ký giao dịch này.
Các giao dịch sử dụng đầu ra tập lệnh cần phải bao gồm dữ liệu của đầu ra tập lệnh. Trường txInfoData
là một danh sách liên kết các dữ liệu với các hàm băm tương ứng của chúng. Nếu có một đầu ra giao dịch đến một địa chỉ tập lệnh mang một số dữ liệu, bạn không cần phải bao gồm dữ liệu đó, bạn chỉ có thể bao gồm băm dữ liệu. Tuy nhiên, các tập lệnh sử dụng một đầu ra cần phải bao gồm dữ liệu, trong trường hợp đó, nó sẽ được đưa vào txInfoData
danh sách.
Cuối cùng, txInfoId
trường là ID của giao dịch này.
3.3.1. txInfoValidRange
Mặc dù có rất nhiều thông tin chứa trong txInfo
loại này, nhưng đối với ví dụ đầu tiên của chúng tôi về cách sử dụng đối số thứ ba để xác thực, chúng tôi sẽ tập trung vào txInfoValidRange
trường này.
Điều này đưa chúng ta đến một tình huống khó xử thú vị. Chúng tôi đã nhấn mạnh nhiều lần rằng lợi thế lớn mà Cardano có so với một thứ như Ethereum là việc xác thực có thể xảy ra trong ví. Nhưng chúng tôi cũng lưu ý rằng một giao dịch vẫn có thể không thành công trên chuỗi sau khi xác thực nếu, khi giao dịch đến trên blockchain, nó đã bị người khác sử dụng. Trong trường hợp này, giao dịch không thành công mà không phải trả phí.
Điều không bao giờ nên xảy ra trong các trường hợp bình thường là một tập lệnh xác thực chạy và sau đó không thành công. Điều này là do bạn luôn có thể chạy xác thực trong cùng một điều kiện trong ví, vì vậy nó sẽ không thành công trước khi bạn gửi nó.
Vì vậy, đó là một tính năng rất hay, nhưng không rõ ràng là làm thế nào để quản lý thời gian trong bối cảnh đó. Thời gian rất quan trọng, bởi vì chúng tôi muốn thể hiện rằng một giao dịch nhất định chỉ có giá trị trước hoặc chỉ có hiệu lực sau khi đã đạt đến một thời điểm nhất định.
Chúng ta đã thấy một ví dụ về điều này trong bài giảng một - ví dụ đấu giá, trong đó giá thầu chỉ được phép cho đến khi đạt đến thời hạn cuối cùng và close
chỉ có thể gọi điểm cuối sau khi thời hạn đã qua.
Điều đó dường như là một sự mâu thuẫn, bởi vì thời gian rõ ràng là đang trôi. Vì vậy, khi bạn cố gắng xác thực một giao dịch mà bạn đang xây dựng trong ví của mình, tất nhiên, thời gian bạn đang thực hiện có thể khác với thời gian giao dịch đến một nút để xác thực. Vì vậy, không rõ ràng làm thế nào để kết hợp hai điều này lại với nhau để xác thực là xác định và để đảm bảo rằng nếu và chỉ khi xác thực thành công trong ví, nó cũng sẽ thành công trong nút.
Cách Cardano giải quyết điều đó, là bằng cách thêm trường phạm vi vị trí txInfoValidRange
vào một giao dịch, về cơ bản nói rằng “Giao dịch này hợp lệ giữa vị trí này và vị trí đó”.
Khi một giao dịch được gửi đến blockchain và được xác thực bởi một nút, sau đó trước khi chạy bất kỳ tập lệnh nào, một số kiểm tra chung sẽ được thực hiện, chẳng hạn như tất cả các đầu vào đều có mặt và số dư cộng lại, các khoản phí được bao gồm, v.v.
Một trong những kiểm tra xảy ra trước khi xác thực là kiểm tra xem phạm vi vị trí có hợp lệ hay không. Nút sẽ xem xét thời điểm hiện tại và kiểm tra xem nó có nằm trong phạm vi vị trí hợp lệ của giao dịch hay không. Nếu không, thì xác thực không thành công ngay lập tức mà không bao giờ chạy các tập lệnh trình xác thực.
Vì vậy, nếu kiểm tra trước thành công, thì điều này có nghĩa là thời gian hiện tại rơi vào phạm vi vị trí hợp lệ. Đến lượt nó, điều này có nghĩa là chúng ta lại hoàn toàn xác định được. Tập lệnh xác thực có thể đơn giản giả định rằng nó đang được chạy tại một vị trí hợp lệ.
Theo mặc định, một tập lệnh sẽ sử dụng phạm vi vị trí vô hạn, một tập lệnh bao gồm tất cả các vị trí bắt đầu từ khối gốc và chạy cho đến hết thời gian.
Có một sự phức tạp nhỏ với điều này, đó là Ouroboros, giao thức đồng thuận cung cấp năng lượng cho Cardano không sử dụng thời gian POSIX, nó sử dụng các khe cắm. Nhưng Plutus sử dụng thời gian thực, vì vậy chúng ta cần có khả năng chuyển đổi qua lại giữa thời gian thực và thời điểm. Điều này không có vấn đề gì miễn là thời gian rãnh được cố định. Ngay bây giờ là một giây, vì vậy ngay bây giờ nó rất dễ dàng.
Tuy nhiên, điều này có thể thay đổi trong tương lai. Có thể có một đợt hard fork với một số thay đổi thông số sẽ thay đổi thời gian của vị trí. Chúng ta không thể biết trước điều đó. Ví dụ, chúng tôi không biết độ dài vị trí sẽ là bao nhiêu trong 10 năm nữa.
Điều đó có nghĩa là các khoảng thời gian được xác định cho các giao dịch không được có giới hạn trên xác định là quá xa trong tương lai. Phải càng xa trong tương lai thì người ta mới có thể biết được độ dài rãnh sẽ là bao nhiêu. Điều này xảy ra tương tự như 36 giờ. Chúng tôi biết rằng nếu sắp có một đợt hard fork, chúng tôi sẽ biết về nó trước ít nhất 36 giờ.
3.3.2. POSIXTimeRange
Hãy xem POSIXTimeRange
loại này, được định nghĩa trong Plutus.V1.Ledger.Time
.
**type** POSIXTimeRange **\=** Interval POSIXTime.
Nó là một loại từ đồng nghĩa với và chúng ta thấy rằng nó được định nghĩa bởi a và an .Interval POSIXTime``Interval``LowerBound``UpperBound
Interval
ivFrom **::** LowerBound a
inTo **::** UpperBound a
Nếu chúng ta đi sâu vào LowerBound
, chúng ta sẽ thấy hàm tạo
**data** LowerBound a **\=** LowerBound (Extended a) Closure
Closure
là một từ đồng nghĩa Bool
và chỉ định liệu một ràng buộc có được đưa vào Interval
hay không.
Extended
có thể là NegInf
âm vô cùng, PosInf
dương vô cùng, hoặc .Finite a
Chúng tôi cũng tìm thấy một số hàm trợ giúp bao gồm member
hàm kiểm tra xem một cái đã cho a
có phải là một phần của một cái đã cho Interval
hay không, miễn là kiểu của a
là một kiểu con của Ord
, đây là trường hợp cho POSIXTime
.
member **::** Ord a **\=>** a **\->** Interval a **\->** Bool
member a i **\=** i \`contains\` singleton a
interval
là một hàm tạo thông minh cho Interval
kiểu tạo một Interval
với một giới hạn trên và dưới bao gồm.
interval **::** a **\->** a **\->** Interval a
interval s s' **\=** Interval (lowerBound s) (upperBound s')
Sau đó, chúng ta có from
cấu trúc Interval
nào bắt đầu từ a
và kéo dài cho đến đời đời.
from **::** a **\->** Interval a
from s **\=** Interval (lowerBound s) (UpperBound PosInf True)
Và chúng tôi có to
, đó là điều ngược lại. Nó xây dựng một Interval
bắt đầu từ khối genesis cho đến, và bao gồm a
.
to **::** a **\->** Interval a
to s **\=** Interval (LowerBound NegInf True) (upperBound s)
always
là mặc định Interval
bao gồm tất cả các lần.
always **::** Interval a
always **\=** Interval (LowerBound NegInf True) (UpperBound PosInf True)
Và chúng tôi có điều ngược lại, never
không chứa khe cắm.
never **::** Interval a
never **\=** Interval (LowerBound PosInf True) (UpperBound NegInf True)
Ngoài ra còn có trình singleton
trợ giúp, tạo ra một khoảng chỉ bao gồm một vị trí.
singleton **::** a **\->** Interval a
singleton s **\=** interval s s
Hàm hull
cho khoảng nhỏ nhất chứa cả hai khoảng đã cho.
hull **::** Ord a **\=>** Interval a **\->** Interval a **\->** Interval a
hull (Interval l1 h1) (Interval l2 h2) **\=** Interval (min l1 l2) (max h1 h2)
Hàm intersection
xác định khoảng lớn nhất được chứa trong cả hai khoảng đã cho. Đây là một Interval
bắt đầu từ giới hạn dưới lớn nhất của hai khoảng và kéo dài cho đến giới hạn trên nhỏ nhất.
intersection **::** Ord a **\=>** Interval a **\->** Interval a **\->** Interval a
intersection (Interval l1 h1) (Interval l2 h2) **\=** Interval (max l1 l2) (min h1 h2)
Hàm overlaps
kiểm tra xem hai khoảng có trùng nhau hay không, tức là có giá trị nào là thành viên của cả hai khoảng hay không.
overlaps **::** Ord a **\=>** Interval a **\->** Interval a **\->** Bool
overlaps l r **\=** isEmpty (l \`intersection\` r)
contains
lấy hai khoảng và xác định xem khoảng thứ hai có hoàn toàn nằm trong khoảng thời gian đầu tiên hay không.
contains **::** Ord a **\=>** Interval a **\->** Interval a **\->** Bool
contains (Interval l1 h1) (Interval l2 h2) **\=** l1 <= l2 && h2 <= h1
Và chúng tôi có các chức năng before
và after
để xác định, nếu một thời gian nhất định, tương ứng, trước hoặc sau mọi thứ trong một thời điểm nhất định Interval
.
before **::** Ord a **\=>** a **\->** Interval a **\->** Bool
before h (Interval f **\_**) **\=** lowerBound h < f
after **::** Ord a **\=>** a **\->** Interval a **\->** Bool
after h (Interval **\_** t) **\=** upperBound h \> t
Hãy chơi trong REPL.
Prelude Week03.Homework1\> **import** **Plutus.V1.Ledger.Interval**
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\>
Hãy xây dựng Interval
từ 10 đến 20, bao gồm cả.
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> interval (10 **::** Integer) 20
Interval {ivFrom **\=** LowerBound (Finite 10) True, ivTo **\=** UpperBound (Finite 20) True}
Chúng ta có thể kiểm tra xem một giá trị có phải là thành viên của một khoảng hay không:
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> member 9 $ interval (10 **::** Integer) 20
False
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> member 10 $ interval (10 **::** Integer) 20
True
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> member 12 $ interval (10 **::** Integer) 20
True
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> member 20 $ interval (10 **::** Integer) 20
True
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> member 21 $ interval (10 **::** Integer) 20
False
Chúng ta có thể sử dụng hàm from
tạo. Ở đây giới hạn dưới lại là một khe hữu hạn, nhưng giới hạn trên là dương vô cùng.
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> member 21 $ from (30 **::** Integer)
False
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> member 30 $ from (30 **::** Integer)
True
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> member 300000 $ from (30 **::** Integer)
True
Và hàm to
tạo. Ở đây giới hạn dưới là âm vô cùng, trong khi giới hạn trên là một số vị trí hữu hạn.
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> member 300000 $ to (30 **::** Integer)
False
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> member 31 $ to (30 **::** Integer)
False
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> member 30 $ to (30 **::** Integer)
True
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> member 7 $ to (30 **::** Integer)
True
Bây giờ, hãy thử intersection
chức năng trên Interval
từ 10 đến 20 và Interval
từ 18 đến 30.
Prelude Plutus.V1.Ledger.Interval Week03.Homework1> intersection (interval (10 :: Integer) 20) $ interval 18 30 Interval {ivFrom \= LowerBound (Finite 18) True, ivTo \= UpperBound (Finite 20) True}
Như dự kiến, chúng tôi nhận được số Interval
chạy từ 18 đến 20, bao gồm cả.
Chúng tôi có thể kiểm tra xem một cái Interval
có chứa cái khác hay không.
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> contains (to (100 **::** Integer)) $ interval 30 80
True
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> contains (to (100 **::** Integer)) $ interval 30 100
True
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> contains (to (100 **::** Integer)) $ interval 30 101
False
Chúng ta thấy rằng ngay sau khi giây thứ hai Interval
kéo dài đến 101, nó không còn được chứa đầy đủ bên trong giá trị Interval
chạy đến 100.
Tuy nhiên, nếu chúng ta kiểm tra với overlaps
, thì nó sẽ đúng vì có các phần tử, chẳng hạn như 40, được chứa trong cả hai khoảng.
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> overlaps (to (100 **::** Integer)) $ interval 30 101
True
Prelude Plutus.V1.Ledger.Interval Week03.Homework1\> overlaps (to (100 **::** Integer)) $ interval 101 110
False
3.4. Ví dụ - Vesting
Hãy tưởng tượng bạn muốn tặng một món quà của Ada cho một đứa trẻ. Bạn muốn đứa trẻ sở hữu Ada, nhưng bạn chỉ muốn đứa trẻ có quyền truy cập vào nó khi đủ mười tám tuổi.
Sử dụng Plutus, nó rất dễ thực hiện. Là hợp đồng đầu tiên của chúng tôi sẽ xem xét đối số ngữ cảnh, chúng tôi sẽ thực hiện một hợp đồng thực hiện một kế hoạch vesting. Tiền sẽ được đưa vào một kịch bản và sau đó nó có thể được lấy bởi một người nhất định, nhưng chỉ khi đến một thời hạn nhất định.
Chúng tôi bắt đầu bằng cách sao chép IsData
hợp đồng từ bài giảng hai vào một mô-đun mới được gọi là Vesting
.
Bước đầu tiên là suy nghĩ về các loại cho dữ liệu và công cụ đổi.
Đối với dữ liệu, điều hợp lý là có hai phần thông tin, người thụ hưởng và thời hạn. Vì vậy, hãy xác định loại này:
**data** VestingDatum **\=** VestingDatum
{ beneficiary **::** PubKeyHash
, deadline **::** POSIXTime
} **deriving** Show
PlutusTx.unstableMakeIsData ''VestingDatum
Để biết liệu ai đó có thể chi tiêu đầu ra tập lệnh này hay không, cần có hai thông tin, tức là chữ ký của người thụ hưởng và thời gian của giao dịch. Trong trường hợp này, cả hai phần thông tin đó đều được chứa trong chính giao dịch. Điều này có nghĩa là chúng tôi không cần bất kỳ thông tin nào trong trình đổi quà, vì vậy chúng tôi chỉ có thể sử dụng ()
cho người đổi quà.
mkValidator **::** VestingDatum **\->** () **\->** ScriptContext **\->** Bool
Chúng ta cần kiểm tra hai điều kiện.
- Chỉ người thụ hưởng chính xác mới có thể mở khóa UTxO tại địa chỉ này. Chúng tôi có thể xác nhận điều này bằng cách kiểm tra xem chữ ký của người thụ hưởng có được bao gồm trong giao dịch hay không.
- Rằng giao dịch này chỉ được thực hiện sau khi đến thời hạn cuối cùng.
Chúng tôi có thể chỉ viết điều này trong một lần, nhưng chúng tôi sẽ viết nó theo kiểu từ trên xuống và ủy quyền cho một số chức năng trợ giúp.
mkValidator dat () ctx **\=**
mkValidator dat () ctx **\=** traceIfFalse "beneficiary's signature missing" signedByBeneficiary &&
traceIfFalse "deadline not reached" deadlineReached
**where**
info **::** TxInfo
info **\=** scriptContextTxInfo ctx
Để kiểm tra xem giao dịch có được người thụ hưởng ký hay không, chúng ta có thể lấy khóa công khai của người thụ hưởng từ dữ liệu và chuyển nó, cùng với thông tin giao dịch vào txSignedBy
hàm.
signedByBeneficiary **::** Bool
signedByBeneficiary **\=** txSignedBy info $ beneficiary dat
Làm thế nào để chúng tôi kiểm tra xem thời hạn đã qua?
Hãy xem xét một giao dịch có giá trị hợp lệ vượt qua thời hạn, được hiển thị dưới dạng phạm vi cao nhất trong biểu đồ trên.
Nhớ lại rằng trước khi chạy tập lệnh trình xác thực, các kiểm tra khác được thực hiện, bao gồm cả kiểm tra thời gian. Nút kiểm tra xem thời gian hiện tại có nằm trong phạm vi hợp lệ của giao dịch hay không và chỉ sau đó trình xác thực mới được chạy. Vì vậy, chúng tôi biết rằng, nếu chúng tôi đang ở trong trình xác thực, thời gian hiện tại nằm ở đâu đó trong khoảng thời gian hiệu lực.
Trong trường hợp phạm vi vượt qua thời hạn, mã trình xác nhận không thể biết liệu thời gian hiện tại là trước hay sau thời hạn. Trong trường hợp này, người xác nhận phải tuyên bố rằng giao dịch không hợp lệ.
Tuy nhiên, ví dụ thứ hai trong sơ đồ là tốt. Chúng tôi vẫn chưa biết chính xác thời gian hiện tại là bao nhiêu, nhưng chúng tôi biết rằng dù thời gian là bao nhiêu thì cũng sẽ đến sau thời hạn.
Vì vậy, những gì chúng tôi đang kiểm tra là toàn bộ khoảng thời gian hiệu lực nằm ở bên phải của thời hạn. Một cách để làm điều này là sử dụng contains
chức năng để kiểm tra xem khoảng thời gian hiệu lực có được chứa đầy đủ trong khoảng thời gian bắt đầu từ thời hạn và kéo dài cho đến khi kết thúc thời gian hay không.
deadlineReached **::** Bool
deadlineReached **\=** contains (from $ deadline dat) $ txInfoValidRange info
Điều đó hoàn thành logic xác thực. Hãy quan tâm đến một số tấm boilerplate.
**data** Vesting
**instance** Scripts.ValidatorTypes Vesting **where**
**type** **instance** DatumType Vesting **\=** VestingDatum
**type** **instance** RedeemerType Vesting **\=** ()
typedValidator **::** Scripts.TypedValidator Vesting
typedValidator **\=** Scripts.mkTypedValidator @Vesting
$$(PlutusTx.compile \[|| mkValidator ||\])
$$(PlutusTx.compile \[|| wrap ||\])
**where**
wrap **\=** Scripts.wrapValidator @VestingDatum @()
Chúng tôi sẽ tập trung nhiều hơn vào phần ví của script sau, nhưng đây là những thay đổi.
Ngoài một số LANGUAGE
pragmas mới và một số nhập bổ sung, chúng tôi đã tạo một GiveParams
kiểu và sửa đổi grab
điểm cuối để không yêu cầu tham số.
Kiểu VestingSchema
xác định các điểm cuối mà chúng tôi muốn hiển thị cho người dùng. Như trong ví dụ cuối cùng của chúng tôi, give
sẽ được sử dụng bởi người dùng đặt tiền vào hợp đồng, sau đó grab
sẽ được sử dụng bởi người dùng muốn yêu cầu tiền.
**type** VestingSchema **\=**
.\\/ Endpoint "give" GiveParams
.\\/ Endpoint "grab" ()
Vậy chúng ta cần những thông số nào give
? Điểm cuối sẽ tạo UTxO tại địa chỉ tập lệnh vesting với một số tiền và một mức dữ liệu. Nếu bạn nhớ lại, dữ liệu của chúng tôi chứa người thụ hưởng và thời hạn. Vì vậy, có ba phần thông tin mà chúng ta phải chuyển đến give
điểm cuối.
**data** GiveParams **\=** GiveParams
{ gpBeneficiary **::** !PubKeyHash
, gpDeadline **::** !POSIXTime
, gpAmount **::** !Integer
} **deriving** (Generic, ToJSON, FromJSON, ToSchema)
Điểm grab
cuối không yêu cầu bất kỳ tham số nào vì người thụ hưởng sẽ chỉ tìm các UTxO ở địa chỉ tập lệnh và sau đó có thể kiểm tra xem họ có phải là người thụ hưởng hay không và thời hạn đã qua hay chưa. Nếu vậy, họ có thể tiêu thụ chúng.
Hãy nhanh chóng nhìn vào give
điểm cuối.
give **::** AsContractError e **\=>** GiveParams **\->** Contract w s e ()
give gp **\=** **do**
**let** dat **\=** VestingDatum
{ beneficiary **\=** gpBeneficiary gp
, deadline **\=** gpDeadline gp
}
tx **\=** mustPayToTheScript dat $ Ada.lovelaceValueOf $ gpAmount gp
ledgerTx **<-** submitTxConstraints typedValidator tx
void $ awaitTxConfirmed $ txId ledgerTx
logInfo @String $ printf "made a gift of %d lovelace to %s with deadline %s"
(gpAmount gp)
(show $ gpBeneficiary gp)
(show $ gpDeadline gp)
Đầu tiên, chúng tôi tính toán dữ liệu mà chúng tôi muốn sử dụng và chúng tôi có thể nhận được cả hai phần thông tin từ GiveParams
đó được chuyển vào hàm.
Sau đó, đối với giao dịch, chúng tôi thêm một ràng buộc rằng phải có một đầu ra tại địa chỉ tập lệnh này với dữ liệu mà chúng tôi vừa xác định và một số lovelace nhất định, mà chúng tôi cũng nhận được từ GiveParams
.
Phần còn lại của chức năng vẫn như trước, chỉ với một thông báo nhật ký khác.
Điểm grab
cuối có liên quan nhiều hơn một chút.
Có thể có nhiều UTxO tại địa chỉ tập lệnh này và một số trong số chúng có thể không phù hợp với chúng tôi, vì chúng tôi không phải là người thụ hưởng hoặc vì thời hạn chưa qua. Nếu chúng tôi cố gắng gửi một giao dịch khi không có UTxO phù hợp, chúng tôi sẽ trả phí, nhưng không nhận lại được gì.
grab **::** forall w s e. AsContractError e **\=>** Contract w s e ()
grab **\=** **do**
now **<-** currentTime
pkh **<-** pubKeyHash <$> ownPubKey
utxos **<-** Map.filter (isSuitable pkh now) <$> utxoAt scrAddress
**if** Map.null utxos
**then** logInfo @String $ "no gifts available"
**else** **do**
**let** orefs **\=** fst <$> Map.toList utxos
lookups **\=** Constraints.unspentOutputs utxos <>
Constraints.otherScript validator
tx **::** TxConstraints Void Void
tx **\=** mconcat \[mustSpendScriptOutput oref $ Redeemer $ PlutusTx.toData () | oref **<-** orefs\] <>
mustValidateIn (from now)
ledgerTx **<-** submitTxConstraintsWith @Void lookups tx
void $ awaitTxConfirmed $ txId ledgerTx
logInfo @String $ "collected gifts"
**where**
isSuitable **::** PubKeyHash **\->** POSIXTime **\->** TxOutTx **\->** Bool
isSuitable pkh now o **\=** **case** txOutDatumHash $ txOutTxOut o **of**
Nothing **\->** False
Just h **\->** **case** Map.lookup h $ txData $ txOutTxTx o **of**
Nothing **\->** False
Just (Datum e) **\->** **case** PlutusTx.fromData e **of**
Nothing **\->** False
Just d **\->** beneficiary d \== pkh && deadline d <= now
Đầu tiên, chúng tôi lấy thời gian hiện tại và tính toán băm khóa công khai của chúng tôi. Sau đó, chúng tôi tìm kiếm tất cả các UTxO tại địa chỉ này và lọc chúng bằng cách sử dụng isSuitable
hàm trợ giúp, được định nghĩa trong where
mệnh đề.
Đầu tiên, nó kiểm tra băm dữ liệu và nếu tìm thấy nó, nó sẽ cố gắng tìm kiếm dữ liệu tương ứng. Nhớ lại rằng giao dịch sản xuất, trong trường hợp give
này không phải cung cấp dữ liệu, nó chỉ cần cung cấp băm dữ liệu. Tuy nhiên, trong trường hợp của chúng ta, chúng ta cần có sẵn dữ liệu cho grab
điểm cuối, vì vậy give
điểm cuối cung cấp dữ liệu.
Nếu grab
điểm cuối tìm thấy dữ liệu, nó phải giải thích nó thành Vesting
kiểu.
Nếu tất cả những điều này thành công, chúng tôi có thể kiểm tra xem chúng tôi có phải là người thụ hưởng hay không và thời hạn đã qua hay chưa.
Tại thời điểm này, utxos
chứa tất cả các UTxO mà chúng ta có thể sử dụng. Nếu chúng tôi không tìm thấy, thì chúng tôi chỉ cần ghi lại một thông báo cho hiệu ứng đó. Nếu có ít nhất một giao dịch, thì chúng tôi tạo một giao dịch sử dụng tất cả chúng làm đầu vào và thanh toán tiền vào ví của chúng tôi.
Như lookups
, chúng tôi cung cấp danh sách các UTxO cũng như tập lệnh trình xác thực. Nhớ lại rằng, để sử dụng UTxO tại địa chỉ này, giao dịch chi tiêu phải cung cấp tập lệnh xác thực.
Sau đó, chúng tôi tạo một giao dịch sử dụng tất cả các UTxO phù hợp cùng với một ràng buộc mà nó phải xác thực trong Interval
đó kéo dài từ bây giờ cho đến hết thời gian. Nếu chúng tôi không cung cấp khoảng thời gian ở đây, thì quá trình xác thực sẽ không thành công, vì khoảng thời gian mặc định là từ ban đầu cho đến khi kết thúc thời gian. Việc xác nhận trên chuỗi sẽ từ chối điều này vì nó cần một khoảng thời gian được chứa đầy đủ trong khoảng thời gian kéo dài từ thời hạn cho đến khi kết thúc thời gian.
Chúng tôi có thể sử dụng singleton Interval
now
, nhưng nếu có bất kỳ sự cố nào, chẳng hạn như sự chậm trễ của mạng và giao dịch đến một nút muộn hơn một hoặc hai vị trí, thì quá trình xác thực sẽ không hoạt động nữa.
Chúng tôi chỉ tập hợp các điểm cuối.
endpoints **::** Contract () VestingSchema Text ()
endpoints **\=** (give' \`select\` grab') \>> endpoints
**where**
give' **\=** endpoint @"give" \>>= give
grab' **\=** endpoint @"grab" \>> grab
Sau đó, có một số tấm bảng chỉ được sử dụng trong sân chơi.
mkSchemaDefinitions ''VestingSchema
mkKnownCurrencies \[\]
3.4.1. Trong sân chơi
Đầu tiên, hãy thêm một ví thứ ba. Chúng tôi sẽ tạo một kịch bản trong đó Ví 1 tạo hai quà tặng cho Ví 2 với thời hạn khác nhau và cũng tạo một quà tặng cho Ví 3.
Thông thường, có thể gửi cả hai give
giao dịch trong cùng một vị trí, nhưng cách mã của chúng tôi được triển khai, chúng tôi chờ xác nhận, có nghĩa là chúng tôi cần thêm hành động chờ. Đây có thể không phải là cách tốt nhất để làm điều đó, nhưng đó là cách làm trong thời điểm hiện tại.
Ở đây chúng tôi gặp phải vấn đề đầu tiên của chúng tôi. Chúng tôi cần cung cấp địa chỉ người thụ hưởng, nhưng không có cách nào trong sân chơi để lấy mã băm khóa công khai của ví.
Nhưng chúng ta có thể lấy nó từ REPL.
Prelude Week03.Homework1> :l src/Week03/Vesting.hs
Ok, one module loaded.
Prelude Week03.Vesting> import Ledger
Prelude Ledger Week03.Vesting> import Wallet.Emulator
Prelude Ledger Wallet.Emulator Week03.Vesting> pubKeyHash $ walletPubKey $ Wallet 2
39f713d0a644253f04529421b9f51b9b08979d08295959c4f3990ee617f5139f
Prelude Ledger Wallet.Emulator Week03.Vesting> pubKeyHash $ walletPubKey $ Wallet 3
dac073e0123bdea59dd9b3bda9cf6037f63aca82627d7abcd5c4ac29dd74003e
Vấn đề tiếp theo là thời hạn. Trong bài giảng trước, chúng ta đã biết cách chuyển đổi giữa các vị trí và thời gian POSIX. Điều này đã thay đổi. Trước đây, bạn chỉ cần một vị trí và xuất hiện thời gian POSIX. Bây giờ có một cuộc tranh cãi thứ hai.
Prelude Ledger Wallet.Emulator Week03.Vesting\> **import** **Ledger.TimeSlot**
Prelude Ledger Wallet.Emulator Ledger.TimeSlot Week03.Vesting\> :t slotToBeginPOSIXTime
slotToBeginPOSIXTime **::** SlotConfig **\->** Slot **\->** POSIXTime
Cũng có những phiên bản slotToBeginPOSIXTime
có thời gian bắt đầu và kết thúc. Điều này là do một thời điểm không chỉ là một thời điểm mà nó là một khoảng thời gian.
Vậy đây là SlotConfig
gì?
Prelude Ledger Wallet.Emulator Ledger.TimeSlot Week03.Vesting\> :i SlotConfig
**type** SlotConfig **::** \*
**data** SlotConfig
**\=** SlotConfig {scSlotLength **::** Integer, scZeroSlotTime **::** POSIXTime}
*\-- Defined in ‘Ledger.TimeSlot’*
**instance** Eq SlotConfig *\-- Defined in ‘Ledger.TimeSlot’*
**instance** Show SlotConfig *\-- Defined in ‘Ledger.TimeSlot’*
Nó tính theo độ dài vị trí và thời gian mà vị trí số 0 bắt đầu.
Vì vậy, bây giờ chúng ta phải tìm ra những gì SlotConfig
để sử dụng cho sân chơi. May mắn thay, nó là mặc định. Để làm được điều đó, chúng ta cần sử dụng Data.Default
mô-đun.
Prelude Ledger Wallet.Emulator Ledger.TimeSlot Week03.Vesting\> **import** **Data.Default**
Prelude Ledger Wallet.Emulator Ledger.TimeSlot Data.Default Week03.Vesting\> def **::** SlotConfig
SlotConfig {scSlotLength **\=** 1000, scZeroSlotTime **\=** POSIXTime {getPOSIXTime **\=** 1596059091000}}
Bây giờ chúng ta có thể sử dụng slotToBeginPOSIXTime
với cấu hình mặc định để lấy thời gian POSIX cho vùng 10 và vùng 20.
Prelude Ledger Wallet.Emulator Ledger.TimeSlot Data.Default Week03.Vesting\> slotToBeginPOSIXTime def 10
POSIXTime {getPOSIXTime **\=** 1596059101000}
Prelude Ledger Wallet.Emulator Ledger.TimeSlot Data.Default Week03.Vesting\> slotToBeginPOSIXTime def 20
POSIXTime {getPOSIXTime **\=** 1596059111000}
Và chúng ta có thể sử dụng chúng trong sân chơi. Chúng tôi sẽ sử dụng vị trí 10 làm thời hạn cuối cùng cho phần đầu tiên và thứ ba . Chúng tôi cũng sẽ đưa ra 10 Ada trong mỗi trường hợp.give``s and slot 20 for the second ``give
Hãy tạo một kịch bản mà mọi thứ đều hoạt động. Ví 3 lấy ở vị trí 10 khi thời hạn cho Ví 3 đã qua và Ví 2 lấy ở vị trí 20, khi cả hai thời hạn của Ví 2 đã qua. Chúng tôi sẽ sử dụng tùy chọn cho việc này.Wait Until..
Sau khi đánh giá, lần đầu tiên chúng ta thấy giao dịch Genesis.
Nếu chúng ta xem xét giao dịch tiếp theo, chúng ta thấy quà tặng từ Ví 1 đến Ví 2 với thời hạn là 10. Tại đây, mười Ada bị khóa địa chỉ tập lệnh.
Giao dịch tiếp theo là quà tặng từ Ví 1 đến Ví 2 với thời hạn là 20. Một UTxO mới hiện đã được tạo tại địa chỉ tập lệnh với mười Ada.
Và món quà thứ ba, lần này là Ví 3, với thời hạn là 10. Ví 1 hiện có khoảng 70 Ada và một UTxO khác được tạo với 10 Ada bị khóa tại địa chỉ tập lệnh.
Tại vị trí số 10, Wallet 3 lấy thành công. UTxO thứ ba là đầu vào, một số khoản phí được thanh toán và sau đó phần còn lại của cuộc tình được gửi đến Ví 3.
Sau đó, tại vị trí 20, Wallet 2 lấy thành công cả hai UTxO mà họ là người thụ hưởng. Lần này phí cao hơn vì phải chạy hai trình xác nhận.
Số dư cuối cùng phản ánh những thay đổi.
Bây giờ chúng ta hãy xem xét trường hợp xảy ra quá sớm. Chúng tôi sẽ lấy ví 2 ở vị trí 15 thay vì vị trí 20.
Bây giờ chúng ta thấy rằng các giao dịch đầu tiên giống nhau, nhưng giao dịch cuối cùng tại vị trí 15 chỉ có một đầu vào, vì UTxO thứ hai chưa có sẵn.
Và chúng ta có thể thấy rằng có 10 Ada vẫn bị khóa tại địa chỉ script.
Mã off-chain của chúng tôi được viết theo cách mà nó sẽ chỉ gửi một giao dịch nếu có UTxO phù hợp có thể được lấy. Điều này có nghĩa là chúng tôi không thực sự sử dụng trình xác thực bởi vì chúng tôi chỉ gửi các giao dịch đến chuỗi khối sẽ vượt qua xác thực.
Nếu bạn muốn kiểm tra trình xác thực, bạn có thể sửa đổi mã ví để điểm cuối lấy cố gắng lấy mọi thứ và sau đó xác thực sẽ không thành công nếu bạn không phải là người thụ hưởng hoặc chưa đến thời hạn.
Bạn cần lưu ý rằng bất kỳ ai cũng có thể viết mã off-chain. Vì vậy, mặc dù nó hoạt động ngay bây giờ miễn là bạn sử dụng grab
điểm cuối mà chúng tôi đã tự viết, ai đó có thể viết một đoạn mã off-chain khác không lọc các UTxO như chúng tôi đã làm. Trong trường hợp này, nếu trình xác nhận không đúng, điều gì đó có thể sai khủng khiếp.
3.5. Ví dụ 2 - Hợp đồng tham số
Chúng ta sẽ bắt đầu ví dụ tiếp theo bằng cách sao chép mã từ ví dụ vesting vào một mô-đun mới có tên Week03.Parameterized
.
3.5.1. On-Chain
Lưu ý rằng trong ví dụ vesting, chúng tôi đã sử dụng Vesting
kiểu làm datum, nhưng nó chỉ được sửa chữa, nó không thay đổi. Ngoài ra, chúng tôi có thể đưa nó vào hợp đồng, có thể nói, để chúng tôi có một hợp đồng mà bản thân tập lệnh đã chứa thông tin về người thụ hưởng và thời hạn.
Tất cả các ví dụ về hợp đồng mà chúng tôi đã thấy cho đến nay đã được sửa chữa. Chúng tôi đã sử dụng a TypedValidator
làm hằng số thời gian biên dịch. Ý tưởng của tập lệnh được tham số hóa là bạn có thể có một tham số và tùy thuộc vào giá trị của tham số, bạn nhận được các giá trị khác nhau của TypedValidator
.
Vì vậy, thay vì xác định một tập lệnh, với một địa chỉ tập lệnh duy nhất, với tất cả các UTxO ở cùng một địa chỉ, bạn có thể xác định một họ tập lệnh được tham số hóa bởi một tham số nhất định. Trong trường hợp của chúng tôi, điều này có nghĩa là các UTxO cho những người thụ hưởng khác nhau và / hoặc thời hạn sẽ là một địa chỉ tập lệnh khác, vì chúng sẽ có trình xác thực được tham số hóa cụ thể cho các tham số của họ thay vì cụ thể cho dữ liệu của UTxO.
Chúng tôi sẽ trình bày cách thực hiện điều này bằng cách sử dụng một tham số thay vì sử dụng số liệu cho người thụ hưởng và giá trị thời hạn.
Hãy bắt đầu bằng cách đổi tên VestingDatum
thành một cái gì đó phù hợp hơn.
**data** VestingParam **\=** VestingParam
{ beneficiary **::** PubKeyHash
, deadline **::** POSIXTime
} **deriving** Show
Chúng tôi cũng sẽ xóa unstableMakeIsData
cuộc gọi vì chúng tôi không cần nó nữa.
Lý do chúng ta không cần nó, là bởi vì chúng ta sẽ sử dụng ()
cho datum trong mkValidator
hàm. Tất cả thông tin chúng tôi yêu cầu sẽ nằm trong một đối số mới mkValidator
, thuộc loại VestingParam
mà chúng tôi thêm vào đầu danh sách các đối số.
*{-# INLINABLE mkValidator #-}*
mkValidator **::** VestingParam **\->** () **\->** () **\->** ScriptContext **\->** Bool
mkValidator p () () ctx **\=** traceIfFalse "beneficiary's signature missing" signedByBeneficiary &&
traceIfFalse "deadline not reached" deadlineReached
**where**
info **::** TxInfo
info **\=** scriptContextTxInfo ctx
signedByBeneficiary **::** Bool
signedByBeneficiary **\=** txSignedBy info $ beneficiary p
deadlineReached **::** Bool
deadlineReached **\=** contains (from $ deadline p) $ txInfoValidRange info
Chúng tôi cũng thay đổi Vesting
kiểu để phản ánh sự thay đổi đối với dữ liệu.
**data** Vesting
**instance** Scripts.ValidatorTypes Vesting **where**
**type** **instance** DatumType Vesting **\=** ()
**type** **instance** RedeemerType Vesting **\=** ()
Bây giờ, di TypedValidator
chúc không còn là một giá trị không đổi. Thay vào đó, nó sẽ nhận một tham số.
Nhớ lại rằng hàm mkTypedValidator
yêu cầu là đối số đầu tiên của nó, mã đã biên dịch của một hàm nhận ba đối số và trả về a Bool
. Nhưng bây giờ, nó có bốn đối số, vì vậy chúng ta cần tính đến điều đó.
typedValidator **::** VestingParam **\->** Scripts.TypedValidator Vesting
typedValidator p **\=** Scripts.mkTypedValidator @Vesting
Bây giờ, những gì chúng tôi muốn làm là một cái gì đó như thế này, truyền tham số mới p
vào mkValidator
để mã được biên dịch trong dấu ngoặc Oxford sẽ có kiểu chính xác.
*\-- this won't work*
$$(PlutusTx.compile \[|| mkValidator p ||\])
$$(PlutusTx.compile \[|| wrap ||\])
**where**
wrap **\=** Scripts.wrapValidator @() @()
Mã này sẽ không hoạt động, nhưng trước khi chúng tôi điều tra, hãy để mã như hiện tại và thực hiện thêm một số thay đổi đối với phần còn lại của mã.
validator
bây giờ sẽ nhận một VestingParam
và sẽ trả về một hàm đã soạn. Hàm được trả về có tác dụng là bất kỳ tham số nào được truyền đến validator
bây giờ sẽ được chuyển đến typedValidator
hàm một cách hiệu quả, giá trị trả về của chúng sẽ được chuyển đến validatorScript
hàm.
validator **::** VestingParam **\->** Validator
validator **\=** Scripts.validatorScript . typedValidator
Và tương tự đối với valHash
và scrAddress
.
valHash **::** VestingParam **\->** Ledger.ValidatorHash
valHash **\=** Scripts.validatorHash . typedValidator
scrAddress **::** VestingParam **\->** Ledger.Address
scrAddress **\=** scriptAddress . validator
Bây giờ, chúng ta hãy tìm hiểu những gì sai với typedValidator
hàm out.
Nếu chúng tôi cố gắng khởi chạy REPL, chúng tôi sẽ gặp lỗi biên dịch.
GHC Core to PLC plugin: E043:Error: Reference to a name which is not a local, a builtin, or an external INLINABLE function: Variable p
No unfolding
Context: Compiling expr: p
Context: Compiling expr: Week03.Parameterized.mkValidator p
Context: Compiling expr at "plutus-pioneer-program-week03-0.1.0.0-inplace:Week03.Parameterized:(67,10)-(67,48)"
Vấn đề là dòng này.
*\-- this won't work*
$$(PlutusTx.compile \[|| mkValidator p ||\])
Nhớ lại rằng mọi thứ bên trong dấu ngoặc Oxford phải được biết rõ ràng tại thời điểm biên dịch. Thông thường, nó thậm chí sẽ cần tất cả mã được viết rõ ràng, nhưng bằng cách sử dụng INLINABLE
pragma trên mkValidator
hàm, chúng ta có thể tham chiếu hàm để thay thế. Tuy nhiên, nó vẫn phải được biết tại thời điểm biên dịch, vì đó là cách hoạt động của Template Haskell - nó được thực thi trước trình biên dịch chính.
Không p
được biết tại thời điểm biên dịch, bởi vì chúng tôi dự định cung cấp nó trong thời gian chạy. May mắn thay, có một cách để giải quyết vấn đề này.
Về phía Haskell, chúng tôi có mkValidator
chức năng của mình và chúng tôi có p
loại VestingParam
. Chúng tôi có thể biên dịch mkValidator
sang Plutus, nhưng chúng tôi không thể biên dịch p
sang Plutus vì chúng tôi không biết nó là gì. Nhưng, nếu chúng ta có thể sử dụng phiên bản đã biên dịch p
, chúng ta có thể áp dụng phiên bản đã biên dịch này cho phiên bản đã biên dịch mkValidator
và điều này sẽ mang lại cho chúng ta những gì chúng ta muốn.
Điều này dường như không giải quyết được gì, bởi vì chúng tôi vẫn cần một phiên bản đã biên dịch p
và chúng tôi có cùng một vấn đề p
chưa được biết tại thời điểm biên dịch.
Tuy nhiên, p
không phải là một số mã Haskell tùy ý, mà là dữ liệu, vì vậy nó không chứa bất kỳ loại chức năng nào. Nếu chúng ta thực hiện kiểu của p
một thể hiện của lớp kiểu được gọi Lift
. Chúng ta có thể sử dụng liftCode
để biên dịch p
trong thời gian chạy sang Plutus Core và sau đó, bằng cách sử dụng applyCode
chúng ta có thể áp dụng Plutus Core p
cho Plutus Core mkValidator
.
3.5.1.1. Lớp nâng
Chúng ta hãy nhìn sơ qua về Lift
lớp học. Nó được định nghĩa trong gói plutus-tx
.
**module** **PlutusTx.Lift.Class**
Nó chỉ có một chức năng , Lift
. Tuy nhiên, chúng tôi sẽ không sử dụng chức năng này trực tiếp.
Tầm quan trọng của lớp là nó cho phép chúng ta, trong thời gian chạy, nâng các giá trị Haskell thành các giá trị tập lệnh Plutus tương ứng. Và đây chính xác là những gì chúng ta cần để chuyển tham số của p
mình thành mã.
Chúng tôi sẽ sử dụng một chức năng khác, được định nghĩa trong cùng một gói nhưng trong một mô-đun khác.
**module** **PlutusTx.Lift**
Hàm chúng ta sẽ sử dụng được gọi liftCode
.
*\-- | Get a Plutus Core program corresponding to the given value as a 'CompiledCodeIn', throwing any errors that occur as exceptions and ignoring fresh names.*
liftCode
**::** (Lift.Lift uni a, Throwable uni fun, PLC.ToBuiltinMeaning uni fun)
**\=>** a **\->** CompiledCodeIn uni fun a
liftCode x **\=** unsafely $ safeLiftCode x
Nó nhận một giá trị Haskell của kiểu a
, với điều kiện a
là một thể hiện của Lift
lớp và biến nó thành một đoạn mã tập lệnh Plutus tương ứng với cùng kiểu.
Bây giờ chúng tôi có thể sửa chữa trình xác thực của mình.
typedValidator **::** VestingParam **\->** Scripts.TypedValidator Vesting
typedValidator p **\=** Scripts.mkTypedValidator @Vesting
($$(PlutusTx.compile \[|| mkValidator ||\]) \`PlutusTx.applyCode\` PlutusTx.liftCode p)
$$(PlutusTx.compile \[|| wrap ||\])
**where**
wrap **\=** Scripts.wrapValidator @() @()
Mã này ổn, nhưng nó sẽ không được biên dịch, bởi vì VestingParam
nó không phải là một phiên bản của Lift
. Để khắc phục điều này, chúng ta có thể sử dụng makeLift
.
PlutusTx.makeLift ''VestingParam
Và, chúng tôi cần bật tiện ích mở rộng GHC.
*{-# LANGUAGE MultiParamTypeClasses #-}*
Bây giờ nó sẽ biên dịch.
3.5.2. Ngoài chuỗi
Mã ngoài chuỗi không thay đổi nhiều.
GiveParams
Vẫn như nhau .
**data** GiveParams **\=** GiveParams
{ gpBeneficiary **::** !PubKeyHash
, gpDeadline **::** !POSIXTime
, gpAmount **::** !Integer
} **deriving** (Generic, ToJSON, FromJSON, ToSchema)
VestingSchema
đã thay đổi một chút vì grab
điểm cuối giờ đây dựa vào việc biết người thụ hưởng và thời hạn để biết xác định địa chỉ tập lệnh. Chúng tôi biết người thụ hưởng vì nó sẽ là mã băm khóa công khai của ví gọi grab
, nhưng chúng tôi không biết thời hạn, vì vậy chúng tôi phải chuyển nó cho grab
.
**type** VestingSchema **\=**
Endpoint "give" GiveParams
.\\/ Endpoint "grab" POSIXTime
Điểm give
cuối tương tự như ví dụ vesting, nhưng có một số khác biệt.
Thay vì tính toán dữ liệu, chúng tôi sẽ xây dựng một cái gì đó kiểu VestingParam
. Chúng tôi cũng thay đổi tham chiếu đến dữ liệu mustPayToTheScript
thành ()
và chúng tôi cung cấp kiểu p
cho typedValidator
nó vì nó không còn là hằng số nữa.
give **::** AsContractError e **\=>** GiveParams **\->** Contract w s e ()
give gp **\=** **do**
**let** p **\=** VestingParam
{ beneficiary **\=** gpBeneficiary gp
, deadline **\=** gpDeadline gp
}
tx **\=** mustPayToTheScript () $ Ada.lovelaceValueOf $ gpAmount gp
ledgerTx **<-** submitTxConstraints (typedValidator p) tx
void $ awaitTxConfirmed $ txId ledgerTx
logInfo @String $ printf "made a gift of %d lovelace to %s with deadline %s"
(gpAmount gp)
(show $ gpBeneficiary gp)
(show $ gpDeadline gp)
Cuối grab
cùng, cũng có một số thay đổi.
Hãy nhớ lại rằng trước đó chúng ta đã có tất cả các UTxO ở tại một địa chỉ tập lệnh này và chúng có thể dành cho những người thụ hưởng tùy ý và cho các thời hạn tùy ý. Vì lý do này, chúng tôi phải lọc những UTxO dành cho chúng tôi và những nơi đã đến thời hạn.
Bây giờ chúng tôi có tham số bổ sung, mà chúng tôi sẽ gọi d
, đại diện cho thời hạn. Vì vậy, chúng tôi có thể ngay lập tức xem nếu thời hạn đã đến hay chưa.
Nếu nó vẫn chưa đạt được, chúng tôi viết một thông báo nhật ký và dừng lại, nếu không chúng tôi tiếp tục và xây dựng VestingParam
.
Sau đó, chúng tôi tìm kiếm các UTxO có tại địa chỉ này. Địa chỉ không phải là một hằng số nữa, nó nhận một tham số. Vì vậy, bây giờ, chúng tôi sẽ chỉ nhận được UTxO dành cho chúng tôi và đã đến thời hạn. Chúng ta không cần phải lọc bất cứ thứ gì.
Nếu không có, chúng tôi ghi lại một thông báo cho hiệu ứng đó và dừng lại, nếu không, chúng tôi làm nhiều hơn hoặc ít hơn những gì chúng tôi đã làm trước đây.
grab d **\=** **do**
now **<-** currentTime
pkh **<-** pubKeyHash <$> ownPubKey
**if** now < d
**then** logInfo @String $ "too early"
**else** **do**
**let** p **\=** VestingParam
{ beneficiary **\=** pkh
, deadline **\=** d
}
utxos **<-** utxoAt $ scrAddress p
**if** Map.null utxos
**then** logInfo @String $ "no gifts available"
**else** **do**
**let** orefs **\=** fst <$> Map.toList utxos
lookups **\=** Constraints.unspentOutputs utxos <>
Constraints.otherScript (validator p)
tx **::** TxConstraints Void Void
tx **\=** mconcat \[mustSpendScriptOutput oref $ Redeemer $ PlutusTx.toData () | oref **<-** orefs\] <>
mustValidateIn (from now)
ledgerTx **<-** submitTxConstraintsWith @Void lookups tx
void $ awaitTxConfirmed $ txId ledgerTx
logInfo @String $ "collected gifts"
Chức endpoints
năng hơi khác do tham số mới cho grab
.
endpoints **::** Contract () VestingSchema Text ()
endpoints **\=** (give' \`select\` grab') \>> endpoints
**where**
give' **\=** endpoint @"give" \>>= give
grab' **\=** endpoint @"grab" \>>= grab
3.5.3. Quay lại sân chơi
Bây giờ chúng tôi sẽ sao chép và dán hợp đồng mới này vào sân chơi và thiết lập một kịch bản mới.
Các give
giao dịch đều giống nhau.
Là grab
hơi khác nhau. Trong lần triển khai trước đó của chúng tôi, một ví có thể lấy UTxO với các thời hạn khác nhau miễn là thời hạn đó đã qua. Bây giờ thời hạn là một phần của tham số script, vì vậy chúng ta cần chỉ định nó để lấy địa chỉ script. Điều này có nghĩa là Ví 2 không thể lấy quà cho các vị trí 10 và 20 cùng một lúc, ít nhất là không theo cách mà chúng tôi đã triển khai.
Trước tiên, chúng ta có thể đợi cho đến khe 10 và sau đó Ví 2 sẽ có thể nhận được món quà đầu tiên và Ví 3 sẽ có thể nhận được món quà duy nhất của nó.
Chúng tôi sẽ thêm một grab
cho Ví 2 và 3. Ở đây, chúng tôi không cần phải phân biệt giữa mỗi giao dịch vì nó là hai ví khác nhau.
Sau đó, chúng tôi đợi cho đến khe 20 và thực hiện lần thứ 2 của Wallet 2 grab
và sau đó đợi 1 khối, như bình thường.
Vì vậy, hãy xem nếu nó hoạt động bằng cách nhấp vào Evaluate
.
Ghi lại địa chỉ tập lệnh cho giao dịch đó tại vị trí 1.
Và so sánh địa chỉ này với địa chỉ tập lệnh cho đầu ra giao dịch tại vị trí 2.
Lưu ý rằng địa chỉ tập lệnh cho các UTxO là khác nhau. Trong phiên bản đầu tiên của hợp đồng vesting, địa chỉ tập lệnh là một hằng số. Điều này có nghĩa là tất cả quà tặng của chúng tôi kết thúc ở cùng một địa chỉ tập lệnh và chỉ có dữ liệu trong mỗi UTxO là khác nhau.
Bây giờ, dữ liệu là chính xác ()
và người thụ hưởng và thời hạn được bao gồm như một phần của chính tập lệnh, vì vậy các địa chỉ bây giờ khác nhau tùy thuộc vào người thụ hưởng và các tham số thời hạn.
Đối với món quà cho Ví 3, chúng tôi thấy có một địa chỉ khác.
Chúng tôi thấy hai lần lấy ở vị trí số 10, một của Ví 2 và một của Ví 3. Thứ tự mà chúng được xử lý không mang tính xác định.
Sau đó, cuối cùng ở khe 20, Wallet 2 lấy phần quà còn lại của nó.
Và số dư cuối cùng phản ánh các giao dịch đã xảy ra.