Smart Contract
Hãy bắt đầu bằng cách viết một hợp đồng thông minh mà chúng ta sẽ truyền đạt các tính năng quan trọng nhất của khung.
Hợp đồng này chỉ nhằm mục đích minh họa. Chúng ta khuyên bạn chỉ nên sử dụng nó trong môi trường thử nghiệm.
Ở đây chúng ta khẳng định rằng UTxO được sử dụng làm đầu vào tham chiếu phải thuộc địa chỉ của Oracle nhưng lưu ý rằng bất kỳ ai cũng có thể tạo UTxO tại địa chỉ của Oracle.
Mô tả hợp đồng
Lấy bối cảnh ở đây là chúng ta có một trận đấu thể thao đang diễn ra và một nhóm bạn muốn đặt cược vào số bàn thắng mà đội bóng yêu thích của họ ghi được trong đó.
Người chiến thắng là người đoán đúng nhất (và trong trường hợp hòa - người đoán nhanh nhất!).
Mã hợp đồng thông minh có sẵn tại đây
Ví dụ này được lấy cảm hứng từ MLabs.
Các tham số của hợp đồng
brpOraclePkh :: PubKeyHash
: Chúng ta sẽ sử dụng đầu vào tham chiếu và mốc thời gian của nó sẽ cung cấp cho chúng ta kết quả thực tế (số lượng mục tiêu). Do đầu vào tham chiếu UTxO phải thuộc về Oracle nên chúng ta kiểm tra nó bằng hàm Hash key công khai thanh toán của Oracle.brpBetUntil :: POSIXTime
: Thời gian cho đến khi có thể đặt cược.brpBetReveal :: POSIXTime
: Thời gian mà Oracle sẽ tiết lộ kết quả trận đấu.brpBetStep :: Value
: Giá trị tối thiểu mà cược phải tăng thêm.
Do đó, các tham số của hợp đồng của chúng ta được đưa ra bởi:
-- | Our contract is parameterized with this.
data BetRefParams = BetRefParams
{ brpOraclePkh :: PubKeyHash -- ^ Oracle's payment public key hash. This is needed to assert that UTxO being looked at indeed belongs to the Oracle.
, brpBetUntil :: POSIXTime -- ^ Time until which bets can be placed.
, brpBetReveal :: POSIXTime -- ^ Time at which Oracle will reveal the correct match result.
, brpBetStep :: Value -- ^ Each newly placed bet must be more than previous bet by `brpBetStep` amount.
}
PlutusTx.makeLift ''BetRefParams
Dữ liệu tùy biến đầu vào tham chiếu (Reference Input Datum)
Oracle cho chúng ta biết số bàn thắng mà đội có liên quan ghi được:
-- | Goals made my the concerned team.
type TeamGoals = Integer
-- | Match result given by the Oracle.
newtype OracleAnswerDatum = OracleAnswerDatum TeamGoals deriving newtype (Eq, Show)
PlutusTx.unstableMakeIsData ''OracleAnswerDatum
Dữ liệu tùy biến của hợp đồng (Contract Datum)
- Danh sách chứa dự đoán của mỗi người cùng với hàm hash (băm) khóa công khai thanh toán của họ. Hash key được sử dụng để liên kết dự đoán với người đoán. Mỗi khi một dự đoán mới được thực hiện, chúng ta sẽ thêm nó vào danh sách này. Hàm Hash key này được lấy từ các bên ký kết giao dịch - chúng ta nhấn mạnh vào hàm Hash key phải có mặt trong các bên ký kết vì nếu không, bất kỳ ai cũng có thể ghi đè đặt cược của người khác.
- Số tiền biểu thị cược đã đặt trước đó. Lưu ý rằng tổng giá trị trong UTxO có liên quan là lũy kế của tất cả các lần đặt cược trước đó và do đó không phải là lần đặt cược cuối cùng. Chúng ta sử dụng điều này để biểu thị rằng lần đặt cược mới phải nhiều hơn lần đặt trước theo số tiền
brpBetStep
.
-- | List of guesses by users along with the maximum bet placed yet.
-- | A new guess gets /prepended/ to this list.
-- | Note that since we are always meant to increment previously placed bet
-- | with `brpBetStep`, the newly placed bet would necessarily be maximum
-- | (it would be foolish to initialize `brpBetStep` with some negative amounts).
data BetRefDatum = BetRefDatum
{ brdBets :: [(PubKeyHash, OracleAnswerDatum)]
, brdPreviousBet :: Value
}
PlutusTx.unstableMakeIsData ''BetRefDatum
Contract Redeemer
Có hai hành động lựa chọn cho người dùng:
- Để đặt cược - trong trường hợp đó họ đưa ra dự đoán của mình.
- Nhận tiền cược trong pot sau khi có kết quả.
Do đó, điều này được mã hóa thành:
-- | Redeemer representing choices available to the user.
data BetRefAction = Bet !OracleAnswerDatum -- ^ User makes a guess.
| Take -- ^ User takes the pot.
PlutusTx.unstableMakeIsData ''BetRefAction
Logic của hợp đồng
Đặt cược
Đặt cược ban đầu được đặt như hiện tại (trong Cardano, tập lệnh trình xác thực chỉ được thực thi khi chi tiêu UTxO thuộc về nó chứ không phải để tạo tại đó).
Đối với các lần đặt cược tiếp theo, chúng ta yêu cầu ba điều kiện:
- Đặt cược phải trước (hoặc bằng) thời gian
brpBetUntil
. - Phải có chính xác một đầu ra tiếp tục tại địa chỉ tập lệnh có dữ liệu dự đoán hiện tại được thêm vào trước cùng với số tiền đặt cược hiện tại.
- Số tiền đặt cược hiện tại phải nhiều hơn số tiền đặt cược trước đó ít nhất
brpBetStep
.
Điều này được mã hóa là:
{-# INLINABLE mkBetRefValidator' #-}
-- | Core smart contract logic. Read its description from Atlas guide.
mkBetRefValidator' :: BetRefParams -> BetRefDatum -> BetRefAction -> ScriptContext -> Bool
mkBetRefValidator' (BetRefParams oraclePkh betUntil betReveal betStep) (BetRefDatum previousGuesses previousBet) brAction ctx =
case brAction of
Bet guess ->
let
sOut = case getContinuingOutputs ctx of
[sOut'] -> sOut'
_anyOtherMatch -> traceError "Expected only one continuing output."
outValue = txOutValue sOut
sIn = maybe (traceError "Could not find own input") txInInfoResolved (findOwnInput ctx)
inValue = txOutValue sIn
(guessesOut, betOut) = case outputToDatum sOut of
Nothing -> traceError "Could not resolve for script output datum"
Just (BetRefDatum guessesOut' betOut') -> (guessesOut', betOut')
in
traceIfFalse
"Must be before `BetUntil` time"
(to betUntil `contains` validRange) &&
traceIfFalse
"Guesses update is wrong"
((signerPkh, guess) : previousGuesses == guessesOut) &&
traceIfFalse
"The current bet must be more than the previous bet by atleast `brpBetStep` amount"
(outValue `geq` (inValue <> previousBet <> betStep)) &&
traceIfFalse
"Out bet is wrong"
(betOut == outValue - inValue)
Nơi chúng ta có những người trợ giúp chung sau đây cho cả hai hành động của người dùng (redemeer):
where
info :: TxInfo
info = scriptContextTxInfo ctx
validRange :: POSIXTimeRange
validRange = txInfoValidRange info
signerPkh :: PubKeyHash
signerPkh = case txInfoSignatories info of
[signerPkh'] -> signerPkh'
[] -> traceError "No signatory"
_anyOtherMatch -> traceError "Expected only one signatory"
outputToDatum :: FromData b => TxOut -> Maybe b
outputToDatum o = case txOutDatum o of
NoOutputDatum -> Nothing
OutputDatum d -> processDatum d
OutputDatumHash dh -> processDatum =<< findDatum dh info
where processDatum = fromBuiltinData . getDatum
Lấy tiền đặt cược
Trong trường hợp này, chúng ta yêu cầu bốn điều kiện sau:
- Hoạt động này phải xảy ra sau thời gian
brpBetReveal
. - Tập lệnh phải được chi tiêu đầy đủ, tức là không được có bất kỳ đầu ra tiếp tục nào tới địa chỉ tập lệnh này.
- Đầu vào tham chiếu có dữ liệu được sử dụng để xem câu trả lời thực tế phải thuộc về Oracle có liên quan.
- Lần cược được xem xét phải là lần cược cuối cùng.
Do đó, điều này được mã hóa là:
Take ->
let
Just guess = find ((== signerPkh) . fst) previousGuesses -- Note that `find` returns the first match. Since we were always prepending, this is valid.
oracleIn = case filter (isNothing . txOutReferenceScript) (txInInfoResolved <$> txInfoReferenceInputs info) of
[oracleIn'] -> oracleIn'
[] -> traceError "No reference input provided"
_anyOtherMatch -> traceError "Expected only one reference input"
oracleAnswer = case outputToDatum oracleIn of
Nothing -> traceError "Could not resolve for datum"
(Just (OracleAnswerDatum oracleAnswer')) -> oracleAnswer'
guessDiff = getGuessDiff $ snd guess
getGuessDiff (OracleAnswerDatum g) = abs (oracleAnswer - g)
oracleInPkh = case toPubKeyHash (txOutAddress oracleIn) of
Nothing -> traceError "Not PKH for oracle address"
Just pkh -> pkh
in
traceIfFalse
"Must be after `RevealTime`"
(from betReveal `contains` validRange) &&
traceIfFalse
"Must fully spend Script"
(null (getContinuingOutputs ctx)) &&
traceIfFalse
"Reference input must be from Oracle address (wrt Payment part)"
(oracleInPkh == oraclePkh) &&
traceIfFalse
"Guess is not closest"
(all (\pg -> getGuessDiff (snd pg) >= guessDiff) previousGuesses)
Và xong rồi! Đó là hợp đồng của chúng ta.