haskoin-wallet-0.2.0: Network/Haskoin/Wallet/Client/Commands.hs
module Network.Haskoin.Wallet.Client.Commands
( cmdStart
, cmdStop
, cmdNewKeyRing
, cmdKeyRing
, cmdKeyRings
, cmdNewAcc
, cmdNewMS
, cmdNewRead
, cmdAddKeys
, cmdSetGap
, cmdAccount
, cmdAccounts
, cmdList
, cmdUnused
, cmdLabel
, cmdTxs
, cmdAddrTxs
, cmdGenAddrs
, cmdSend
, cmdSendMany
, cmdImport
, cmdSign
, cmdBalance
, cmdGetTx
, cmdGetOffline
, cmdSignOffline
, cmdRescan
, cmdDecodeTx
, cmdVersion
, cmdStatus
)
where
import System.ZMQ4.Monadic
( Req(..)
, runZMQ
, socket
, send
, receive
, connect
)
import Control.Monad (forM_, when, liftM2)
import Control.Monad.Trans (liftIO)
import qualified Control.Monad.Reader as R (ReaderT, ask, asks)
import Data.Maybe
(listToMaybe, isNothing, fromJust, fromMaybe, isJust, maybeToList)
import Data.List (intercalate, intersperse)
import Data.Text (Text, pack, unpack, splitOn)
import Data.Word (Word64)
import qualified Data.Yaml as YAML (encode)
import qualified Data.Aeson.Encode.Pretty as JSON
( Config(..)
, encodePretty'
, defConfig
)
import Data.Aeson
( Value(..)
, FromJSON
, ToJSON
, toJSON
, object
, encode
, decode
, eitherDecode
, (.=)
)
import Data.String.Conversions (cs)
import Network.Haskoin.Block
import Network.Haskoin.Crypto
import Network.Haskoin.Transaction
import Network.Haskoin.Script
import Network.Haskoin.Util
import Network.Haskoin.Constants
import Network.Haskoin.Node.STM
import Network.Haskoin.Wallet.Types
import Network.Haskoin.Wallet.Settings
import Network.Haskoin.Wallet.Server
import Network.Haskoin.Wallet.Database
type Handler = R.ReaderT Config IO
-- hw start [config] [--detach]
cmdStart :: Handler ()
cmdStart = do
cfg <- R.ask
liftIO $ runSPVServer cfg
liftIO $ putStrLn "Process started"
-- hw stop [config]
cmdStop :: Handler ()
cmdStop = R.ask >>= \cfg -> liftIO $ do
stopSPVServer cfg
putStrLn "Process stopped"
cmdNewKeyRing :: [String] -> Handler ()
cmdNewKeyRing mnemonicLs = do
keyRingName <- R.asks configKeyRing
passphraseM <- R.asks configPass
let mnemonicM = pack <$> listToMaybe mnemonicLs
newKeyRing = NewKeyRing keyRingName passphraseM mnemonicM
sendZmq (PostKeyRingsR newKeyRing) $ putStr . printKeyRing
cmdKeyRing :: Handler ()
cmdKeyRing = do
k <- R.asks configKeyRing
sendZmq (GetKeyRingR k) $ putStr . printKeyRing
cmdKeyRings :: Handler ()
cmdKeyRings = sendZmq GetKeyRingsR $ \ks -> do
let xs = map (putStr . printKeyRing) ks
sequence_ $ intersperse (putStrLn "-") xs
cmdNewAcc :: String -> Handler ()
cmdNewAcc name = do
k <- R.asks configKeyRing
sendZmq (PostAccountsR k newAcc) $
\(JsonWithKeyRing _ acc) -> putStr $ printAccount acc
where
newAcc = NewAccount (pack name) (AccountRegular False) []
-- First argument: is account read-only?
cmdNewMS :: Bool -> String -> String -> String -> [String] -> Handler ()
cmdNewMS r name mStr nStr ks = case keysM of
Just keys -> do
k <- R.asks configKeyRing
let newAcc = NewAccount (pack name) (AccountMultisig r m n) keys
sendZmq (PostAccountsR k newAcc) $
\(JsonWithKeyRing _ acc) -> putStr $ printAccount acc
_ -> error "Could not parse key(s)"
where
m = read mStr
n = read nStr
keysM = mapM (xPubImport . cs) ks
cmdNewRead :: String -> String -> Handler ()
cmdNewRead name keyStr = case keyM of
Just key -> do
k <- R.asks configKeyRing
let newAcc = NewAccount (pack name) (AccountRegular True) [key]
sendZmq (PostAccountsR k newAcc) $
\(JsonWithKeyRing _ acc) -> putStr $ printAccount acc
_ -> error "Could not parse key"
where
keyM = xPubImport $ cs keyStr
cmdAddKeys :: String -> [String] -> Handler ()
cmdAddKeys name ks = case keysM of
Just keys -> do
k <- R.asks configKeyRing
sendZmq (PostAccountKeysR k (pack name) keys) $
\(JsonWithKeyRing _ acc) -> putStr $ printAccount acc
_ -> error "Could not parse key(s)"
where
keysM = mapM (xPubImport . cs) ks
cmdSetGap :: String -> String -> Handler ()
cmdSetGap name gap = do
k <- R.asks configKeyRing
sendZmq (PostAccountGapR k (pack name) setGap) $
\(JsonWithKeyRing _ acc) -> putStr $ printAccount acc
where
setGap = SetAccountGap $ read gap
cmdAccount :: String -> Handler ()
cmdAccount name = do
k <- R.asks configKeyRing
sendZmq (GetAccountR k $ pack name) $
\(JsonWithKeyRing _ acc) -> putStr $ printAccount acc
cmdAccounts :: Handler ()
cmdAccounts = do
k <- R.asks configKeyRing
sendZmq (GetAccountsR k) $ \(JsonWithKeyRing _ as) -> do
let xs = map (putStr . printAccount) as
sequence_ $ intersperse (putStrLn "-") xs
pagedAction :: (FromJSON a, ToJSON a)
=> [String]
-> (PageRequest -> WalletRequest)
-> ([a] -> IO ())
-> Handler ()
pagedAction pageLs requestBuilder action = do
c <- R.asks configCount
r <- R.asks configReversePaging
let pageReq = PageRequest page c r
sendZmq (requestBuilder pageReq) $ \(JsonWithAccount _ _ (PageRes a m)) -> do
putStrLn $ unwords [ "Page", show page, "of", show m ]
action a
where
page = fromMaybe 1 (read <$> listToMaybe pageLs)
cmdList :: String -> [String] -> Handler ()
cmdList name pageLs = do
k <- R.asks configKeyRing
t <- R.asks configAddrType
m <- R.asks configMinConf
o <- R.asks configOffline
let f = GetAddressesR k (pack name) t m o
pagedAction pageLs f $ \as -> forM_ as (putStrLn . printAddress)
cmdUnused :: String -> Handler ()
cmdUnused name = do
k <- R.asks configKeyRing
t <- R.asks configAddrType
sendZmq (GetAddressesUnusedR k (pack name) t) $
\(JsonWithAccount _ _ as) -> forM_ (as :: [JsonAddr]) $ putStrLn . printAddress
cmdLabel :: String -> String -> String -> Handler ()
cmdLabel name iStr label = do
k <- R.asks configKeyRing
t <- R.asks configAddrType
sendZmq (PutAddressR k (pack name) i t addrLabel) $
\(JsonWithAccount _ _ a) -> putStrLn $ printAddress a
where
i = read iStr
addrLabel = AddressLabel $ pack label
cmdTxs :: String -> [String] -> Handler ()
cmdTxs name pageLs = do
k <- R.asks configKeyRing
pagedAction pageLs (GetTxsR k (pack name)) $ \ts -> do
let xs = map (putStr . printTx) ts
sequence_ $ intersperse (putStrLn "-") xs
cmdAddrTxs :: String -> String -> [String] -> Handler ()
cmdAddrTxs name i pageLs = do
k <- R.asks configKeyRing
t <- R.asks configAddrType
c <- R.asks configCount
r <- R.asks configReversePaging
let req = GetAddrTxsR k (pack name) index t $ PageRequest page c r
sendZmq req $ \(JsonWithAddr _ _ _ (PageRes ts m)) -> do
putStrLn $ unwords [ "Page", show page, "of", show m ]
let xs = map (putStr . printAddrTx) ts
sequence_ $ intersperse (putStrLn "-") xs
where
page = fromMaybe 1 (read <$> listToMaybe pageLs)
index = read i
cmdGenAddrs :: String -> String -> Handler ()
cmdGenAddrs name i = do
k <- R.asks configKeyRing
t <- R.asks configAddrType
let req = PostAddressesR k (pack name) index t
sendZmq req $ \(JsonWithAccount _ _ cnt) ->
putStrLn $ unwords [ "Generated", show (cnt :: Int), "addresses" ]
where
index = read i
cmdSend :: String -> String -> String -> Handler ()
cmdSend name addrStr amntStr = cmdSendMany name [addrStr ++ ":" ++ amntStr]
cmdSendMany :: String -> [String] -> Handler ()
cmdSendMany name xs = case rcpsM of
Just rcps -> do
k <- R.asks configKeyRing
fee <- R.asks configFee
rcptFee <- R.asks configRcptFee
minconf <- R.asks configMinConf
sign <- R.asks configSignTx
let action = CreateTx rcps fee minconf rcptFee sign
sendZmq (PostTxsR k (pack name) action) $
\(JsonWithAccount _ _ tx) -> putStr $ printTx tx
_ -> error "Could not parse recipient information"
where
g str = map cs $ splitOn ":" (pack str)
f [a,v] = liftM2 (,) (base58ToAddr a) (return $ read $ cs v)
f _ = Nothing
rcpsM = mapM (f . g) xs
cmdImport :: String -> String -> Handler ()
cmdImport name txStr = case txM of
Just tx -> do
k <- R.asks configKeyRing
let action = ImportTx tx
sendZmq (PostTxsR k (pack name) action) $
\(JsonWithAccount _ _ t) -> putStr $ printTx t
_ -> error "Could not parse transaction"
where
txM = decodeToMaybe =<< decodeHex (cs txStr)
cmdSign :: String -> String -> Handler ()
cmdSign name txidStr = case txidM of
Just txid -> do
k <- R.asks configKeyRing
let action = SignTx txid
sendZmq (PostTxsR k (pack name) action) $
\(JsonWithAccount _ _ tx) -> putStr $ printTx tx
_ -> error "Could not parse txid"
where
txidM = hexToTxHash $ cs txidStr
cmdGetOffline :: String -> String -> Handler ()
cmdGetOffline name tidStr = case tidM of
Just tid -> do
k <- R.asks configKeyRing
sendZmq (GetOfflineTxR k (pack name) tid) $
\(OfflineTxData tx dat) -> do
putStrLn $ unwords
[ "Tx :", cs $ encodeHex $ encode' tx ]
putStrLn $ unwords
[ "CoinData:", cs $ encodeHex $ cs $ encode dat ]
_ -> error "Could not parse txid"
where
tidM = hexToTxHash $ cs tidStr
cmdSignOffline :: String -> String -> String -> Handler ()
cmdSignOffline name txStr datStr = case (txM, datM) of
(Just tx, Just dat) -> do
k <- R.asks configKeyRing
sendZmq (PostOfflineTxR k (pack name) tx dat) $
\(TxCompleteRes tx' c) -> do
putStrLn $ unwords [ "Tx :", cs $ encodeHex $ encode' tx' ]
putStrLn $ unwords [ "Complete:", if c then "Yes" else "No" ]
_ -> error "Could not decode input data"
where
datM = decode . cs =<< decodeHex (cs datStr)
txM = decodeToMaybe =<< decodeHex (cs txStr)
cmdBalance :: String -> Handler ()
cmdBalance name = do
k <- R.asks configKeyRing
m <- R.asks configMinConf
o <- R.asks configOffline
sendZmq (GetBalanceR k (pack name) m o) $
\(JsonWithAccount _ _ bal) ->
putStrLn $ unwords [ "Balance:", show (bal :: Word64) ]
cmdGetTx :: String -> String -> Handler ()
cmdGetTx name tidStr = case tidM of
Just tid -> do
k <- R.asks configKeyRing
sendZmq (GetTxR k (pack name) tid) $
\(JsonWithAccount _ _ tx) -> putStr $ printTx tx
_ -> error "Could not parse txid"
where
tidM = hexToTxHash $ cs tidStr
cmdRescan :: [String] -> Handler ()
cmdRescan timeLs =
sendZmq (PostNodeR $ NodeActionRescan timeM) $ \(RescanRes ts) ->
putStrLn $ unwords [ "Timestamp:", show ts]
where
timeM = read <$> listToMaybe timeLs
cmdDecodeTx :: String -> Handler ()
cmdDecodeTx txStr = do
when (isNothing txM) $ error "Could not parse transaction"
format <- R.asks configFormat
liftIO $ formatStr $ cs $ case format of
OutputJSON -> cs jsn
_ -> YAML.encode val
where
txM = decodeToMaybe =<< decodeHex (cs txStr)
val = encodeTxJSON $ fromJust txM
jsn = JSON.encodePretty' JSON.defConfig{ JSON.confIndent = 2 } val
cmdVersion :: Handler ()
cmdVersion = liftIO $ do
putStrLn $ unwords [ "network :", cs networkName ]
putStrLn $ unwords [ "user-agent:", cs haskoinUserAgent ]
putStrLn $ unwords [ "database :", cs databaseEngine ]
cmdStatus :: Handler ()
cmdStatus = do
v <- R.asks configVerbose
sendZmq (PostNodeR NodeActionStatus) $ mapM_ putStrLn . printNodeStatus v
{- Helpers -}
sendZmq :: (FromJSON a, ToJSON a)
=> WalletRequest -> (a -> IO ()) -> Handler ()
sendZmq req handle = do
sockName <- R.asks configConnect
resE <- liftIO $ runZMQ $ do
sock <- socket Req
connect sock sockName
send sock [] (cs $ encode req)
eitherDecode . cs <$> receive sock
case resE of
Right (ResponseValid (Just a)) -> formatOutput a =<< R.asks configFormat
Right (ResponseValid Nothing) -> return ()
Right (ResponseError err) -> error $ unpack err
Left err -> error err
where
formatOutput a format = liftIO $ case format of
OutputJSON -> formatStr $ cs $
JSON.encodePretty' JSON.defConfig{ JSON.confIndent = 2 } a
OutputYAML -> formatStr $
cs $ YAML.encode a
OutputNormal -> handle a
formatStr :: String -> IO ()
formatStr str = forM_ (lines str) putStrLn
encodeTxJSON :: Tx -> Value
encodeTxJSON tx@(Tx v is os i) = object
[ "txid" .= (cs $ txHashToHex (txHash tx) :: Text)
, "version" .= v
, "inputs" .= zipWith input is [0..]
, "outputs" .= zipWith output os [0..]
, "locktime" .= i
]
where
input x j = object
[pack ("input " ++ show (j :: Int)) .= encodeTxInJSON x]
output x j = object
[pack ("output " ++ show (j :: Int)) .= encodeTxOutJSON x]
encodeTxInJSON :: TxIn -> Value
encodeTxInJSON (TxIn o s i) = object $
[ "outpoint" .= encodeOutPointJSON o
, "sequence" .= i
, "raw-script" .= (cs $ encodeHex s :: Text)
, "script" .= encodeScriptJSON sp
] ++ decoded
where
sp = fromMaybe (Script []) $ decodeToMaybe s
decoded = either (const []) f $ decodeInputBS s
f inp = ["decoded-script" .= encodeScriptInputJSON inp]
encodeTxOutJSON :: TxOut -> Value
encodeTxOutJSON (TxOut v s) = object $
[ "value" .= v
, "raw-script" .= (cs $ encodeHex s :: Text)
, "script" .= encodeScriptJSON sp
] ++ decoded
where
sp = fromMaybe (Script []) $ decodeToMaybe s
decoded = either (const [])
(\out -> ["decoded-script" .= encodeScriptOutputJSON out])
(decodeOutputBS s)
encodeOutPointJSON :: OutPoint -> Value
encodeOutPointJSON (OutPoint h i) = object
[ "txid" .= (cs $ txHashToHex h :: Text)
, "pos" .= i
]
encodeScriptJSON :: Script -> Value
encodeScriptJSON (Script ops) =
toJSON $ map f ops
where
f (OP_PUSHDATA bs _) = String $ pack $ unwords
["OP_PUSHDATA", cs $ encodeHex bs]
f x = String $ pack $ show x
encodeScriptInputJSON :: ScriptInput -> Value
encodeScriptInputJSON si = case si of
RegularInput (SpendPK s) -> object
[ "spendpubkey" .= object [ "sig" .= encodeSigJSON s ] ]
RegularInput (SpendPKHash s p) -> object
[ "spendpubkeyhash" .= object
[ "sig" .= encodeSigJSON s
, "pubkey" .= (cs $ encodeHex (encode' p) :: Text)
, "sender-address" .= (cs $ addrToBase58 (pubKeyAddr p) :: Text)
]
]
RegularInput (SpendMulSig sigs) -> object
[ "spendmulsig" .= object [ "sigs" .= map encodeSigJSON sigs ] ]
ScriptHashInput s r -> object
[ "spendscripthash" .= object
[ "scriptinput" .= encodeScriptInputJSON (RegularInput s)
, "redeem" .= encodeScriptOutputJSON r
, "raw-redeem" .= (cs $ encodeHex (encodeOutputBS r) :: Text)
, "sender-address" .= (cs $ addrToBase58 (scriptAddr r) :: Text)
]
]
encodeScriptOutputJSON :: ScriptOutput -> Value
encodeScriptOutputJSON so = case so of
PayPK p -> object
[ "pay2pubkey" .= object
[ "pubkey" .= (cs $ encodeHex (encode' p) :: Text) ]
]
PayPKHash a -> object
[ "pay2pubkeyhash" .= object
[ "address-base64" .=
(cs $ encodeHex (encode' $ getAddrHash a) :: Text)
, "address-base58" .= (cs $ addrToBase58 a :: Text)
]
]
PayMulSig ks r -> object
[ "pay2mulsig" .= object
[ "required-keys" .= r
, "pubkeys" .= (map (cs . encodeHex . encode') ks :: [Text])
]
]
PayScriptHash a -> object
[ "pay2scripthash" .= object
[ "address-base64" .= (cs $ encodeHex $ encode' $ getAddrHash a :: Text)
, "address-base58" .= (cs (addrToBase58 a) :: Text)
]
]
encodeSigJSON :: TxSignature -> Value
encodeSigJSON ts@(TxSignature _ sh) = object
[ "raw-sig" .= (cs $ encodeHex (encodeSig ts) :: Text)
, "sighash" .= encodeSigHashJSON sh
]
encodeSigHashJSON :: SigHash -> Value
encodeSigHashJSON sh = case sh of
SigAll acp -> object
[ "type" .= String "SigAll"
, "acp" .= acp
]
SigNone acp -> object
[ "type" .= String "SigNone"
, "acp" .= acp
]
SigSingle acp -> object
[ "type" .= String "SigSingle"
, "acp" .= acp
]
SigUnknown acp v -> object
[ "type" .= String "SigUnknown"
, "acp" .= acp
, "value" .= v
]
{- Print utilities -}
printKeyRing :: JsonKeyRing -> String
printKeyRing JsonKeyRing{..} = unlines $
[ "KeyRing: " ++ unpack jsonKeyRingName ]
++
[ "Master key: " ++ cs (xPrvExport m)
| m <- maybeToList jsonKeyRingMaster
]
++
[ "Mnemonic: " ++ cs m
| m <- maybeToList jsonKeyRingMnemonic
]
printAccount :: JsonAccount -> String
printAccount JsonAccount{..} = unlines $
[ "Account: " ++ unpack jsonAccountName
, "Type : " ++ showType
, "Gap : " ++ show jsonAccountGap
]
++
[ "Deriv : " ++ pathToStr d
| d <- maybeToList jsonAccountDerivation
]
++
concat [ printKeys | not (null jsonAccountKeys) ]
where
printKeys =
("Keys : " ++ cs (xPubExport (head jsonAccountKeys))) :
map ((" " ++) . cs . xPubExport) (tail jsonAccountKeys)
showType = case jsonAccountType of
AccountRegular r -> if r then "Read-Only" else "Regular"
AccountMultisig r m n -> unwords
[ if r then "Read-Only Multisig" else "Multisig"
, show m, "of", show n
]
printAddress :: JsonAddr -> String
printAddress JsonAddr{..} = unwords $
[ show jsonAddrIndex, ":", cs (addrToBase58 jsonAddrAddress) ]
++
[ "(" ++ unpack jsonAddrLabel ++ ")" | not (null $ unpack jsonAddrLabel) ]
++ concat
[ [ "[Received: " ++ show (balanceInfoInBalance bal) ++ "]"
, "[Coins: " ++ show (balanceInfoCoins bal) ++ "]"
, "[Spent Coins: " ++ show (balanceInfoSpentCoins bal) ++ "]"
]
| isJust jsonAddrBalance && balanceInfoCoins bal > 0
]
where
bal = fromMaybe (error "Could not get address balance") jsonAddrBalance
printTx :: JsonTx -> String
printTx tx@JsonTx{..} = unlines $
[ "Value : " ++ printTxType jsonTxType ++ " " ++ show jsonTxValue ]
++
[ "Confidence : " ++ printTxConfidence tx ]
++ concat
[ printAddrInfos "Inputs : " jsonTxInputs
| not (null jsonTxInputs)
]
++ concat
[ printAddrInfos "Outputs : " jsonTxOutputs
| not (null jsonTxOutputs)
]
++ concat
[ printAddrInfos "Change : " jsonTxChange
| not (null jsonTxChange)
]
where
printAddrInfos header xs =
(header ++ f (head xs)) :
map ((" " ++) . f) (tail xs)
f (AddressInfo addr valM local) = unwords $
cs (addrToBase58 addr) :
[ show v | v <- maybeToList valM ]
++
[ if local then "<-" else "" ]
printAddrTx :: AddrTx -> String
printAddrTx (AddrTx tx BalanceInfo{..}) = unlines $
concat
[ [ "Incoming value: " ++ show balanceInfoInBalance
, "Incoming coins: " ++ show balanceInfoCoins
]
| balanceInfoInBalance > 0
] ++ concat
[ [ "Outgoing value: " ++ show balanceInfoOutBalance
, "Spent coins : " ++ show balanceInfoSpentCoins
]
| balanceInfoOutBalance > 0
] ++
[ "Confidence : " ++ printTxConfidence tx ]
printTxConfidence :: JsonTx -> String
printTxConfidence JsonTx{..} = case jsonTxConfidence of
TxBuilding -> "Building" ++ confirmations
TxPending -> "Pending" ++ confirmations
TxDead -> "Dead" ++ confirmations
TxOffline -> "Offline"
where
confirmations = case jsonTxConfirmations of
Just conf -> " (Confirmations: " ++ show conf ++ ")"
_ -> ""
printTxType :: TxType -> String
printTxType t = case t of
TxIncoming -> "Incoming"
TxOutgoing -> "Outgoing"
TxSelf -> "Self"
printNodeStatus :: Bool -> NodeStatus -> [String]
printNodeStatus verbose NodeStatus{..} =
[ "Network Height : " ++ show nodeStatusNetworkHeight
, "Best Header : " ++ cs (blockHashToHex nodeStatusBestHeader)
, "Best Header Height: " ++ show nodeStatusBestHeaderHeight
, "Best Block : " ++ cs (blockHashToHex nodeStatusBestBlock)
, "Bloom Filter Size : " ++ show nodeStatusBloomSize
] ++
[ "Header Peer : " ++ show h
| h <- maybeToList nodeStatusHeaderPeer, verbose
] ++
[ "Merkle Peer : " ++ show m
| m <- maybeToList nodeStatusMerklePeer, verbose
] ++
[ "Pending Headers : " ++ show nodeStatusHaveHeaders | verbose ] ++
[ "Pending Tickles : " ++ show nodeStatusHaveTickles | verbose ] ++
[ "Pending Txs : " ++ show nodeStatusHaveTxs | verbose ] ++
[ "Pending GetData : " ++ show (map txHashToHex nodeStatusGetData)
| verbose
] ++
[ "Pending Rescan : " ++ show r
| r <- maybeToList nodeStatusRescan, verbose
] ++
[ "Synced Mempool : " ++ show nodeStatusMempool | verbose ] ++
[ "HeaderSync Lock : " ++ show nodeStatusSyncLock | verbose ] ++
[ "LevelDB Lock : " ++ show nodeStatusLevelDBLock | verbose ] ++
[ "Peers: " ] ++
intercalate ["-"] (map (printPeerStatus verbose) nodeStatusPeers)
printPeerStatus :: Bool -> PeerStatus -> [String]
printPeerStatus verbose PeerStatus{..} =
[ " Peer Id : " ++ show peerStatusPeerId
, " Peer Host: " ++ peerHostString peerStatusHost
, " Connected: " ++ if peerStatusConnected then "yes" else "no"
, " Height : " ++ show peerStatusHeight
] ++
[ " Protocol : " ++ show p | p <- maybeToList peerStatusProtocol
] ++
[ " UserAgent: " ++ ua | ua <- maybeToList peerStatusUserAgent
] ++
[ " Avg Ping : " ++ p | p <- maybeToList peerStatusPing
] ++
[ " DoS Score: " ++ show d | d <- maybeToList peerStatusDoSScore
] ++
[ " ThreadId : " ++ peerStatusThreadId | verbose ] ++
[ " Merkles : " ++ show peerStatusHaveMerkles | verbose ] ++
[ " Messages : " ++ show peerStatusHaveMessage | verbose ] ++
[ " Nonces : " ++ show peerStatusPingNonces | verbose ] ++
[ " Reconnect: " ++ show t
| t <- maybeToList peerStatusReconnectTimer, verbose
] ++
[ " Logs : " | verbose ] ++
[ " - " ++ msg | msg <- maybe [] id peerStatusLog, verbose]