Haskell ledger bindings (#1181)

* Bindings & tests for more ledger services

* fix hardcoded package id in test

* language-support/hs/bindings/examples/nim-console

* make hlint happy

* headers & format

* README

* increase timeouts; track location of GPRC Deadline Exceeded.

* Update language-support/hs/bindings/examples/nim-console/README.md

Co-Authored-By: Neil Mitchell <35463327+neil-da@users.noreply.github.com>

* rename: Mes --> Logger, mes --> log, tagMes --> tagLog (etc)

* replace isOpen with ==Open

* Avoid fragile hardcoded PackageId in testcase
This commit is contained in:
nickchapman-da 2019-05-16 16:45:54 +01:00 committed by GitHub
parent 69429f3a14
commit 8510e6afc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1152 additions and 177 deletions

View File

@ -9,6 +9,7 @@ da_haskell_library(
srcs = glob(["src/**/*.hs"]),
hazel_deps = [
"base",
"bytestring",
"text",
"vector",
"containers",
@ -16,6 +17,9 @@ da_haskell_library(
],
visibility = ["//visibility:public"],
deps = [
"//compiler/daml-lf-ast",
"//compiler/daml-lf-proto",
"//daml-lf/archive:daml_lf_haskell_proto",
"//ledger-api/grpc-definitions:ledger-api-haskellpb",
"//nix/third-party/gRPC-haskell:grpc-haskell",
],
@ -38,6 +42,27 @@ da_haskell_binary(
],
)
da_haskell_binary(
name = "nim",
srcs = glob(["examples/nim-console/src/*.hs"]),
compiler_flags = [],
hazel_deps = [
"ansi-terminal",
"base",
"containers",
"extra",
"haskeline",
"random",
"text",
"transformers",
"uuid",
],
visibility = ["//visibility:public"],
deps = [
"//language-support/hs/bindings:hs-ledger",
],
)
daml_compile(
name = "quickstart",
srcs = glob(["test/daml/quickstart/*.daml"]),
@ -57,10 +82,11 @@ da_haskell_test(
"directory",
"extra",
"process",
"split",
"tasty",
"tasty-hunit",
"text",
"random",
"uuid",
],
main_function = "DA.Ledger.Tests.main",
src_strip_prefix = "test",

View File

@ -25,12 +25,12 @@ main = do
let lid = Ledger.identity h
putStrLn $ "LedgerIdentity = " <> show lid
aliceTs <- Ledger.getTransactionStream h alice
bobTs <- Ledger.getTransactionStream h bob
aliceTs <- Ledger.transactions h alice
bobTs <- Ledger.transactions h bob
watch (show alice) aliceTs
watch (show bob) bobTs
cs <- Ledger.getCompletionStream h myAid [alice,bob]
cs <- Ledger.completions h myAid [alice,bob]
watch "completions" cs
sleep 1
@ -82,9 +82,9 @@ myAid = ApplicationId "<my-application>"
randomCid :: IO CommandId
randomCid = do fmap (CommandId . Text.pack . UUID.toString) randomIO
watch :: Show a => String -> ResponseStream a -> IO ()
watch :: Show a => String -> Stream a -> IO ()
watch tag rs = void $ forkIO $ loop (1::Int)
where loop n = do
x <- nextResponse rs
x <- takeStream rs
putStrLn $ tag <> "(" <> show n <> ") = " <> show x
loop (n+1)

View File

@ -0,0 +1,10 @@
# `nim-console`
Example of a ledger app written in Haskell, to demo the Haskell ledger bindings.
Currently the app is hooked up with a simulated ledger. Next step: hook up to real ledger!
## Build and Run
$ bazel run language-support/hs/bindings:nim

View File

@ -0,0 +1,62 @@
-- Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
-- Domain types (will originate in Daml model)
module Domain(Player(..),
Offer(..),
Game(..),
Move(..),
playersOfGame,
playersOfOffer,
legalMovesOfGame,
initGame,
playMove
) where
import Data.List as List(splitAt)
data Player = Player String
deriving (Eq,Ord)
instance Show Player where show (Player s) = s
data Offer = Offer { from :: Player, to :: [Player] }
deriving (Show)
data Game = Game { p1 :: Player, p2 :: Player, piles :: [Int] }
deriving (Show)
data Move = Move { pileNum :: Int, howMany :: Int }
deriving Show
playersOfGame :: Game -> [Player]
playersOfGame Game{p1,p2} = [p1,p2]
playersOfOffer :: Offer -> [Player]
playersOfOffer Offer{from,to} = from : to
legalMovesOfGame :: Game -> [Move]
legalMovesOfGame Game{piles} = do
(pileNum,remaining) <- zip [1..] piles
howMany <- [1..min 3 remaining]
return $ Move {pileNum,howMany}
initGame :: Player -> Player -> Game
initGame p1 p2 = Game {p1, p2, piles = standardInitPiles}
standardInitPiles :: [Int]
standardInitPiles = [7,5,3]
type Rejection = String
playMove :: Move -> Game -> Either Rejection Game
playMove Move{pileNum,howMany} Game{p1,p2,piles} =
case List.splitAt (pileNum - 1) piles of
(xs,selected:ys)
| howMany > 3 -> Left "may only take 1,2 or 3"
| selected < howMany -> Left "not that many in pile"
| otherwise -> Right $ Game { p1 = p2, p2 = p1, piles = xs ++ [selected - howMany] ++ ys }
_ -> Left"no such pile"

View File

@ -0,0 +1,49 @@
-- Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
module External(XCommand(..),
XTrans(..),
Xoid, genXoid,
Xgid, genXgid,
) where
import Domain
import qualified Data.UUID as UUID(toString)
import System.Random (randomIO)
-- external commands
data XCommand
= OfferGame Offer
| AcceptOffer Player Xoid
| MakeMove Player Xgid Move
-- | ClaimWin Xgid -- TODO
deriving Show
-- external transitions
data XTrans
= NewOffer { xoid :: Xoid, offer :: Offer }
| OfferWithdrawn { xoid :: Xoid, offer :: Offer } -- because someone accepted
| NewGame { xgid :: Xgid, game :: Game } -- replaces an offer
| GameMove { oldXgid :: Xgid, newXgid :: Xgid, game :: Game }
deriving Show
-- order id & game id
data Xoid = Xoid String
deriving (Eq,Ord)
instance Show Xoid where show (Xoid s) = s
data Xgid = Xgid String
deriving (Eq,Ord)
instance Show Xgid where show (Xgid s) = s
genXgid :: IO Xgid
genXgid = do fmap (Xgid . UUID.toString) randomIO
genXoid :: IO Xoid
genXoid = do fmap (Xoid . UUID.toString) randomIO

View File

@ -0,0 +1,188 @@
-- Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
module Local(State(..), Onum(..), Gnum(..), initState, applyTransPureSimple, applyTrans,
LCommand(..), externCommand,lookForAnAction,
OpenState, getOpenState,
LTrans,
) where
import Prelude hiding(id)
import Control.Monad(when)
import qualified Data.List as List(find,concatMap)
import Data.List ((\\))
import Data.Maybe(mapMaybe,listToMaybe)
import qualified Data.Map.Strict as Map(toList,lookup,empty,adjust,insert,elems,keys)
import Data.Map.Strict (Map)
import Domain
import External
----------------------------------------------------------------------
-- local command, map to X in context of state
data LCommand
= OfferNewGameToAnyone
| OfferGameL Player
| AcceptOfferL Onum
| MakeMoveL Gnum Move
deriving Show
externCommand :: Player -> State -> LCommand -> Maybe XCommand
externCommand who state@State{knownPlayers} = \case
OfferNewGameToAnyone -> do
return $ OfferGame (Offer {from = who, to = knownPlayers \\ [who] })
OfferGameL player -> do
return $ OfferGame (Offer {from = who, to = [player] }) --can play self!
AcceptOfferL lid -> do
xid <- externOid state lid
return $ AcceptOffer who xid
MakeMoveL lid move -> do
xid <- externGid state lid
return $ MakeMove who xid move
externOid :: State -> Onum -> Maybe Xoid
externOid State{offers} low = (fmap fst . List.find (\(_,(l,_,status)) -> l==low && status == Open) . Map.toList) offers
externGid :: State -> Gnum -> Maybe Xgid
externGid State{games} low = (fmap fst . List.find (\(_,(l,_,status)) -> l==low && status == Open) . Map.toList) games
----------------------------------------------------------------------
-- local trans, for reporting what happing in terms of local oid/gid
data LTrans
= NewOfferL Onum Offer
| OfferNowUnavailable Onum Offer
| NewGameL Gnum Game
| GameMoveL Gnum Game
deriving Show
-- TODO: return list, then caller can make random choice!
lookForAnAction :: State -> Maybe LCommand
lookForAnAction State{whoami,offers,games} =
listToMaybe $
-- prefer to play a game move..
mapMaybe
(\(onum,offer,status) ->
if whoami `elem` to offer && status == Open
then Just $ AcceptOfferL onum
else Nothing
) (Map.elems offers)
++
-- otherwise accept any pending offer
List.concatMap
-- randomize move order here !
(\(gnum,game,status) ->
if whoami == p1 game && status == Open
then map (MakeMoveL gnum) (legalMovesOfGame game)
else []
) (Map.elems games)
----------------------------------------------------------------------
-- local state, accumulates external transitions
data State = State {
whoami :: Player,
knownPlayers :: [Player],
offers :: Map Xoid (Onum,Offer,Status),
games :: Map Xgid (Gnum,Game,Status),
-- TODO: share the next number thing for offers and games
-- when an offer is converted to a game, keep the number
-- this mean a X-NewGame wil need to refernce the offer is comes from
nextOfferId :: Onum,
nextGameId :: Gnum
}
deriving (Show)
data Status = Open | Closed
deriving (Eq,Show)
initState :: Player -> [Player] -> State
initState whoami knownPlayers = State {
whoami,
knownPlayers, -- TODO: = [] when support Hello
offers = Map.empty,
games = Map.empty,
nextOfferId = Onum 1,
nextGameId = Gnum 1
}
data Onum = Onum Int deriving (Show,Eq) -- local offer id
data Gnum = Gnum Int deriving (Show,Eq) -- local game id
incOfferId :: Onum -> Onum
incOfferId (Onum n) = Onum (n+1)
incGameId :: Gnum -> Gnum
incGameId (Gnum n) = Gnum (n+1)
applyTransPureSimple :: State -> XTrans -> State
applyTransPureSimple s xt = either error snd (applyTrans s xt)
applyTrans :: State -> XTrans -> Either String ([LTrans], State)
applyTrans state0 = \case
NewOffer{xoid,offer} -> do
let State{offers,nextOfferId=onum} = state0
when (xoid `elem` Map.keys offers) $ fail "new offer, dup id"
return (
[NewOfferL onum offer],
state0 { offers = Map.insert xoid (onum,offer,Open) offers,
nextOfferId = incOfferId onum })
OfferWithdrawn{xoid,offer} -> do
let State{offers} = state0
case Map.lookup xoid offers of
Nothing -> fail "offer withdrawm, unknown id"
Just (onum,_,_) ->
return (
[OfferNowUnavailable onum offer],
state0 { offers = Map.adjust archive xoid offers })
NewGame {xgid,game} -> do
let State{games,nextGameId=gnum} = state0
when (xgid `elem` Map.keys games) $ fail "new game, dup id"
return (
[NewGameL gnum game],
state0 { games = Map.insert xgid (gnum,game,Open) games,
nextGameId = incGameId gnum })
GameMove {oldXgid,newXgid,game} -> do
let State{games} = state0
when (newXgid `elem` Map.keys games) $ fail "game move, dup new id"
case Map.lookup oldXgid games of
Nothing -> fail "game move, unknown old id"
Just (gnum,_,_) -> return (
[GameMoveL gnum game],
state0 { games = Map.insert newXgid (gnum,game,Open) (Map.adjust archive oldXgid games) }
)
where archive (k,v,_) = (k,v,Closed)
----------------------------------------------------------------------
-- Visualize local open state
data OpenState = OpenState {
me :: Player,
oOffers :: [(Onum,Offer)],
oGames :: [(Gnum,Game)]
}
instance Show OpenState where
show OpenState{me,oOffers,oGames} = unlines (
[show me] ++
["- offers:"] ++
map (\(id,offer) -> "- " <> show id <> " = " <> show offer) oOffers ++
["- games:"] ++
map (\(id,game) -> "- " <> show id <> " = " <> show game) oGames
)
getOpenState :: State -> OpenState
getOpenState State{whoami=me,offers,games} = OpenState{me,oOffers,oGames}
-- TODO: sort the games and offers, by index number
-- geerally just make this prettier
-- also, maybe have a summary version, which can show when switch whoami
where
oOffers = (map (\(id,offer,_) -> (id,offer)) . filter (\(_,_,status) -> status == Open) . Map.elems) offers
oGames = (map (\(id,game,_) -> (id,game)) . filter (\(_,_,status) -> status == Open) . Map.elems) games

View File

@ -0,0 +1,27 @@
-- Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
module Logging (Logger,noLog,tagLog,colourLog,plainLog,colourWrap) where
import System.Console.ANSI(
setSGRCode, Color(..), SGR(SetColor), ConsoleLayer(Foreground), ColorIntensity(Vivid),)
type Logger = String -> IO ()
noLog :: Logger
noLog _ = return ()
plainLog :: Logger
plainLog = putStrLn
colourWrap :: Color -> String -> String
colourWrap col s =
setSGRCode [SetColor Foreground Vivid col] <> s <>
setSGRCode [SetColor Foreground Vivid White]
colourLog :: Color -> Logger -> Logger
colourLog col log s = log (colourWrap col s)
tagLog :: String -> Logger -> Logger
tagLog tag log s = log (tag <> s)

View File

@ -0,0 +1,7 @@
-- Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
module Main(main) where
import qualified UI(main)
main :: IO ()
main = UI.main

View File

@ -0,0 +1,8 @@
-- Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
module PastAndFuture(PastAndFuture(..)) where
import Stream
data PastAndFuture a = PF { past :: [a], future :: Stream a }

View File

@ -0,0 +1,137 @@
-- Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
-- Simulate a ledger
-- Accepting commands, and feeding back the resultant transitions
module SimLedger(Handle, connect, sendCommand, getTrans) where
import Control.Concurrent
import Control.Monad(when,filterM)
import Data.List as List(filter)
import qualified Data.Map.Strict as Map(lookup,empty,adjust,insert)
import Data.Map.Strict (Map)
import System.Time.Extra(sleep)
import Domain
import External
import Stream
import PastAndFuture
data Client = Client { player :: Player, stream :: Stream XTrans }
newClient :: Player -> IO Client
newClient player = do
stream <- newStream
return Client{player,stream}
sendToClient :: XTrans -> Client -> IO ()
sendToClient xt Client{player,stream} = do
when (canSeeTrans player xt) $ writeStream stream (Right xt)
isClientClosed :: Client -> IO Bool
isClientClosed Client{stream} = do
Stream.isClosed stream >>= \case
Just _ -> return True
Nothing -> return False
sendListToClients :: [XTrans] -> [Client] -> IO ()
sendListToClients xts clients =
mapM_ (\xt -> mapM_ (sendToClient xt) clients) xts
after :: Double -> IO () -> IO ()
after n action = do
_ <- forkIO (do sleep n; action)
return ()
data Handle = LH {
state :: MVar Ledger,
history :: MVar [XTrans],
watching :: MVar [Client]
}
connect :: IO Handle
connect = do
state <- newMVar emptyLedger
history <- newMVar []
watching <- newMVar []
return LH {state,history,watching}
sendCommand :: Handle -> XCommand -> IO (Maybe Rejection)
sendCommand LH{state,watching,history} com = do
ledger <- takeMVar state
(ledger',rejOpt,xts) <- recordOnLedger ledger com
putMVar state ledger'
modifyMVar_ history (return . (reverse xts ++))
clients <- takeMVar watching
-- TODO: would be better to setup onClose handler
clients' <- filterM (fmap not . isClientClosed) clients
putMVar watching clients'
-- delay to simulate the lag of a real ledger
after 1.5 (sendListToClients xts clients')
return rejOpt
getTrans :: Player -> Handle -> IO (PastAndFuture XTrans)
getTrans player LH{watching,history} = do
client@Client{stream=future} <- newClient player
modifyMVar_ watching (return . (client:))
past <- fmap (reverse . List.filter (canSeeTrans player)) $ readMVar history
return PF{past, future}
canSeeTrans :: Player -> XTrans -> Bool
canSeeTrans player xt = player `elem` playersOfTrans xt
playersOfTrans :: XTrans -> [Player]
playersOfTrans = \case
NewOffer {offer} -> playersOfOffer offer
OfferWithdrawn{offer} -> playersOfOffer offer
NewGame {game} -> playersOfGame game
GameMove {game} -> playersOfGame game
data Ledger = Ledger (Map Xoid (Offer,Bool)) (Map Xgid (Game,Bool))
emptyLedger :: Ledger
emptyLedger = Ledger Map.empty Map.empty
type Rejection = String
-- TODO: Better return type -- IO (Either Rejection (Ledger,[XTrans]))
-- which makes clear than on rejection we chage nothing & generate no xtrans
recordOnLedger :: Ledger -> XCommand -> IO (Ledger,Maybe Rejection,[XTrans])
recordOnLedger ledger@(Ledger os gs) = \case
OfferGame offer -> do
oid <- genXoid
let os' = Map.insert oid (offer,True) os
accept (Ledger os' gs) [NewOffer oid offer]
AcceptOffer acceptor oid ->
case Map.lookup oid os of
Nothing -> reject "no such oid"
Just (_,False) -> reject "double accept"
Just (offer,True) -> do
let os' = Map.adjust archive oid os
if acceptor `notElem` to offer then reject "not in offer to-list" else do
xgid <- genXgid
let game = initGame acceptor (from offer)
let gs' = Map.insert xgid (game,True) gs
accept (Ledger os' gs') [OfferWithdrawn oid offer, NewGame {xgid, game}]
MakeMove player gid move ->
case Map.lookup gid gs of
Nothing -> reject "no such gid"
Just (_,False) -> reject "double play"
Just (game,True) -> do
if player /= p1 game then reject "not player1" else do
gid' <- genXgid
case playMove move game of
Left reason -> reject reason
Right game' -> do
let gs' = Map.insert gid' (game',True) (Map.adjust archive gid gs)
accept (Ledger os gs') [GameMove gid gid' game']
where
accept l ts = return (l,Nothing,ts)
reject reason = return (ledger,Just reason,[])
archive (x,_) = (x,False)

View File

@ -0,0 +1,92 @@
-- Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
-- TODO: better name than "clients" for the onClose callbacks?
-- Streams which are closable at both ends.
-- When closed (at the read-end), elements in flight are dropped, clients are notified
-- When closure is requested at the write-end, elements in flight are processed. The stream becomes properly closed when the closure request reaches the read-end of the stream. Subsequent writes are dropped.
module Stream(Stream, newStream,
Closed(..), onClose, closeStream,
takeStream,
writeStream,
whenClosed, isClosed,
) where
import Control.Concurrent
newtype Stream a = Stream {status :: MVar (Either Closed (Open a))}
newtype Closed = Closed { reason :: String }
data Open a = Open {
chan :: Chan (Either Closed a),
clients :: MVar [Closed -> IO ()] -- notified on close
}
-- Create a new open stream.
newStream :: IO (Stream a)
newStream = do
chan <- newChan
clients <- newMVar []
status <- newMVar (Right (Open {chan,clients}))
return Stream{status}
-- Set a callback for when a stream becomes closed.
-- Called immediately if the stream is already closed.
onClose :: Stream a -> (Closed -> IO ()) -> IO ()
onClose Stream{status} f = do
readMVar status >>= \case
Left closed -> f closed -- already closed, call f now
Right Open{clients} -> do
modifyMVar_ clients (return . (f:))
-- Close stream now! Elements in flight are lost. Clients are notified.
closeStream :: Stream a -> Closed -> IO ()
closeStream Stream{status} closed = do
takeMVar status >>= \case
Left alreadyClosed -> putMVar status (Left alreadyClosed) -- do nothing
Right Open{clients} -> do
putMVar status (Left closed) -- now can't get new clients
fs <- readMVar clients
mapM_ (\f -> f closed) fs
-- Get the next element from a stream, or find the stream is closed.
-- (Blocking until one of the above occurs)
takeStream :: Stream a -> IO (Either Closed a)
takeStream Stream{status} = do
readMVar status >>= \case
Left closed -> return (Left closed)
Right Open{chan} -> do
readChan chan >>= \case
Right a -> return (Right a)
Left closed -> do
-- closed the stream, as requested by writer
modifyMVar_ status (\_ -> return (Left closed))
-- forward the closure request in case of multiple blocked readers
writeChan chan (Left closed)
return (Left closed)
-- Write an element onto the stream,
-- Or write a closure-request which happens after elems in flight are processed.
writeStream :: Stream a -> Either Closed a -> IO ()
writeStream Stream{status} item = do
readMVar status >>= \case
Left _closed -> return () -- already closed, item dropped on floor
Right Open{chan} -> writeChan chan item
-- Block until the stream is closed, which might be already
whenClosed :: Stream a -> IO Closed
whenClosed stream = do
signal <- newEmptyMVar
onClose stream (putMVar signal)
takeMVar signal
-- Is the stream closed right now?
isClosed :: Stream a -> IO (Maybe Closed)
isClosed Stream{status} =
readMVar status >>= \case
Left closed -> return (Just closed)
Right _ -> return Nothing

View File

@ -0,0 +1,240 @@
-- Copyright (c) 2019 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
module UI(main) where
import Control.Concurrent
import Control.Monad.Trans.Class (lift)
import Data.Foldable(forM_)
import Prelude hiding (id)
import System.Console.ANSI(Color(..))
import qualified System.Console.Haskeline as HL (InputT,runInputT,getInputLine,defaultSettings,getExternalPrint)
import Text.Read (readMaybe)
import System.Time.Extra(sleep)
import Domain
import Stream
import PastAndFuture
import External
import Local(State,LCommand,Onum(..),Gnum(..))
import qualified Local
import qualified SimLedger as Ledger
import SimLedger(Handle)
import Logging
----------------------------------------------------------------------
-- players, colours
alice,bob,charles :: Player
alice = Player "alice"
bob = Player "bob"
charles = Player "charles"
message :: String -> IO ()
message = colourLog Cyan plainLog
----------------------------------------------------------------------
-- PlayerState
data PlayerState = PlayerState {
player :: Player,
sv :: MVar State,
stream :: Stream XTrans
}
makePlayerState :: Handle -> Logger -> Player -> IO PlayerState
makePlayerState h xlog player = do
-- TODO: handle knownPlayers by "Hello" contracts
let knownPlayers = [alice,bob,charles] -- john *not* a known player
let s = Local.initState player knownPlayers
sv <- newMVar s
stream <- manageUpdates h player (playerLog player xlog) sv
return PlayerState{player,sv,stream}
playerLog :: Player -> Logger -> Logger
playerLog player log =
tagLog ("(" <> show player <> ") ") $
--colourLog (colourForPlayer player) log
colourLog Blue log
----------------------------------------------------------------------
-- main
main :: IO ()
main = HL.runInputT HL.defaultSettings $ do
h <- lift Ledger.connect
xlog <- HL.getExternalPrint
let player = alice -- initial interactive player
lift $ runBotFor h bob
lift $ runBotFor h charles -- 2nd bot
ps <- lift $ makePlayerState h xlog player
readLoop h xlog ps
----------------------------------------------------------------------
-- readLoop
promptPlayer :: Player -> String
promptPlayer player =
--colourWrap (colourForPlayer player) (show player <> "> ")
colourWrap Green (show player <> "> ")
readLoop :: Handle -> Logger -> PlayerState -> HL.InputT IO ()
readLoop h xlog ps = do
let PlayerState{player} = ps
lineOpt <- HL.getInputLine (promptPlayer player)
case lineOpt of
Nothing -> return ()
Just line -> do
ps' <- lift $ processLine h xlog ps line
readLoop h xlog ps'
processLine :: Handle -> Logger -> PlayerState -> String -> IO PlayerState
processLine h xlog ps line = do
case parseWords (words line) of
Nothing -> do
message $ "failed to parse: " <> line
return ps
Just res -> runParsed h xlog ps res
----------------------------------------------------------------------
-- parse console input line
data LQuery
= ShowFullState --debug
| ShowOpenState
-- TODO: individual game state
data Parsed
= Submit LCommand
| Query LQuery
| Become Player
parseWords :: [String] -> Maybe Parsed
parseWords = \case
["become",p] -> do
return $ Become (Player p)
["show"] ->
return $ Query ShowOpenState
["show","full"] ->
return $ Query ShowFullState
["offer"] ->
return $ Submit Local.OfferNewGameToAnyone
["offer",p] -> do
return $ Submit $ Local.OfferGameL (Player p)
["accept",o] -> do
oid <- parseOnum o
return $ Submit $ Local.AcceptOfferL oid
["move",g,p,n] -> do
gid <- parseGnum g
pileNum <- readMaybe p
howMany <- readMaybe n
return $ Submit $ Local.MakeMoveL gid (Move {pileNum,howMany})
_ ->
Nothing
parseOnum :: String -> Maybe Onum
parseOnum s = fmap Onum (readMaybe s)
parseGnum :: String -> Maybe Gnum
parseGnum s = fmap Gnum (readMaybe s)
----------------------------------------------------------------------
-- run thhe parsed command
runParsed :: Handle -> Logger -> PlayerState -> Parsed -> IO PlayerState
runParsed h xlog ps = \case
Submit lc -> do
runSubmit h message ps lc
return ps
Query lq -> do
let PlayerState{sv} = ps
s <- readMVar sv
runLocalQuery s lq
return ps
Become player' -> do
message $ "becoming: " <> show player'
let PlayerState{stream} = ps
closeStream stream (Closed "changing player")
makePlayerState h xlog player'
runLocalQuery :: State -> LQuery -> IO ()
runLocalQuery s = \case
ShowFullState -> message (show s)
ShowOpenState -> message (show (Local.getOpenState s))
----------------------------------------------------------------------
-- runSubmit (used by runParsed and robot)
runSubmit :: Handle -> Logger -> PlayerState -> LCommand -> IO ()
runSubmit h log ps lc = do
--log $ "lc: " <> show lc
let PlayerState{player,sv} = ps
s <- readMVar sv
case Local.externCommand player s lc of
Nothing -> do
log $ "bad local command: " <> show lc
return ()
Just xc -> do
Ledger.sendCommand h xc >>= \case
Nothing -> return ()
Just rej -> do
log $ "command rejected by ledger: " <> rej
----------------------------------------------------------------------
-- Manage updates in response to XTrans from the ledger
manageUpdates :: Handle -> Player -> Logger -> MVar State -> IO (Stream XTrans)
manageUpdates h player log sv = do
PF{past,future} <- Ledger.getTrans player h
modifyMVar_ sv (\s -> return $ foldl Local.applyTransPureSimple s past)
_ <- forkIO (updateX log sv future)
return future
updateX :: Logger -> MVar State -> Stream XTrans -> IO ()
updateX log sv stream = loop
where
loop = takeStream stream >>= \case
Left Closed{} -> do
log "transaction stream is closed"
return () -- forked thread will terminate
Right xt ->
do applyX log sv xt; loop
applyX :: Logger -> MVar State -> XTrans -> IO ()
applyX log sv xt = do
s <- takeMVar sv
--log $ "xt: " <> show xt
(lts,s') <- either fail return (Local.applyTrans s xt)
mapM_ (\lt -> log $ "lt: " <> show lt) lts -- TODO: improve message for "local trans"
putMVar sv s'
----------------------------------------------------------------------
-- robot
runBotFor :: Handle -> Player -> IO ()
runBotFor h player = do
ps <- makePlayerState h noLog player
_tid <- forkIO (robot h noLog ps)
return ()
robot :: Handle -> Logger -> PlayerState -> IO ()
robot h log ps = loop
where
loop = do
sleep 2
log "thinking..."
let PlayerState{sv} = ps
s <- readMVar sv
forM_ (Local.lookForAnAction s) (runSubmit h noLog ps) -- quiet!
loop

View File

@ -6,123 +6,148 @@
{-# LANGUAGE OverloadedStrings #-}
module DA.Ledger( -- WIP: High level interface to the Ledger API services
module DA.Ledger.Types,
Port(..),
LedgerHandle,
Party(..),
LL_Transaction, -- TODO: remove
ResponseStream,
connect,
identity,
getTransactionStream,
nextResponse,
-- services
listPackages,
getPackage, Package,
transactions,
completions,
submitCommands,
getCompletionStream,
module DA.Ledger.Types,
LL_Transaction, -- TODO: remove when coded `raise' (LL->HL) operation on Transaction types
Port(..),
Stream, takeStream, getStreamContents,
LedgerHandle, connect, identity,
) where
import Control.Concurrent
import Control.Monad.Fix (fix)
import qualified Data.Map as Map
import qualified Data.Text.Lazy as Text
import Data.Vector as Vector (fromList, toList)
import Prelude hiding (log)
import DA.Ledger.Types
import Control.Concurrent
import Control.Exception (Exception,SomeException,catch,throwIO)
import Control.Monad.Fix (fix)
import qualified Data.Map as Map(empty,singleton)
import qualified Data.Text.Lazy as Text(unpack)
import qualified Data.Vector as Vector(toList,fromList)
import DA.Ledger.Convert(lowerCommands)
import DA.Ledger.LowLevel as LL hiding(Commands)
import qualified DA.Ledger.LowLevel as LL
import DA.Ledger.LowLevel(
ClientRequest(
ClientReaderRequest,
ClientNormalRequest
),
ClientResult(
ClientErrorResponse,
ClientReaderResponse,
ClientNormalResponse
),
MetadataMap(..),
GRPCMethodType(Normal,ServerStreaming),
ClientConfig(..),
Port(..),
Host(..),
LedgerIdentityService(..),
GetLedgerIdentityRequest(..),
GetLedgerIdentityResponse(..),
TransactionService(..),
GetTransactionsRequest(..),
GetTransactionsResponse(..),
CommandSubmissionService(..),
SubmitRequest(..),
Empty(..),
CommandCompletionService(..),
CompletionStreamRequest(..),
CompletionStreamResponse(completionStreamResponseCompletions),
TransactionFilter(..),
Filters(..),
LedgerOffset(..),
LedgerOffsetValue(..),
LedgerOffset_LedgerBoundary(..),
TraceContext,
)
import qualified Proto3.Suite(fromByteString)
import qualified DA.Daml.LF.Ast as LF(Package)
import qualified DA.Daml.LF.Proto3.Decode as Decode(decodePayload)
data LedgerHandle = LedgerHandle { port :: Port, lid :: LedgerId }
identity :: LedgerHandle -> LedgerId
identity LedgerHandle{lid} = lid
newtype ResponseStream a = ResponseStream { chan :: Chan a }
nextResponse :: ResponseStream a -> IO a
nextResponse ResponseStream{chan} = readChan chan
connect :: Port -> IO LedgerHandle
connect port = do
connect port = wrapE "connect" $ do
lid <- getLedgerIdentity port
return $ LedgerHandle {port, lid}
getLedgerIdentity :: Port -> IO LedgerId
getLedgerIdentity port = do
getLedgerIdentity port = wrapE "getLedgerIdentity" $ do
let request = GetLedgerIdentityRequest noTrace
LL.withGRPCClient (config port) $ \client -> do
service <- LL.ledgerIdentityServiceClient client
let LedgerIdentityService rpc = service
response <- rpc (wrap (GetLedgerIdentityRequest noTrace))
response <- rpc (ClientNormalRequest request timeout mdm)
GetLedgerIdentityResponse text <- unwrap response
return $ LedgerId text
listPackages :: LedgerHandle -> IO [PackageId]
listPackages LedgerHandle{port,lid} = wrapE "listPackages" $ do
LL.withGRPCClient (config port) $ \client -> do
service <- LL.packageServiceClient client
let PackageService rpc1 _ _ = service
let request = ListPackagesRequest (unLedgerId lid) noTrace
response <- rpc1 (ClientNormalRequest request timeout mdm)
ListPackagesResponse xs <- unwrap response
return $ map PackageId $ Vector.toList xs
data Package = Package LF.Package deriving Show
getPackage :: LedgerHandle -> PackageId -> IO Package
getPackage LedgerHandle{port,lid} pid = wrapE "getPackage" $ do
let request = GetPackageRequest (unLedgerId lid) (unPackageId pid) noTrace
LL.withGRPCClient (config port) $ \client -> do
service <- LL.packageServiceClient client
let PackageService _ rpc2 _ = service
response <- rpc2 (ClientNormalRequest request timeout mdm)
GetPackageResponse _ bs _ <- unwrap response
let ap = either (error . show) id (Proto3.Suite.fromByteString bs)
case Decode.decodePayload ap of
Left e -> fail (show e)
Right package -> return (Package package)
submitCommands :: LedgerHandle -> Commands -> IO ()
submitCommands h commands = do
let request = wrap (SubmitRequest (Just (lowerCommands commands)) noTrace)
let LedgerHandle{port} = h
submitCommands LedgerHandle{port} commands = wrapE "submitCommands" $ do
let request = SubmitRequest (Just (lowerCommands commands)) noTrace
LL.withGRPCClient (config port) $ \client -> do
service <- LL.commandSubmissionServiceClient client
let CommandSubmissionService rpc = service
response <- rpc request
response <- rpc (ClientNormalRequest request timeout mdm)
Empty{} <- unwrap response
return ()
wrap :: r -> ClientRequest 'Normal r a
wrap r = ClientNormalRequest r timeout mdm
where timeout = 3
timeout :: Int -- Seconds
timeout = 30 -- TODO: sensible default? user configuarable?
unwrap :: ClientResult 'Normal a -> IO a
unwrap = \case
ClientNormalResponse x _m1 _m2 _status _details -> return x
ClientErrorResponse e -> fail (show e)
mdm :: MetadataMap
mdm = MetadataMap Map.empty
----------------------------------------------------------------------
-- Services with streaming responses
data Elem a = Elem a | Eend | Eerr String
deElem :: Elem a -> IO a
deElem = \case
Elem a -> return a
Eend -> fail "readStream, end"
Eerr s -> fail $ "readStream, err: " <> s
newtype Stream a = Stream { mv :: MVar (Elem a) }
newStream :: IO (Stream a)
newStream = do
mv <- newEmptyMVar
return Stream{mv}
writeStream :: Stream a -> Elem a -> IO () -- internal use only
writeStream Stream{mv} elem = putMVar mv elem
takeStream :: Stream a -> IO a
takeStream Stream{mv} = takeMVar mv >>= deElem
data StreamState = SS -- TODO
getStreamContents :: Stream a -> IO ([a],StreamState)
getStreamContents Stream{mv} = do xs <- loop ; return (xs,SS)
where
loop = do
tryTakeMVar mv >>= \case
Nothing -> return []
Just e -> do
x <- deElem e
xs <- loop
return (x:xs)
-- wrap LL.Transaction to show summary
newtype LL_Transaction = LL_Transaction { low :: LL.Transaction } --TODO: remove
deriving Eq
instance Show LL_Transaction where
show LL_Transaction{low} = _summary
@ -131,45 +156,50 @@ instance Show LL_Transaction where
_full = show low
LL.Transaction{transactionTransactionId} = low
-- TODO: return (HL) [Transaction]
getTransactionStream :: LedgerHandle -> Party -> IO (ResponseStream LL_Transaction)
getTransactionStream h party = do
let tag = "getTransactionStream for " <> show party
let LedgerHandle{port,lid} = h
chan <- newChan
transactions :: LedgerHandle -> Party -> IO (Stream LL_Transaction)
transactions LedgerHandle{port,lid} party = wrapE "transactions" $ do
stream <- newStream
let request = mkGetTransactionsRequest lid offsetBegin Nothing (filterEverthingForParty party)
forkIO_ tag $
_ <- forkIO $ --TODO: dont use forkIO
LL.withGRPCClient (config port) $ \client -> do
rpcs <- LL.transactionServiceClient client
let (TransactionService rpc1 _ _ _ _ _ _) = rpcs
sendToChan request f chan rpc1
return $ ResponseStream{chan}
sendToStream request f stream rpc1
return stream
where f = map LL_Transaction . Vector.toList . getTransactionsResponseTransactions
-- TODO: return (HL) [Completion]
getCompletionStream :: LedgerHandle -> ApplicationId -> [Party] -> IO (ResponseStream LL.Completion)
getCompletionStream h aid partys = do
let tag = "getCompletionStream for " <> show (aid,partys)
let LedgerHandle{port,lid} = h
chan <- newChan
completions :: LedgerHandle -> ApplicationId -> [Party] -> IO (Stream LL.Completion)
completions LedgerHandle{port,lid} aid partys = wrapE "completions" $ do
stream <- newStream
let request = mkCompletionStreamRequest lid aid partys
forkIO_ tag $
_ <- forkIO $ --TODO: dont use forkIO
LL.withGRPCClient (config port) $ \client -> do
rpcs <- LL.commandCompletionServiceClient client
let (CommandCompletionService rpc1 _) = rpcs
sendToChan request (Vector.toList . completionStreamResponseCompletions) chan rpc1
return $ ResponseStream{chan}
sendToStream request (Vector.toList . completionStreamResponseCompletions) stream rpc1
return stream
sendToChan :: a -> (b -> [c]) -> Chan c -> (ClientRequest 'ServerStreaming a b -> IO (ClientResult 'ServerStreaming b)) -> IO ()
sendToChan request f chan rpc1 = do
sendToStream :: a -> (b -> [c]) -> Stream c -> (ClientRequest 'ServerStreaming a b -> IO (ClientResult 'ServerStreaming b)) -> IO ()
sendToStream request f stream rpc1 = do
ClientReaderResponse _meta _code _details <- rpc1 $
ClientReaderRequest request timeout mdm $ \ _mdm recv -> fix $
ClientReaderRequest request timeout mdm $ \ _mdm recv -> fix $ -- TODO: whileM better?
\again -> do
either <- recv
case either of
Left e -> fail (show e)
Right Nothing -> return ()
Right (Just x) -> do writeList2Chan chan (f x); again
Left e -> do
writeStream stream (Eerr (show e)) -- notify reader of error
return ()
Right Nothing -> do
writeStream stream Eend -- notify reader of end-of-stream
return ()
Right (Just x) ->
do
mapM_ (writeStream stream . Elem) (f x)
again
return ()
-- After a minute, we stop collecting the events.
-- But we ought to wait indefinitely.
@ -183,9 +213,6 @@ config port =
, clientSSLConfig = Nothing
}
mdm :: MetadataMap
mdm = MetadataMap Map.empty
-- Low level data mapping for Request
@ -224,15 +251,8 @@ noTrace :: Maybe TraceContext
noTrace = Nothing
-- Misc / logging
data LedgerApiException = LedgerApiException { tag :: String, underlying :: SomeException } deriving Show
instance Exception LedgerApiException
forkIO_ :: String -> IO () -> IO ()
forkIO_ tag m = do
tid <- forkIO $ do m; log $ tag <> " is done"
log $ "forking " <> tag <> " on " <> show tid
return ()
log :: String -> IO ()
log s = do
tid <- myThreadId
putStrLn $ "[" <> show tid <> "]: " ++ s
wrapE :: String -> IO a -> IO a
wrapE tag io = io `catch` \e -> throwIO (LedgerApiException {tag,underlying=e})

View File

@ -4,6 +4,7 @@
module DA.Ledger.LowLevel(module X) where -- Low level GRPC and Generated Haskell code
import Network.GRPC.HighLevel.Generated as X
import Proto3.Suite.Types as X
import Google.Protobuf.Empty as X
@ -25,3 +26,5 @@ import Com.Digitalasset.Ledger.Api.V1.Transaction as X
import Com.Digitalasset.Ledger.Api.V1.TransactionFilter as X
import Com.Digitalasset.Ledger.Api.V1.TransactionService as X
import Com.Digitalasset.Ledger.Api.V1.Value as X
import Da.DamlLf as X hiding (HashFunction,HashFunctionSHA256)

View File

@ -73,6 +73,7 @@ data Command
createArgs :: Record,
choice :: Choice,
choiceArg :: Value }
deriving Show
-- completion.proto
@ -80,6 +81,7 @@ data Completion
= Completion {
cid :: CommandId,
status :: Status }
deriving Show
-- transaction.proto
@ -90,7 +92,7 @@ data WIP_Transaction
wid :: Maybe WorkflowId,
leTime :: Timestamp,
events :: [Event],
ofset :: AbsOffset }
ofset :: AbsOffset } deriving Show
-- event.proto
@ -118,7 +120,7 @@ data Event
acting :: [Party],
consuming :: Bool,
witness :: [Party],
childEids :: [EventId] }
childEids :: [EventId] } deriving Show
-- value.proto
@ -128,7 +130,7 @@ data Value
| VContract ContractId
| VList [Value]
| VInt Int
| VDecimal Text -- TODO: why not Int?
| VDecimal Text -- TODO: Maybe use Haskell Decimal type
| VString Text
| VTimestamp MicroSecondsSinceEpoch
| VParty Party
@ -137,63 +139,66 @@ data Value
| VDate DaysSinceEpoch
| VOpt (Maybe Value)
| VMap (Map Text Value)
deriving Show
data Record
= Record {
rid :: Maybe Identifier,
fields :: [RecordField] }
fields :: [RecordField] } deriving Show
data RecordField
= RecordField {
label :: Text,
value :: Value }
value :: Value } deriving Show
data Variant
= Variant {
vid :: VariantId,
constructor :: ConstructorId,
value :: Value }
value :: Value } deriving Show
data Identifier
= Identifier {
pid :: PackageId,
mod :: ModuleName,
ent :: EntityName }
ent :: EntityName } deriving Show
newtype MicroSecondsSinceEpoch = MicroSecondsSinceEpoch Int
newtype DaysSinceEpoch = DaysSinceEpoch Int
newtype MicroSecondsSinceEpoch = MicroSecondsSinceEpoch Int deriving Show-- TODO: Int64?
newtype DaysSinceEpoch = DaysSinceEpoch Int deriving Show
data Timestamp
= Timestamp {
seconds :: Integer,
nanos :: Integer }
seconds :: Integer, -- TODO: Int64?
nanos :: Integer } deriving Show
data Status -- TODO: from standard google proto, determining success/failure
data Status = Status-- TODO: from standard google proto, determining success/failure
deriving Show
newtype TemplateId = TemplateId Identifier
deriving Show
newtype LedgerId = LedgerId { unLedgerId :: Text } deriving Show
-- Text wrappers
newtype TransactionId = TransactionId { unTransactionId :: Text }
newtype EventId = EventId { unEventId :: Text }
newtype ContractId = ContractId { unContractId :: Text }
newtype WorkflowId = WorkflowId { unWorkflowId :: Text }
newtype TransactionId = TransactionId { unTransactionId :: Text } deriving Show
newtype EventId = EventId { unEventId :: Text } deriving Show
newtype ContractId = ContractId { unContractId :: Text } deriving Show
newtype WorkflowId = WorkflowId { unWorkflowId :: Text } deriving Show
newtype ApplicationId = ApplicationId { unApplicationId :: Text } deriving Show
newtype CommandId = CommandId { unCommandId :: Text }
newtype PackageId = PackageId { unPackageId :: Text }
newtype ConstructorId = ConstructorId { unConstructorId :: Text }
newtype VariantId = VariantId { unVariantId :: Text }
newtype CommandId = CommandId { unCommandId :: Text } deriving (Show,Eq)
newtype PackageId = PackageId { unPackageId :: Text } deriving Show
newtype ConstructorId = ConstructorId { unConstructorId :: Text } deriving Show
newtype VariantId = VariantId { unVariantId :: Text } deriving Show
newtype Choice = Choice { unChoice :: Text }
newtype Choice = Choice { unChoice :: Text } deriving Show
newtype Party = Party { unParty :: Text }
instance Show Party where show = Text.unpack . unParty
newtype ModuleName = ModuleName { unModuleName :: Text }
newtype EntityName = EntityName { unEntityName :: Text }
newtype ModuleName = ModuleName { unModuleName :: Text } deriving Show
newtype EntityName = EntityName { unEntityName :: Text } deriving Show
newtype AbsOffset = AbsOffset { unAbsOffset :: Text } -- TODO: why not an int?
newtype AbsOffset = AbsOffset { unAbsOffset :: Text } deriving Show -- TODO: why not an int?
-- TODO: .proto message types now yet handled
{-

View File

@ -9,13 +9,12 @@ module DA.Ledger.Sandbox ( -- Run a sandbox for testing on a dynamically selecte
withSandbox
) where
import Control.Monad (when)
import Control.Exception (Exception, bracket, evaluate, onException, throw)
import Control.Monad(when)
import Control.Exception (bracket, evaluate, onException)
import DA.Ledger (Port (..), unPort)
import Data.List (isInfixOf)
import Data.List.Split (splitOn)
import Data.List.Extra(splitOn)
import GHC.IO.Handle (Handle, hGetLine)
import Prelude hiding (log)
import System.IO (hFlush, stdout)
import System.Process (CreateProcess (..), ProcessHandle, StdStream (CreatePipe), createProcess, getPid, interruptProcessGroupOf, proc, waitForProcess)
import System.Time.Extra (Seconds, timeout)
@ -42,16 +41,16 @@ startSandboxProcess spec = do
create_group = True -- To avoid sending INT to ourself
}
pid <- getPid proh
log $ "Sandbox process started, pid = " <> show pid
trace $ "Sandbox process started, pid = " <> show pid
return (proh,hOutOpt)
shutdownSandboxProcess :: ProcessHandle -> IO ()
shutdownSandboxProcess proh = do
pidOpt <- getPid proh
log $ "Sending INT to sandbox process: " <> show pidOpt
trace $ "Sending INT to sandbox process: " <> show pidOpt
interruptProcessGroupOf proh
x <- timeoutError 10 "Sandbox process didn't exit" (waitForProcess proh)
log $ "Sandbox process exited with: " <> show x
x <- timeoutError 30 "Sandbox process didn't exit" (waitForProcess proh)
trace $ "Sandbox process exited with: " <> show x
return ()
parsePortFromListeningLine :: String -> IO Port
@ -70,7 +69,7 @@ getListeningLine :: Handle -> IO String
getListeningLine h = loop where
loop = do
line <- hGetLine h
when (interestingLineFromSandbox line) $ log $ "SANDBOX: " <> line
when (interestingLineFromSandbox line) $ trace $ "SANDBOX: " <> line
if "listening" `isInfixOf` line
then return line
else if "initialization error" `isInfixOf` line
@ -80,18 +79,18 @@ getListeningLine h = loop where
discoverListeningPort :: Maybe Handle -> IO Port
discoverListeningPort hOpt = do
Just h <- return hOpt
log "Looking for sandbox listening port..."
trace "Looking for sandbox listening port..."
line <- getListeningLine h
port <- parsePortFromListeningLine line
`onException` log ("Failed to parse listening port from: " <> show line)
log $ "Sandbox listening on port: " <> show (unPort port)
`onException` trace ("Failed to parse listening port from: " <> show line)
trace $ "Sandbox listening on port: " <> show (unPort port)
return port
startSandbox :: SandboxSpec-> IO Sandbox
startSandbox spec = do
(proh,hOpt) <-startSandboxProcess spec
port <-
timeoutError 10 "Didn't discover sandbox port" (discoverListeningPort hOpt)
timeoutError 30 "Didn't discover sandbox port" (discoverListeningPort hOpt)
`onException` shutdownSandboxProcess proh
return Sandbox { port, proh }
@ -100,21 +99,17 @@ shutdownSandbox Sandbox{proh} = do shutdownSandboxProcess proh
withSandbox :: SandboxSpec -> (Sandbox -> IO a) -> IO a
withSandbox spec f =
bracket (startSandbox spec)
bracket (startSandbox spec) -- TODO: too long to backet over? (masks C-c ?)
shutdownSandbox
f
data Timeout = Timeout String deriving Show
instance Exception Timeout
timeoutError :: Seconds -> String -> IO a -> IO a
timeoutError n tag io =
timeout n io >>= \case
Just x -> return x
Nothing -> do
log $ "Timeout: " <> tag <> ", after " <> show n <> " seconds."
throw (Timeout tag)
fail $ "Timeout: " <> tag <> ", after " <> show n <> " seconds."
_log,log :: String -> IO ()
_log s = do putStr ("\n["<>s<>"]"); hFlush stdout -- debugging
log _ = return ()
_trace,trace :: String -> IO ()
_trace s = do putStr ("\n["<>s<>"]"); hFlush stdout -- debugging
trace _ = return ()

View File

@ -5,14 +5,30 @@
module DA.Ledger.Tests (main) where
import Control.Monad(unless)
import Control.Exception (SomeException, try)
import qualified DA.Ledger as Ledger
import Data.List (isPrefixOf)
import qualified Data.Text.Lazy as Text (unpack)
import Prelude hiding (log)
import DA.Ledger as Ledger
import Data.List (isPrefixOf,isInfixOf)
import qualified Data.Text.Lazy as Text (pack,unpack)
import DA.Ledger.Sandbox as Sandbox(SandboxSpec (..), port, shutdownSandbox, withSandbox)
import Test.Tasty as Tasty (TestTree, defaultMain, testGroup)
import Test.Tasty.HUnit as Tasty (assertBool, assertFailure, testCase)
import Test.Tasty.HUnit as Tasty (assertEqual, assertBool, assertFailure, testCase)
import Data.Text.Lazy(Text)
import qualified Data.UUID as UUID
import System.Random(randomIO)
import qualified DA.Ledger.LowLevel as LL(Completion(..))
expectException :: IO a -> IO SomeException
expectException io =
try io >>= \case
Right _ -> assertFailure "exception was expected"
Left (e::SomeException) -> return e
assertExceptionTextContains :: SomeException -> String -> IO ()
assertExceptionTextContains e frag =
unless (frag `isInfixOf` show e) (assertFailure msg)
where msg = "expected frag: " ++ frag ++ "\n contained in: " ++ show e
main :: IO ()
main = Tasty.defaultMain tests
@ -22,7 +38,13 @@ spec1 = SandboxSpec {dar}
where dar = "language-support/hs/bindings/quickstart.dar"
tests :: TestTree
tests = testGroup "Haskell Ledger Bindings" [t1,t2]
tests = testGroup "Haskell Ledger Bindings" [
t1, t2, t3,
t4, t4_1,
t5, t6
-- we really need sandboxes shared between tests..
--,t1,t1,t1,t1,t1,t1
]
t1 :: Tasty.TestTree
t1 = testCase "connect, ledgerid" $ do
@ -31,14 +53,98 @@ t1 = testCase "connect, ledgerid" $ do
let lid = Ledger.identity h
let got = Text.unpack $ Ledger.unLedgerId lid
assertBool "bad ledgerId" (looksLikeSandBoxLedgerId got)
looksLikeSandBoxLedgerId :: String -> Bool
looksLikeSandBoxLedgerId s = "sandbox-" `isPrefixOf` s && length s == 44
where looksLikeSandBoxLedgerId s =
"sandbox-" `isPrefixOf` s && length s == 44
t2 :: Tasty.TestTree
t2 = testCase "connect, sandbox dead -> exception" $ do
withSandbox spec1 $ \sandbox -> do
shutdownSandbox sandbox -- kill it here
try (Ledger.connect (Sandbox.port sandbox)) >>= \case
Left (_::SomeException) -> return ()
Right _ -> assertFailure "an Exception was expected"
e <- expectException (Ledger.connect (Sandbox.port sandbox))
assertExceptionTextContains e "ClientIOError"
t3 :: Tasty.TestTree
t3 = testCase "no transcations to start with" $ do
withSandbox spec1 $ \sandbox -> do
h <- Ledger.connect (Sandbox.port sandbox)
stream <- Ledger.transactions h alice
(ts,_) <- Ledger.getStreamContents stream
assertEqual "#transactions" 0 (length ts)
t4 :: Tasty.TestTree
t4 = testCase "submit bad package id" $ do
withSandbox spec1 $ \sandbox -> do
h <- Ledger.connect (Sandbox.port sandbox)
e <- expectException (submitCommand h alice command)
assertExceptionTextContains e "Couldn't find package"
where command = createIOU pid alice "A-coin" 100
pid = PackageId "xxxxxxxxxxxxxxxxxxxxxx"
t4_1 :: Tasty.TestTree
t4_1 = testCase "submit good package id" $ do
withSandbox spec1 $ \sandbox -> do
h <- Ledger.connect (Sandbox.port sandbox)
-- TODO: Use Ledger.getPackage to find the correct package with the "Iou" contract.
[pid,_,_] <- Ledger.listPackages h -- for now assume it's in the 1st of the 3 listed packages.
let command = createIOU pid alice "A-coin" 100
completions <- Ledger.completions h myAid [alice]
(cs1,_) <- Ledger.getStreamContents completions
assertEqual "before submit 1" [] cs1
cid1 <- submitCommand h alice command
comp1 <- takeStream completions
let LL.Completion{completionCommandId} = comp1
let cid1' = CommandId completionCommandId
assertEqual "submit1" cid1' cid1
t5 :: Tasty.TestTree
t5 = testCase "package service, listPackages" $ do
withSandbox spec1 $ \sandbox -> do
h <- Ledger.connect (Sandbox.port sandbox)
ids <- Ledger.listPackages h
assertEqual "#packages" 3 (length ids)
t6 :: Tasty.TestTree -- WIP (Ledger.getPackage not working yet)
t6 = testCase "package service, get Package" $ do
withSandbox spec1 $ \sandbox -> do
h <- Ledger.connect (Sandbox.port sandbox)
ids <- Ledger.listPackages h
ps <- mapM (Ledger.getPackage h) ids
assertEqual "#packages" 3 (length ps)
return ()
alice :: Ledger.Party
alice = Ledger.Party "Alice"
createIOU :: PackageId -> Party -> Text -> Int -> Command
createIOU quickstart party currency quantity = CreateCommand {tid,args}
where
tid = TemplateId (Identifier quickstart mod ent)
-- TODO: use package-service to find package-id
-- da run damlc inspect-dar target/quickstart.dar
mod = ModuleName "Iou"
ent = EntityName "Iou"
args = Record Nothing [
RecordField "issuer" (VParty party),
RecordField "owner" (VParty party),
RecordField "currency" (VString currency),
RecordField "amount" (VDecimal $ Text.pack $ show quantity),
RecordField "observers" (VList [])
]
submitCommand :: LedgerHandle -> Party -> Command -> IO CommandId
submitCommand h party com = do
let lid = Ledger.identity h
cid <- randomCid
Ledger.submitCommands h
(Commands {lid,wid,aid=myAid,cid,party,leTime,mrTime,coms=[com]})
return cid
where
wid = Nothing
leTime = Timestamp 0 0
mrTime = Timestamp 5 0
myAid :: ApplicationId
myAid = ApplicationId "<my-application>"
randomCid :: IO CommandId
randomCid = do fmap (CommandId . Text.pack . UUID.toString) randomIO