Sử dụng Marlowe bằng dòng lệnh ghci

Hướng dẫn này chỉ cho bạn cách sử dụng Marlowe từ bên trong Haskell và đặc biệt cho thấy cách thực hiện hợp đồng bằng cách sử dụng ngữ nghĩa đã cho trước đó.

Marlowe ở Haskell

Hướng dẫn này hoạt động trong phiên bản Marlowe có thể được tìm thấy trong mainnhánh của kho lưu trữ marlowe-cardano . Chúng tôi có thể chạy ghci bằng cách sử dụng nix-shell có sẵn trong kho lưu trữ marlowe-dependency-docs :

git clone "https://github.com/input-output-hk/marlowe-dependency-docs.git"
cd marlowe-dependency-docs
nix-shell
ghci

Bạn cũng có thể tìm thấy một phiên bản độc lập và chính thức hóa ngữ nghĩa trong kho lưu trữ marlowe , nhưng một số chi tiết có thể hơi khác một chút, chẳng hạn như việc sử dụng các khe thay vì thời gian POSIX.

Bước qua các hợp đồng

Như chúng ta đã thấy trước đó, ngữ nghĩa của một giao dịch đơn lẻ được xác định bởi hàm

computeTransaction :: TransactionInput -> State -> Contract -> TransactionOutput

trong đó các loại được định nghĩa như thế này:

data TransactionInput = TransactionInput
    { txInterval :: TimeInterval
    , txInputs   :: [Input] }

data TransactionOutput =
   TransactionOutput
      { txOutWarnings :: [TransactionWarning]
      , txOutPayments :: [Payment]
      , txOutState    :: State
      , txOutContract :: Contract }
   | Error TransactionError

Statesđược định nghĩa như thế này, với một hàm trợ giúp để xác định trạng thái trống ban đầu:

data State = State { accounts    :: Accounts
                   , choices     :: Map ChoiceId ChosenNum
                   , boundValues :: Map ValueId Integer
                   , minTime     :: POSIXTime }

emptyState :: POSIXTime -> State
emptyState sn = State { accounts = Map.empty
                      , choices  = Map.empty
                      , boundValues = Map.empty
                      , minTime = sn }

Chúng tôi có thể sử dụng các phương tiện của ghciđể thực hiện từng giao dịch một trong hợp đồng và tại đây, chúng tôi sẽ thực hiện điều đó với hợp đồng ký quỹ được nhúng trong Escrow.hs .

Để tải hợp đồng vào trình thông dịch, trước tiên, chúng tôi nhập một số thư viện và tạo các chức năng tiện ích:

Prelude> :set -XOverloadedStrings
Prelude> import qualified Marlowe.Contracts.Escrow as Escrow
Prelude Escrow> import qualified Language.Marlowe.Extended as EM
Prelude Escrow EM> import Language.Marlowe as M
Prelude Escrow EM M> import qualified Data.Time.Clock.POSIX as P
Prelude Escrow EM M P> :set prompt "> "
> let toPOSIX = POSIXTime . floor . P.utcTimeToPOSIXSeconds . read
> let toEMPOSIX = EM.POSIXTime . floor . P.utcTimeToPOSIXSeconds . read

Ví dụ được viết bằng Extended Marlowe, vì vậy trước tiên chúng ta cần chuyển đổi nó thành Marlowe cốt lõi để thực thi nó:

> let Just contract = EM.toCore $ Escrow.escrow (EM.Constant 450) "bob" "alice" "carol" (toEMPOSIX "2023-02-01 00:00:00.000000 UTC") (toEMPOSIX "2023-03-01 00:00:00.000000 UTC") (toEMPOSIX "2023-04-01 00:00:00.000000 UTC") (toEMPOSIX "2023-05-01 00:00:00.000000 UTC") :: Maybe Contract

Bây giờ chúng ta có thể thực hiện từng bước một bằng cách sử dụng cơ sở để tạo các ràng buộc cục bộ:

> let (TransactionOutput txWarn1 txPay1 state1 con1) = computeTransaction (TransactionInput (toPOSIX "2023-01-01 00:00:00.000000 UTC", toPOSIX "2023-01-31 23:59:59.000000 UTC") [NormalInput (IDeposit "bob" "alice" ada 450)]) (emptyState 0) contract

Khi làm điều này, chúng ta có mẫu phù hợp với đầu ra của một ứng dụng computeTransaction, trong đó có ba đầu vào: đầu vào thứ hai là trạng thái ban đầu (tại vị trí slot số 0) và thứ ba là hợp đồng ký quỹ ban đầu. Đầu tiên là một TransactionInputtrong đó có một TimeInterval- tại đây ((toPOSIX "2023-01-01 00:00:00.000000 UTC"), (toPOSIX "2023-01-31 23:59:59.000000 UTC")) - và một khoản tiền gửi 450 Lovelace từ tài khoản "alice" vào bob's' cụ thể là NormalInput (IDeposit "bob" "alice" ada 450).

Ghi chú

Nếu bạn muốn thử điều này cho chính mình trong ghci, bạn có thể sao chép và dán từ các ví dụ mã: chúng nằm trong cửa sổ cuộn theo chiều ngang.

Đầu ra được so khớp với TransactionOutput txWarn1 txPay1 state1 con1 để chúng tôi có thể kiểm tra các thành phần khác nhau một cách riêng biệt:

> txWarn1
[]
>  txPay1
[]
> state1
State {accounts = Map {unMap = [(("bob",Token "" ""),450)]}, choices = Map {unMap = []}, boundValues = Map {unMap = []}, minTime = POSIXTime {getPOSIXTime = 1672531200}}
> con1
When [Case (Choice (ChoiceId "Everything is alright" "alice") [Bound 0 0]) Close,Case (Choice (ChoiceId "Report problem" "alice") [Bound 1 1]) (Pay "bob" (Account "alice") (Token "" "") (Constant 450) (When [Case (Choice (ChoiceId "Confirm problem" "bob") [Bound 1 1]) Close,Case (Choice (ChoiceId "Dispute problem" "bob") [Bound 0 0]) (When [Case (Choice (ChoiceId "Dismiss claim" "carol") [Bound 0 0]) (Pay "alice" (Account "bob") (Token "" "") (Constant 450) Close),Case (Choice (ChoiceId "Confirm claim" "carol") [Bound 1 1]) Close] (POSIXTime {getPOSIXTime = 1682899200}) Close)] (POSIXTime {getPOSIXTime = 1680307200}) Close))] (POSIXTime {getPOSIXTime = 1677628800}) Close

Điều này cho thấy rằng giao dịch không tạo ra cảnh báo hoặc thanh toán, nhưng cập nhật trạng thái để hiển thị số dư trong tài khoản "bob"và cập nhật hợp đồng, sẵn sàng nhận được lựa chọn từ Alice.

Ở trạng thái tiếp theo, hợp đồng đang chờ đầu vào và nếu Alice đồng ý rằng “Everything is alright”, thì một khoản thanh toán cho Bob sẽ được tạo. Điều này được xác minh thông qua tương tác này trong GHCI:

> let (TransactionOutput txWarn2 txPay2 state2 con2) = computeTransaction (TransactionInput (toPOSIX "2023-02-01 00:00:00.000000 UTC", toPOSIX "2023-02-28 23:59:59.000000 UTC") [NormalInput (IChoice (ChoiceId "Everything is alright" "alice") 0)]) state1 con1
> txPay2
[Payment "bob" (Party "bob") (Value (Map [(,Map [("",450)])]))]
> con2
Close
> state2
State {accounts = Map {unMap = []}, choices = Map {unMap = [(ChoiceId "Everything is alright" "alice",0)]}, boundValues = Map {unMap = []}, minTime = POSIXTime {getPOSIXTime = 1675209600}}

Một cách khác để làm điều này là thêm các định nghĩa này vào một tệp đang làm việc, ví dụ Build.hs, nơi các định nghĩa này sẽ được giữ nguyên. Thật vậy, sẽ rất hợp lý nếu đưa một số định nghĩa được sử dụng ở trên vào một tệp như vậy.

Các tuyến đường thay thế thông qua hợp đồng

Một cách thực hiện hợp đồng thay thế được đưa ra bởi

  • Bước đầu tiên: Alice gửi tiền như trong ví dụ trước đó.

  • Bước thứ hai: Alice báo cáo một vấn đề và Bob không đồng ý. Điều này có thể được thực hiện như thế này:

> let (TransactionOutput txWarn2 txPay2 state2 con2) = computeTransaction (TransactionInput (toPOSIX "2023-02-01 00:00:00.000000 UTC", toPOSIX "2023-02-28 23:59:59.000000 UTC") [NormalInput (IChoice (ChoiceId "Report problem" "alice") 1), NormalInput (IChoice (ChoiceId "Dispute problem" "bob") 0)]) state1 con1
> con2
When [Case (Choice (ChoiceId "Dismiss claim" "carol") [Bound 0 0]) (Pay "alice" (Account "bob") (Token "" "") (Constant 450) Close),Case (Choice (ChoiceId "Confirm claim" "carol") [Bound 1 1]) Close] (POSIXTime {getPOSIXTime = 1682899200}) Close
> state2
State {accounts = Map {unMap = [(("alice",Token "" ""),450)]}, choices = Map {unMap = [(ChoiceId "Report problem" "alice",1),(ChoiceId "Dispute problem" "bob",0)]}, boundValues = Map {unMap = []}, minTime = POSIXTime {getPOSIXTime = 1675209600}}

Điều này cho thấy rằng chúng tôi hiện đang ở trong một hợp đồng mà sự lựa chọn là tùy thuộc vào Carol và vẫn còn 450 Lovelace trong "alice"tài khoản.

Lưu ý rằng chúng tôi có hai đầu vào trong cùng một giao dịch, Marlowe hỗ trợ điều này miễn là giao dịch có chữ ký của tất cả các bên liên quan và khoảng thời gian là trước khi hết thời gian sớm nhất When.

  • Bước thứ ba: Carol đưa ra lựa chọn. Nếu cô ấy chọn "Loại bỏ xác nhận quyền sở hữu - Dismiss claim", thì khoản thanh toán cho Bob sẽ được thực hiện. Nếu cô ấy chọn "Xác nhận yêu cầu - Confirm claim", Alice sẽ được hoàn lại tiền. Hãy làm điều đó ngay bây giờ:

> let (TransactionOutput txWarn3 txPay3 state3 con3) = computeTransaction (TransactionInput (toPOSIX "2023-04-01 00:00:00.000000 UTC", toPOSIX "2023-04-30 23:59:59.000000 UTC") [NormalInput (IChoice (ChoiceId "Confirm claim" "carol") 1)]) state2 con2
> txPay3
[Payment "alice" (Party "alice") (Value (Map [(,Map [("",450)])]))]
> con3
Close
> state3
State {accounts = Map {unMap = []}, choices = Map {unMap = [(ChoiceId "Report problem" "alice",1),(ChoiceId "Dispute problem" "bob",0),(ChoiceId "Confirm claim" "carol",1)]}, boundValues = Map {unMap = []}, minTime = POSIXTime {getPOSIXTime = 1680307200}}

Vì vậy, bây giờ hợp đồng đã sẵn sàng Close, và do đó, để hoàn trả bất kỳ số tiền còn lại nào, nhưng rõ ràng state3là không có tài khoản nào chứa số dư khác 0, và do đó hợp đồng bị chấm dứt.

Tại sao một bước lại hữu ích? Nó tương đương với gỡ lỗi và chúng ta có thể thấy trạng thái bên trong của hợp đồng ở mỗi giai đoạn, sự tiếp tục của hợp đồng, tức là những gì còn lại sẽ được thực hiện và các hành động được tạo ra ở mỗi bước.

Bài tập

Khám phá một số cách khác để tham gia vào hợp đồng - Điều gì xảy ra khi Bob xác nhận có vấn đề? - Chuyện gì xảy ra nếu Bob và Alice không đồng ý, nhưng Carol lại đứng về phía Bob?

Last updated