packages feed

stratum-tool-0.0.4: src/Main.hs

{-# LANGUAGE DeriveDataTypeable, RecordWildCards, OverloadedStrings #-}
module Main where

import Control.Applicative
import Control.Concurrent.Async (mapConcurrently)
import Control.Concurrent.STM (atomically, readTChan)
import Control.Monad (when)
import Data.Aeson
import Data.ByteString.Builder
import qualified Data.HashMap.Strict as H
import qualified Data.Map as M
import qualified Data.Vector as V
import Data.Monoid
import Data.String (fromString)
import Data.Text as T (Text, pack, toLower)
import System.Console.CmdArgs.Implicit
import System.IO

import BitPay
import Common
import StratumClient
import PrettyJson

type Printer = Value -> IO ()

data Args = Args { server   :: String
                 , port     :: Int
                 , command  :: String
                 , params   :: [String]
                 , multi    :: Bool
                 , json     :: Bool
                 , follow   :: Bool
                 , currency :: String
                 , security :: Security
                 } deriving (Show, Data, Typeable)

synopsis =
  Args { server = "electrum.bittiraha.fi" &=
                  help "Electrum server address (electrum.bittiraha.fi)" &=
                  typ "HOST" &= name "s"
       , port = def &= help "Electrum port (50002 for SSL, 50001 for TCP)"
       , command = def &= argPos 0 &= typ "COMMAND"
       , params = def &= args &= typ "PARAMS"
       , multi = def &=
                 help "Instead of passing multiple parameters for a single \
                      \command, repeat command for each argument"
       , json = def &=
                help "Output as raw JSON instead of JSON breadcrumbs format"
       , follow = def &=
                  help "Subscribe to given addresses and run given command \
                       \when something happens. Implies --multi."
       , currency = def &= typ "CODE" &=
                    help "Convert bitcoins to given currency using BitPay. \
                         \All currency codes supported by BitPay are available."
       , security = Ssl &= name "S" &= typ "MODE" &=
                    help "Connection method to Electrum server. Possible \
                         \values: 'tcp' for unencrypted connection, \
                         \'ssl' for SSL without certificate check (default), \
                         \and 'safessl' for SSL with certificate check."
       }
  &= program "stratum-tool"
  &= summary "StratumTool v0.0.4"
  &= help "Connect to Electrum server via Stratum protocol and \
          \allows querying wallet balances etc."

main = do
  args@Args{..} <- cmdArgs synopsis
  let realPort = case (port, security) of
        (0, SafeSsl)   -> 50002
        (0, Ssl)       -> 50002
        (0, Tcp)       -> 50001
        (x, _)         -> fromIntegral x
  stratumConn <- connectStratum server realPort security
  hSetBuffering stdout LineBuffering
  bitpay <- initBitpay
  let currencyText = T.toLower $ T.pack currency
      printer ans = do
        -- First print the value as usual
        printValue json ans
        -- When currency conversion is needed, then update rates and
        -- print converted values if they contain any amounts.
        when (currency /= "" && usefulValue (currencyInjector ans Nothing)) $ do
          rates <- bitpay
          printValue json $
            currencyInjector ans $ Just $ simpleRate rates currencyText
  (if follow then trackAddresses else oneTime) printer stratumConn args

-- |Track changes in given addresses and run the command when changes
-- occur.
trackAddresses :: Printer -> StratumConn -> Args -> IO ()
trackAddresses printer stratumConn Args{..} = do
  chan <- stratumChan stratumConn "blockchain.address.subscribe"
  -- Subscribe and collect the hashes for future comparison
  hashes <- mapConcurrently (qv "blockchain.address.subscribe" . pure) params
  -- Print current state at first
  oneTime printer stratumConn Args{multi=True,..}
  -- Listen for changes
  let loop m = do
        [addr,newHash] <- takeJSON <$> atomically (readTChan chan)
        if m M.! addr /= newHash
          then do newValue <- qv command [addr]
                  printer $ object [fromString addr .= newValue]
                  loop $ M.insert addr newHash m
          else loop m
    in loop $ M.fromList $ zipWith mapify params hashes
  where qv = queryStratumValue stratumConn
        mapify a h = (a, takeJSON h)

-- |Process single request. 
oneTime :: Printer -> StratumConn -> Args -> IO ()
oneTime printer stratumConn Args{..} = do
  ans <- if multi
         then objectZip params <$>
              mapConcurrently (queryStratumValue stratumConn command . pure) params
         else queryStratumValue stratumConn command params
  printer ans

-- |Prints given JSON value to stdout. When `json` is True, then just
-- print as encoded to JSON, otherwise breadcrumbs format is used.
printValue :: Bool -> Printer
printValue json ans =
  hPutBuilder stdout $ if json
                       then lazyByteString (encode ans) <> byteString "\n"
                       else breadcrumbs ans

-- |Pairs a given list of strings corresponding values to generate
-- JSON object with string as a key.
objectZip :: [String] -> [Value] -> Value
objectZip ss vs = object $ zipWith toPair ss vs
  where toPair s v = (fromString s, v)

-- |Inject currency data recursively to given Value. Vacuum all other
-- data from the JSON value.
currencyInjector :: Value -> Maybe (Text, Value) -> Value
currencyInjector v rate = recurse v
  where
    recurse (Object o) = Object $ H.filter usefulValue $ H.map conv $ H.filterWithKey isAmount o
    recurse (Array a) = Array $ V.filter usefulValue $ V.map recurse a
    recurse v = v
    -- isAmount keeps Numbers which are currencies, Objects, and Arrays
    isAmount k (Number _) = k `elem` currencyFields
    isAmount _ (Object _) = True
    isAmount _ (Array _) = True
    isAmount _ _ = False
    -- conv converts all Numbers and recurses into others
    conv (Number n) = maybe Null (inject $ Number n) rate
    conv v = recurse v

-- |List of Stratum object key names which contain bitcoin amounts.
currencyFields :: [Text]
currencyFields = ["confirmed"
                 ,"unconfirmed"
                 ,"value"
                 ]

-- |Converts given numeric value to Object containing amount in
-- satoshis and given currency.
inject :: Value -> (Text, Value) -> Value
inject (Number n) (code, Number rate) =
  object [(code, Number (n*rate*1e-8))]

-- |Filter for removing empty objects and arrays.
usefulValue :: Value -> Bool
usefulValue (Object o) = not $ H.null o
usefulValue (Array a) = not $ V.null a
usefulValue _ = True