Skip to main content

Week 03 - Script Context

Đây là phiên bản đã dịch của Bài giảng số 3 Dr. Lars.

Trong bài giảng này, chúng ta tìm hiểu về context 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 là 81ba78edb1d634a13371397d8c8b19829345ce0d.

Trước khi chúng ta bắt đầu

Kể từ bài giảng cuối cùng đã có một 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ã 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

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 một địa chỉ kịch bản, kịch bản gắn liền với địa chỉ được chạy, và kịch bản này có được ba thông tin - datum, redeemercontext.

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 biểu thị bằng kiểu Data. 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 Typed, trong đó Datum và Redeemer có thể là kiểu tùy chỉnh (miễn là chúng triển khai lớp kiểu IsData) 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 Datum và Redeemer, nhưng chúng tôi luôn bỏ qua Context. Nhưng Context 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 với Context.

ScriptContext

ScriptContext được định nghĩa trong gói plutus-ledger-api, mà là một gói phần mềm, cho đến bây giờ, chúng tôi đã không cần thiết. Nhưng bây giờ chúng tôi cần nó, và nó đã được đưa vào files .cabal 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 context 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.

Các Minting dùng khi bạn muốn định nghĩa một token gốc. Mục đích của nó là chúng tôi mô tả trong những trường hợp nào token 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 chứa Context thực tế scriptContextTxInfo là trường 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, Context xác thực là giao dịch chi tiêu và các đầu vào và đầu ra của nó. Context này được thể hiện trong TxInfo.

Có một số trường là toàn cầu cho toàn bộ giao dịch và cụ thể 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 thứ trong số chúng có nhiều trường 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 token 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ý kết 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. Các txInfoData lĩnh vực là một danh sách liên kết Datum với băm tương ứng của nọ. Nếu có một đầu ra giao dịch tới một địa chỉ tập lệnh mang một số dữ liệu nào đó, bạn không cần phải bao gồm dữ liệu đó, bạn chỉ có thể bao gồm dữ liệu băm . Tuy nhiên, các tập lệnh sử dụng một đầu ra cần phải bao gồm Datum, trong trường hợp đó, nó sẽ được đưa vào danh sách txInfoData .

Cuối cùng, trường txInfoId là ID của giao dịch này.

txInfoValidRange

Mặc dù có rất nhiều thông tin chứa trong kiểu txInfo, 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 trường txInfoValidRange 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 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 on-chain sau khi xác thực, nếu khi giao dịch 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 Context đó. 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ó hiệu lực 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 1 - ví dụ đấu giá (bid), trong đó giá thầu chỉ được phép cho đến khi đạt đến thời hạn cuối cùng và Endpoint close chỉ khi có thể gọ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 tạo 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õ 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í, thì 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í kia".

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ó và số dư cộng lại cả 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 slot. 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à khe slot. Điều này không có vấn đề gì miễn là thời gian slot được cố định. Ngay bây giờ là một giây, vì vậy ngay bây giờ nó là 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 tôi 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. Chỉ càng xa trong tương lai thì người ta mới có thể biết được độ dài slot 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ờ.

POSIXTimeRange

Hãy xem POSIXTimeRange này , được định nghĩa trong Plutus.V1.Ledger.Time.

type POSIXTimeRange = Interval POSIXTime.

Nó là một từ đồng nghĩa với Interval POSIXTime và chúng ta thấy rằng nó Interval được định nghĩa bởi LowerBoundUpperBound.

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 với Bool và chắc chắn rằng 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.

Chúng tôi cũng tìm thấy một số hàm trợ giúp bao gồm cả hàm member 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 hay không Interval, 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 này tạo Interval với giới hạn trên và dưới.

interval :: a -> a -> Interval a
interval s s' = Interval (lowerBound s) (upperBound s')

Sau đó chúng ta có from với Interval cái này bắt đầu từ a và kéo dài đến vô cùng.

from :: a -> Interval a
from s = Interval (lowerBound s) (UpperBound PosInf True)

Và chúng ta có to, nó là ngước lại với from. Nó cũng được dùng Interval nó bắt đầu block genesis tới a, và bao gồm cả a.

to :: a -> Interval a
to s = Interval (LowerBound NegInf True) (upperBound s)

always luôn mặc định Interval bao gồm tất cả từ âm vô cùng đến dương vô cùng.

always :: Interval a
always = Interval (LowerBound NegInf True) (UpperBound PosInf True)

và có điều ngươc lại, never, Nó không chứa slots nào.

never :: Interval a
never = Interval (LowerBound PosInf True) (UpperBound NegInf True)

Ngoài ra còn trình trợ giúp singleton, nó bao gồm một slot

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 thời gian lớn nhất được chứa trong khoảng thời gian nhất định. Đâ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 chức năng cho dù hai khoảng thời gian chồng lên nhau, có nghĩa là, cho dù có một giá trị chồng lên nhau của cả hai khoảng thời gian.

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 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 beforeafter để 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 gian 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 vào 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ả hai đầu.

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. Ở đây giới hạn dưới lại là một slot 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. Ở đây giới hạn dưới là âm vô cùng, trong khi giới hạn trên là số slot 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ờ, chúng ta hãy thử hàm intersection với 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ư mong đợi, chúng tôi nhận được Interval chạy từ 18 đến 20, bao gồm cả giá trị hai đầu.

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 tôi 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

Ví dụ - Vesting

Hãy tưởng tượng bạn muốn tặng một món quà bằng 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 tròn 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ố Context, 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 hợp đồng và sau đó nó có thể được lấy bởi một người nào đó, 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 hợp đồng IsData từ bài giảng 2 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 DatumRedeemer.

Đối với Datum, đ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 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 Redeemer, vì vậy chúng tôi chỉ có thể sử dụng () cho trình Redeemer.

mkValidator :: VestingDatum -> () -> ScriptContext -> Bool

Chúng ta cần kiểm tra hai điều kiện.

  1. 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 trong giao dịch hay không.
  2. 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 ký bởi người thụ hưởng hay không, chúng tôi 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.

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 thực không thể biết liệu thời điểm 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à đúng. 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 cuối cùng. Một cách để làm điều này là sử dụng hàm contains để 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 hết 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ố 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 ra thêm vào tham số mới LANGUAGE và một số module bổ xung, chung tôi có tạo kiểu GiveParams, và sửa endpoint grab để không yêu cầu tham số.

Các kiểu VestingSchema định nghĩa endpoints cái mà Chúng ta muốn cho người dùng biết. 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 nhận tiền.

type VestingSchema =
.\/ Endpoint "give" GiveParams
.\/ Endpoint "grab" ()

Vậy chúng ta cần những thông số nào give? Endpoint sẽ tạo một UTxO tại địa chỉ tập lệnh vesting với một số tiền và một Datum. Nếu bạn nhớ lại, Datum 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 endpoint give.

data GiveParams = GiveParams
{ gpBeneficiary :: !PubKeyHash
, gpDeadline :: !POSIXTime
, gpAmount :: !Integer
} deriving (Generic, ToJSON, FromJSON, ToSchema)

Endpoint grab 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ể dùng chúng.

Giờ hãy nhìn vào endpoint give .

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.

Endpoint grab 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 trôi 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 tra cứu tất cả các UTxO tại địa chỉ này và lọc chúng bằng cách sử dụng hàm isSuitable trợ giúp, được định nghĩa trong mệnh đề where.

Trước tiên, nó kiểm tra băm Datum và nếu tìm thấy nó, nó sẽ cố gắng tìm kiếm Datum 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 datum, nó chỉ cần cung cấp băm datum. 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 endpoint grab , vì vậy endpoint give cung cấp Datum.

Nếu endpoint grab tìm thấy Datum, nó phải chuyển thành kiểuVesting .

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ìn hàm 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ừ slot hiện tại cho đến hết thời hạn. Nếu chúng tôi không cung cấp khoảng thời gian ở đây, thì việc 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. Xác thực On-chain 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 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 slot, thì quá trình xác thực sẽ không hoạt động nữa.

và giờ, chúng ta chỉ tập hợp các endpoints.

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 được sử dụng trong playground.

mkSchemaDefinitions ''VestingSchema

mkKnownCurrencies []

Trong sân chơi (playground)

Đầ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 và cũng tạo một quà tặng cho Ví 3 với thời hạn khác.

Thông thường, có thể gửi cả hai giao dịch give 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í (slot) 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à là một khoảng thời gian.

Vậy đây là SlotConfig?

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.

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 cho give ví đầu tiên và thứ ba và vị trí 20 cho give ví thứ hai. Chúng tôi cũng sẽ đưa ra 10 Ada trong mỗi trường hợp.

Hãy tạo một kịch bản mà mọi thứ đều hoạt động. Ví 3 nhận ở 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 Wait Until.. tùy chọn cho việc này.

Sau khi đánh giá, đầ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 slot. Tại đây, 10 Ada bị khóa tại đị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 slot. Một UTxO mới hiện đã được tạo tại địa chỉ tập lệnh với 10 Ada.

Và món quà thứ ba, lần này là Ví 3, với thời hạn là 10 slot. Ví 1 hiện còn 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, Ví 3 lấy ADA 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 đượ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 thực.

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ẽ tạo cho Wallet 2 lấy ở 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ỉ tập lệnh.

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 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ã ngoài chuỗi. Vì vậy, mặc dù nó hoạt động ngay bây giờ miễn là bạn sử dụng endpointgrab 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.

Ví dụ 2 - Tham số hóa hợp đồng (Parameterized Contract)

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.

On-Chain

Lưu ý rằng trong ví dụ vesting, chúng tôi đã sử dụng kiểu Vesting 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 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 and/or 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 nó thay vì cụ thể cho Datum 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 Datum 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 vì chúng tôi không cần nó nữa.

Lý do chúng ta không cần nó, là vì chúng ta sẽ sử dụng () cho datum trong hàm mkValidator. 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 kiểu Vesting để phản ánh sự thay đổi đối với datum.

data Vesting
instance Scripts.ValidatorTypes Vesting where
type instance DatumType Vesting = ()
type instance RedeemerType Vesting = ()

Bây giờ, TypedValidator 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ề một 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, chuyể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 mà bất kỳ tham số nào được truyền đến validator bây giờ sẽ được chuyển đến hàm typedValidator một cách hiệu quả , giá trị trả về của chúng sẽ được chuyển đến validatorScript.

validator :: VestingParam -> Validator
validator = Scripts.validatorScript . typedValidator

Và tương tự đối với valHashscrAddress.

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 hàm typedValidator.

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 [] 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 thống số INLINABLE trong hàm mkValidator, 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.

p không biết đến lúc biên dịch, bởi vì chúng tôi có ý định cung cấp nó khi 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ó hàm mkValidator của mình và chúng tôi có p kiểu VestingParam. Chúng tôi có thể biên dịch mkValidator sang Plutus core, nhưng chúng tôi không thể biên dịch p sang Plutus core 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 pkhô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 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 biên dịch 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.

Lớp Lift

Chúng ta hãy nhìn sơ qua về lớp Lift Nó được định nghĩa trong gói plutus-tx.

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, các giá trị lift của 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 mình p thành mã.

Chúng ta sẽ sử dụng một hàm 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 lớp Lift 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 nhận 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 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 ta cần bật tiện ích mở rộng GHC.

{-# LANGUAGE MultiParamTypeClasses #-}

Bây giờ nó sẽ biên dịch.

Off-Chain

Mã Off-Chain không thay đổi nhiều.

Các GiveParams vẫn giống nhau.

data GiveParams = GiveParams
{ gpBeneficiary :: !PubKeyHash
, gpDeadline :: !POSIXTime
, gpAmount :: !Integer
} deriving (Generic, ToJSON, FromJSON, ToSchema)

VestingSchema đã thay đổi một chút vì endpoint grab 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

endpointgive 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 Datum trong mustPayToTheScript để trở thành (), và chúng tôi cung cấp các loại p để typedValidator như nó không còn là một hằng số.

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)

Endpoint grab cũng có một số thay đổi.

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 những 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 lọc bất cứ thứ gì.

Nếu không có thông báo nào, 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 đó.

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"

Các hàm endpoints là hơi khác nhau do các tham số mới chograb.

endpoints :: Contract () VestingSchema Text ()
endpoints = (give' `select` grab') >> endpoints
where
give' = endpoint @"give" >>= give
grab' = endpoint @"grab" >>= grab

Quay lại với sân chơi (playground)

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.

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à 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ứ hai của grabví 2 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í 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.