packages feed

haskoin-wallet-0.4.1: examples/embedded-inproc-wallet-server/Main.hs

module Main where

import           Network.Haskoin.Wallet           (Config(..),
                                                   WalletRequest(..), WalletResponse(..),
                                                   AddressType(..),   OutputFormat(..),
                                                   SPVMode(..),       NodeAction(..))

import           Network.Haskoin.Wallet.Server    (runSPVServerWithContext)
import           Network.Haskoin.Wallet.Internals (BTCNode(..), Notif(..))
import qualified Network.Haskoin.Node.STM       as Node

import           Data.String.Conversions          (cs)
import qualified System.ZMQ4                    as ZMQ
import qualified Control.Monad.Logger           as Log
import qualified Data.HashMap.Strict            as HM
import qualified Database.Persist.Sqlite        as DB
import qualified Control.Monad.Trans.Resource   as Resource
import qualified Data.Aeson                     as JSON
import qualified Control.Concurrent             as Con
import qualified Data.Aeson.Encode.Pretty       as PrettyJSON
import qualified Control.Monad                  as M
import qualified Control.Exception              as Except


databaseConf :: DB.SqliteConf
databaseConf = DB.SqliteConf "/tmp/tmpdb" 1

cmdSocket :: String
cmdSocket = "inproc://cmd"

notifSocket :: String
notifSocket = "inproc://notif"


-- |Simple example app that embeds a haskoin-wallet server.
--  Start wallet server + notification thread, and execute Status command when pressing ENTER
main :: IO ()
main = ZMQ.withContext $ \ctx -> do
    -- Server
    putStrLn "Starting server..."
    _ <- Con.forkIO $ runWallet walletServerConf ctx
    -- Notify thread
    putStrLn "Starting notification thread..."
    _ <- Con.forkIO $ notifyThread ctx notifyHandler
    -- Status loop
    M.forever $ do
        putStrLn "Press ENTER to get server status..."
        _ <- getLine
        cmdGetStatus ctx >>= printStatusJSON
  where
    printStatusJSON     = putStrLn . cs . PrettyJSON.encodePretty
    notifyHandler notif =
        putStrLn $ "NOTIFY: New block: " ++ cs (PrettyJSON.encodePretty notif)

-- |Run haskoin-wallet using the specified ZeroMQ Context,
--  and log to stderr.
runWallet :: Config -> ZMQ.Context -> IO ()
runWallet cfg ctx = run $ runSPVServerWithContext cfg ctx
    where run           = Resource.runResourceT . runLogging
          runLogging    = Log.runStderrLoggingT . Log.filterLogger logFilter
          logFilter _ l = l >= configLogLevel cfg

cmdGetStatus :: ZMQ.Context -> IO Node.NodeStatus
cmdGetStatus ctx =
    sendCmdOrFail (PostNodeR NodeActionStatus) ctx >>=
    \res -> case res of
        Nothing     -> error "ERROR: Status command: no response."
        Just status -> return status

sendCmdOrFail :: (JSON.FromJSON a, JSON.ToJSON a)
              => WalletRequest
              -> ZMQ.Context
              -> IO (Maybe a)
sendCmdOrFail cmd ctx =
    sendCmd cmd ctx >>=
    either error return >>=
    \res -> case res of
        ResponseError e -> error $ "ERROR: Send cmd, ResponseError: " ++ cs e
        ResponseValid r -> return r

sendCmd :: (JSON.FromJSON a, JSON.ToJSON a)
        => WalletRequest
        -> ZMQ.Context
        -> IO (Either String (WalletResponse a))
sendCmd req ctx =
    ZMQ.withSocket ctx ZMQ.Req $ \sock -> do
        ZMQ.setLinger (ZMQ.restrict (0 :: Int)) sock
        ZMQ.connect sock cmdSocket
        ZMQ.send sock [] (cs $ JSON.encode req)
        JSON.eitherDecode . cs <$> ZMQ.receive sock

-- |Connect to notify socket, subscribe to new blocks,
--  and execute the supplied handler for each new block as it arrives.
notifyThread :: ZMQ.Context -> (Notif -> IO ()) -> IO ()
notifyThread ctx handler = waitAndCatch $
    ZMQ.withSocket ctx ZMQ.Sub $ \sock -> do
        ZMQ.setLinger (ZMQ.restrict (0 :: Int)) sock
        ZMQ.connect sock notifSocket
        ZMQ.subscribe sock "[block]"
        putStrLn "NOTIFY: Connected. Subscribed to new blocks."
        M.forever $ do
            [_,m] <- ZMQ.receiveMulti sock
            notif <- either failOnErr return $ JSON.eitherDecode (cs m)
            handler notif
  where
    failOnErr = fail . ("NOTIFY: ERROR: recv failed: " ++)
    waitAndCatch ioa = Con.threadDelay 10000 >> ioa `Except.finally` waitAndCatch ioa

btcNodes :: [BTCNode]
btcNodes =
    [ BTCNode "dnsseed.bluematt.me"             8333
    , BTCNode "dnsseed.bitcoin.dashjr.org"      8333
    , BTCNode "dnsseed.bluematt.me"             8333
    , BTCNode "seed.bitcoinstats.com"           8333
    , BTCNode "seed.bitcoin.jonasschnelli.ch"   8333
    , BTCNode "seed.bitcoin.sipa.be"            8333
    , BTCNode "seed.bitnodes.io"                8333
    , BTCNode "seed.btcc.com"                   8333
    ]

walletServerConf :: Config
walletServerConf = Config
    { configCount         = 100
    -- ^ Output size of commands
    , configMinConf       = 6
    -- ^ Minimum number of confirmations
    , configSignTx        = True
    -- ^ Sign transactions
    , configFee           = 50000
    -- ^ Fee to pay per 1000 bytes when creating new transactions
    , configRcptFee       = False
    -- ^ Recipient pays fee (dangerous, no config file setting)
    , configAddrType      = AddressExternal
    -- ^ Return internal instead of external addresses
    , configOffline       = False
    -- ^ Display the balance including offline transactions
    , configReversePaging = False
    -- ^ Use reverse paging for displaying addresses and transactions
    , configPath          = Nothing
    -- ^ Derivation path when creating account
    , configFormat        = OutputNormal
    -- ^ How to format the command-line results
    , configConnect       = cmdSocket
    -- ^ ZeroMQ socket to connect to (location of the server)
    , configConnectNotif  = notifSocket
    -- ^ ZeroMQ socket to connect for notifications
    , configDetach        = False
    -- ^ Detach server when launched from command-line
    , configFile          = ""
    -- ^ Configuration file
    , configTestnet       = False
    -- ^ Use Testnet3 network
    , configDir           = ""
    -- ^ Working directory
    , configBind          = cmdSocket
    -- ^ Bind address for the ZeroMQ socket
    , configBindNotif     = notifSocket
    -- ^ Bind address for ZeroMQ notifications
    , configBTCNodes      = HM.fromList [ ( "prodnet", btcNodes ) ]
    -- ^ Trusted Bitcoin full nodes to connect to
    , configMode          = SPVOnline
    -- ^ Operation mode of the SPV node.
    , configBloomFP       = 0.00001
    -- ^ False positive rate for the bloom filter.
    , configDatabase      = HM.fromList [ ( "prodnet", databaseConf ) ]
    -- ^ Database configuration
    , configLogFile       = ""
    -- ^ Log file
    , configPidFile       = ""
    -- ^ PID File
    , configLogLevel      = Log.LevelInfo
    -- ^ Log level
    , configVerbose       = True
    -- ^ Verbose
    , configServerKey     = Nothing
    -- ^ Server key for authentication and encryption (server config)
    , configServerKeyPub  = Nothing
    -- ^ Server public key for authentication and encryption (client config)
    , configClientKey     = Nothing
    -- ^ Client key for authentication and encryption (client config)
    , configClientKeyPub  = Nothing
    -- ^ Client public key for authentication and encryption
    }