packages feed

haskoin-wallet-0.2.0: Network/Haskoin/Wallet/Server/Handler.hs

module Network.Haskoin.Wallet.Server.Handler where

import Control.Arrow (first)
import Control.Monad (when, unless, liftM)
import Control.Exception (SomeException(..), throwIO, tryJust)
import Control.Monad.Trans (liftIO, MonadIO, lift)
import Control.Monad.Logger (MonadLogger, logInfo, logError)
import Control.Monad.Base (MonadBase)
import Control.Monad.Catch (MonadThrow)
import Control.Monad.Trans.Resource (MonadResource)
import Control.Monad.Trans.Control (MonadBaseControl, liftBaseOp_)
import qualified Control.Concurrent.MSem as Sem (MSem, with)
import qualified Control.Monad.State as S (StateT, evalStateT, gets)

import Data.Aeson (Value(..), toJSON)
import Data.Word (Word32)
import Data.Text (Text, pack, unpack)
import qualified Data.Map.Strict as M (intersectionWith, fromList, elems)
import Data.String.Conversions (cs)

import Database.Esqueleto (SqlPersistT, Entity(..))

import Database.Persist.Sql
    ( SqlPersistM
    , ConnectionPool
    , runSqlPool
    , runSqlPersistMPool
    )

import Network.Haskoin.Crypto
import Network.Haskoin.Transaction
import Network.Haskoin.Node.STM
import Network.Haskoin.Node.HeaderTree
import Network.Haskoin.Node.BlockChain
import Network.Haskoin.Node.Peer

import Network.Haskoin.Wallet.Model
import Network.Haskoin.Wallet.KeyRing
import Network.Haskoin.Wallet.Transaction
import Network.Haskoin.Wallet.Settings
import Network.Haskoin.Wallet.Types

type Handler m = S.StateT HandlerSession m

data HandlerSession = HandlerSession
    { handlerConfig    :: !Config
    , handlerPool      :: !ConnectionPool
    , handlerNodeState :: !(Maybe SharedNodeState)
    , handlerSem       :: !(Sem.MSem Int)
    }

runHandler :: Monad m => HandlerSession -> Handler m a -> m a
runHandler = flip S.evalStateT

runDB :: MonadBaseControl IO m => SqlPersistT m a -> Handler m a
runDB action = do
    sem  <- S.gets handlerSem
    pool <- S.gets handlerPool
    lift $ runDBPool sem pool action

runDBPool :: MonadBaseControl IO m
          => Sem.MSem Int -> ConnectionPool -> SqlPersistT m a -> m a
runDBPool sem pool action = liftBaseOp_ (Sem.with sem) $ runSqlPool action pool

tryDBPool :: (MonadIO m, MonadLogger m)
          => Sem.MSem Int -> ConnectionPool -> SqlPersistM a -> m (Maybe a)
tryDBPool sem pool action = do
    resE <- liftIO $ Sem.with sem $ tryJust f $ runSqlPersistMPool action pool
    case resE of
        Right res -> return $ Just res
        Left err -> do
            $(logError) $ pack $ unwords [ "A database error occured:", err]
            return Nothing
  where
    f (SomeException e) = Just $ show e

runNode :: MonadIO m => NodeT m a -> Handler m a
runNode action = do
    nodeStateM <- S.gets handlerNodeState
    case nodeStateM of
        Just nodeState -> lift $ runNodeT nodeState action
        _ -> error "runNode: No node state available"

{- Server Handlers -}

getKeyRingsR :: ( MonadLogger m
                , MonadIO m
                , MonadBaseControl IO m
                , MonadBase IO m
                , MonadThrow m
                , MonadResource m
                )
             => Handler m (Maybe Value)
getKeyRingsR = do
    $(logInfo) $ format "GetKeyRingsR"
    res <- runDB keyRings
    return $ Just $ toJSON $ map (\k -> toJsonKeyRing k Nothing Nothing) res

getKeyRingR :: (MonadLogger m, MonadBaseControl IO m, MonadIO m)
            => KeyRingName -> Handler m (Maybe Value)
getKeyRingR name = do
    $(logInfo) $ format $ unwords [ "GetKeyRingR", unpack name ]
    Entity _ keyRing <- runDB $ getKeyRing name
    return $ Just $ toJSON $ toJsonKeyRing keyRing Nothing Nothing

postKeyRingsR :: (MonadLogger m, MonadBaseControl IO m, MonadIO m)
              => NewKeyRing -> Handler m (Maybe Value)
postKeyRingsR (NewKeyRing name passM msM) = do
    $(logInfo) $ format $ unwords [ "PostKeyRingsR", unpack name ]
    (ms, seed) <- case msM of
        Just ms -> case mnemonicToSeed pass (cs ms) of
            Left err   -> liftIO $ throwIO $ WalletException err
            Right seed -> return (cs ms, seed)
        Nothing -> do
            ent <- liftIO $ getEntropy 16
            either (liftIO . throwIO . WalletException) return $ do
                ms   <- toMnemonic ent
                seed <- mnemonicToSeed pass ms
                return (cs ms, seed)
    Entity _ keyRing <- runDB $ newKeyRing name seed
    return $ Just $ toJSON $ toJsonKeyRing keyRing Nothing (Just ms)
  where
    pass = maybe "" cs passM

getAccountsR :: ( MonadLogger m
                , MonadIO m
                , MonadBaseControl IO m
                , MonadBase IO m
                , MonadThrow m
                , MonadResource m
                )
             => KeyRingName -> Handler m (Maybe Value)
getAccountsR keyRingName = do
    $(logInfo) $ format $ unwords [ "GetAccountsR", unpack keyRingName ]
    (keyRing, accs) <- runDB $ do
        Entity ki keyRing <- getKeyRing keyRingName
        accs <- accounts ki
        return (keyRing, accs)
    return $ Just $ toJSON JsonWithKeyRing
        { withKeyRingKeyRing = toJsonKeyRing keyRing Nothing Nothing
        , withKeyRingData    = map toJsonAccount accs
        }

postAccountsR
    :: ( MonadResource m, MonadThrow m, MonadLogger m
       , MonadBaseControl IO m, MonadIO m
       )
    => KeyRingName -> NewAccount -> Handler m (Maybe Value)
postAccountsR keyRingName NewAccount{..} = do
    $(logInfo) $ format $ unlines
        [ "PostAccountsR"
        , "  KeyRing name: " ++ unpack keyRingName
        , "  Account type: " ++ show newAccountType
        , "  Account name: " ++ unpack newAccountAccountName
        ]
    (keyRing, Entity _ newAcc) <- runDB $ do
        keyRingE@(Entity _ keyRing) <- getKeyRing keyRingName
        newAcc <- newAccount
            keyRingE newAccountAccountName newAccountType newAccountKeys
        return (keyRing, newAcc)
    -- Update the bloom filter if the account is complete
    whenOnline $ when (isCompleteAccount newAcc) updateNodeFilter
    return $ Just $ toJSON JsonWithKeyRing
        { withKeyRingKeyRing = toJsonKeyRing keyRing Nothing Nothing
        , withKeyRingData    = toJsonAccount newAcc
        }

getAccountR :: (MonadLogger m, MonadBaseControl IO m, MonadIO m)
            => KeyRingName -> AccountName -> Handler m (Maybe Value)
getAccountR keyRingName name = do
    $(logInfo) $ format $ unlines
        [ "GetAccountR"
        , "  KeyRing name: " ++ unpack keyRingName
        , "  Account name: " ++ unpack name
        ]
    (keyRing, Entity _ acc) <- runDB $ getAccount keyRingName name
    return $ Just $ toJSON JsonWithKeyRing
        { withKeyRingKeyRing = toJsonKeyRing keyRing Nothing Nothing
        , withKeyRingData    = toJsonAccount acc
        }

postAccountKeysR
    :: ( MonadResource m, MonadThrow m, MonadLogger m
       , MonadBaseControl IO m, MonadIO m
       )
    => KeyRingName -> AccountName -> [XPubKey] -> Handler m (Maybe Value)
postAccountKeysR keyRingName name keys = do
    $(logInfo) $ format $ unlines
        [ "PostAccountKeysR"
        , "  KeyRing name: " ++ unpack keyRingName
        , "  Account name: " ++ unpack name
        , "  Key count   : " ++ show (length keys)
        ]
    (keyRing, newAcc) <- runDB $ do
        (keyRing, accE) <- getAccount keyRingName name
        newAcc <- addAccountKeys accE keys
        return (keyRing, newAcc)
    -- Update the bloom filter if the account is complete
    whenOnline $ when (isCompleteAccount newAcc) updateNodeFilter
    return $ Just $ toJSON JsonWithKeyRing
        { withKeyRingKeyRing = toJsonKeyRing keyRing Nothing Nothing
        , withKeyRingData    = toJsonAccount newAcc
        }

postAccountGapR :: ( MonadLogger m
                   , MonadBaseControl IO m
                   , MonadBase IO m
                   , MonadIO m
                   , MonadThrow m
                   , MonadResource m
                   )
                => KeyRingName -> AccountName -> SetAccountGap
                -> Handler m (Maybe Value)
postAccountGapR keyRingName name (SetAccountGap gap) = do
    $(logInfo) $ format $ unlines
        [ "PostAccountGapR"
        , "  KeyRing name: " ++ unpack keyRingName
        , "  Account name: " ++ unpack name
        , "  New gap size: " ++ show gap
        ]
    -- Update the gap
    (keyRing, newAcc) <- runDB $ do
        (keyRing, accE) <- getAccount keyRingName name
        newAcc <- setAccountGap accE gap
        return (keyRing, newAcc)
    -- Update the bloom filter
    whenOnline updateNodeFilter
    return $ Just $ toJSON JsonWithKeyRing
        { withKeyRingKeyRing = toJsonKeyRing keyRing Nothing Nothing
        , withKeyRingData    = toJsonAccount newAcc
        }

getAddressesR :: (MonadLogger m, MonadBaseControl IO m, MonadIO m)
              => KeyRingName
              -> AccountName
              -> AddressType
              -> Word32
              -> Bool
              -> PageRequest
              -> Handler m (Maybe Value)
getAddressesR keyRingName name addrType minConf offline page = do
    $(logInfo) $ format $ unlines
        [ "GetAddressesR"
        , "  KeyRing name: " ++ unpack keyRingName
        , "  Account name: " ++ unpack name
        , "  Address type: " ++ show addrType
        , "  Page number : " ++ show (pageNum page)
        , "  Page size   : " ++ show (pageLen page)
        , "  Page reverse: " ++ show (pageReverse page)
        , "  MinConf     : " ++ show minConf
        , "  Offline     : " ++ show offline
        ]

    (keyRing, acc, res, bals, maxPage) <- runDB $ do
        (keyRing, accE@(Entity _ acc)) <- getAccount keyRingName name
        (res, maxPage) <- addressPage accE addrType page
        case res of
            [] -> return (keyRing, acc, res, [], maxPage)
            _ -> do
                let is = map keyRingAddrIndex res
                    (iMin, iMax) = (minimum is, maximum is)
                bals <- addressBalances accE iMin iMax addrType minConf offline
                return (keyRing, acc, res, bals, maxPage)

    -- Join addresses and balances together
    let g (addr, bal) = toJsonAddr addr (Just bal)
        addrBals = map g $ M.elems $ joinAddrs res bals
    return $ Just $ toJSON JsonWithAccount
        { withAccountKeyRing = toJsonKeyRing keyRing Nothing Nothing
        , withAccountAccount = toJsonAccount acc
        , withAccountData    = PageRes addrBals maxPage
        }
  where
    joinAddrs addrs bals =
        let f addr = (keyRingAddrIndex addr, addr)
        in  M.intersectionWith (,) (M.fromList $ map f addrs) (M.fromList bals)

getAddressesUnusedR :: (MonadLogger m, MonadBaseControl IO m, MonadIO m)
                    => KeyRingName -> AccountName -> AddressType
                    -> Handler m (Maybe Value)
getAddressesUnusedR keyRingName name addrType = do
    $(logInfo) $ format $ unlines
        [ "GetAddressesUnusedR"
        , "  KeyRing name: " ++ unpack keyRingName
        , "  Account name: " ++ unpack name
        , "  Address type: " ++ show addrType
        ]

    (keyRing, acc, addrs) <- runDB $ do
        (keyRing, accE@(Entity _ acc)) <- getAccount keyRingName name
        addrs <- unusedAddresses accE addrType
        return (keyRing, acc, addrs)

    return $ Just $ toJSON JsonWithAccount
        { withAccountKeyRing = toJsonKeyRing keyRing Nothing Nothing
        , withAccountAccount = toJsonAccount acc
        , withAccountData    = map (`toJsonAddr` Nothing) addrs
        }

getAddressR :: (MonadLogger m, MonadBaseControl IO m, MonadIO m)
            => KeyRingName -> AccountName -> KeyIndex -> AddressType
            -> Word32 -> Bool
            -> Handler m (Maybe Value)
getAddressR keyRingName name i addrType minConf offline = do
    $(logInfo) $ format $ unlines
        [ "GetAddressR"
        , "  KeyRing name: " ++ unpack keyRingName
        , "  Account name: " ++ unpack name
        , "  Index       : " ++ show i
        , "  Address type: " ++ show addrType
        ]

    (keyRing, acc, addr, balM) <- runDB $ do
        (keyRing, accE@(Entity _ acc)) <- getAccount keyRingName name
        addrE <- getAddress accE addrType i
        bals <- addressBalances accE i i addrType minConf offline
        return $ case bals of
            ((_,bal):_) -> (keyRing, acc, entityVal addrE, Just bal)
            _           -> (keyRing, acc, entityVal addrE, Nothing)
    return $ Just $ toJSON JsonWithAccount
        { withAccountKeyRing = toJsonKeyRing keyRing Nothing Nothing
        , withAccountAccount = toJsonAccount acc
        , withAccountData    = toJsonAddr addr balM
        }

putAddressR :: (MonadLogger m, MonadBaseControl IO m, MonadIO m)
            => KeyRingName
            -> AccountName
            -> KeyIndex
            -> AddressType
            -> AddressLabel
            -> Handler m (Maybe Value)
putAddressR keyRingName name i addrType (AddressLabel label) = do
    $(logInfo) $ format $ unlines
        [ "PutAddressR"
        , "  KeyRing name: " ++ unpack keyRingName
        , "  Account name: " ++ unpack name
        , "  Index       : " ++ show i
        , "  Label       : " ++ unpack label
        ]

    (keyRing, acc, newAddr) <- runDB $ do
        (keyRing, accE@(Entity _ acc)) <- getAccount keyRingName name
        newAddr <- setAddrLabel accE i addrType label
        return (keyRing, acc, newAddr)

    return $ Just $ toJSON JsonWithAccount
        { withAccountKeyRing = toJsonKeyRing keyRing Nothing Nothing
        , withAccountAccount = toJsonAccount acc
        , withAccountData    = toJsonAddr newAddr Nothing
        }

postAddressesR :: ( MonadLogger m
                  , MonadBaseControl IO m
                  , MonadIO m
                  , MonadThrow m
                  , MonadBase IO m
                  , MonadResource m
                  )
               => KeyRingName
               -> AccountName
               -> KeyIndex
               -> AddressType
               -> Handler m (Maybe Value)
postAddressesR keyRingName name i addrType = do
    $(logInfo) $ format $ unlines
        [ "PostAddressesR"
        , "  KeyRing name: " ++ unpack keyRingName
        , "  Account name: " ++ unpack name
        , "  Index       : " ++ show i
        ]

    (keyRing, acc, cnt) <- runDB $ do
        (keyRing, accE@(Entity _ acc)) <- getAccount keyRingName name
        cnt <- generateAddrs accE addrType i
        return (keyRing, acc, cnt)

    -- Update the bloom filter
    whenOnline updateNodeFilter

    return $ Just $ toJSON JsonWithAccount
        { withAccountKeyRing = toJsonKeyRing keyRing Nothing Nothing
        , withAccountAccount = toJsonAccount acc
        , withAccountData    = cnt
        }

getTxsR :: (MonadLogger m, MonadBaseControl IO m, MonadIO m)
        => KeyRingName -> AccountName -> PageRequest -> Handler m (Maybe Value)
getTxsR keyRingName name page = do
    $(logInfo) $ format $ unlines
        [ "GetTxsR"
        , "  KeyRing name: " ++ unpack keyRingName
        , "  Account name: " ++ unpack name
        , "  Page number : " ++ show (pageNum page)
        , "  Page size   : " ++ show (pageLen page)
        , "  Page reverse: " ++ show (pageReverse page)
        ]

    (keyRing, acc, res, maxPage, height) <- runDB $ do
        (keyRing, Entity ai acc) <- getAccount keyRingName name
        (_, height) <- getBestBlock
        (res, maxPage) <- txPage ai page
        return (keyRing, acc, res, maxPage, height)

    return $ Just $ toJSON JsonWithAccount
        { withAccountKeyRing = toJsonKeyRing keyRing Nothing Nothing
        , withAccountAccount = toJsonAccount acc
        , withAccountData =
            PageRes (map (`toJsonTx` Just height) res) maxPage
        }

getAddrTxsR :: (MonadLogger m, MonadBaseControl IO m, MonadIO m)
            => KeyRingName -> AccountName
            -> KeyIndex -> AddressType -> PageRequest
            -> Handler m (Maybe Value)
getAddrTxsR keyRingName name index addrType page = do
    $(logInfo) $ format $ unlines
        [ "GetAddrTxsR"
        , "  KeyRing name : " ++ unpack keyRingName
        , "  Account name : " ++ unpack name
        , "  Address index: " ++ show index
        , "  Address type : " ++ show addrType
        , "  Page number  : " ++ show (pageNum page)
        , "  Page size    : " ++ show (pageLen page)
        , "  Page reverse : " ++ show (pageReverse page)
        ]

    (keyRing, acc, res, maxPage, height, addr) <- runDB $ do
        (keyRing, accE@(Entity _ acc)) <- getAccount keyRingName name
        Entity addrI addr <- getAddress accE addrType index
        (_, height) <- getBestBlock
        (res, maxPage) <- addrTxPage accE addrI page
        return (keyRing, acc, res, maxPage, height, addr)

    return $ Just $ toJSON JsonWithAddr
        { withAddrKeyRing = toJsonKeyRing keyRing Nothing Nothing
        , withAddrAccount = toJsonAccount acc
        , withAddrAddress = toJsonAddr addr Nothing
        , withAddrData    = PageRes (map (f height) res) maxPage
        }
  where
    f height (tx, bal) = AddrTx (toJsonTx tx (Just height)) bal

postTxsR :: ( MonadLogger m, MonadBaseControl IO m, MonadBase IO m
            , MonadIO m, MonadThrow m, MonadResource m
            )
         => KeyRingName -> AccountName -> TxAction -> Handler m (Maybe Value)
postTxsR keyRingName name action = do
    (keyRing, accE@(Entity ai acc), height) <- runDB $ do
        (keyRing, accE) <- getAccount keyRingName name
        (_, height) <- getBestBlock
        return (keyRing, accE, height)

    (txRes, newAddrs) <- case action of
        CreateTx rs fee minconf rcptFee sign -> do
            $(logInfo) $ format $ unlines
                [ "PostTxsR CreateTx"
                , "  KeyRing name: " ++ unpack keyRingName
                , "  Account name: " ++ unpack name
                , "  Recipients  : " ++ show (map (first addrToBase58) rs)
                , "  Fee         : " ++ show fee
                , "  Minconf     : " ++ show minconf
                , "  Rcpt. Fee   : " ++ show rcptFee
                , "  Sign        : " ++ show sign
                ]
            runDB $ createTx keyRing accE rs fee minconf rcptFee sign
        ImportTx tx -> do
            $(logInfo) $ format $ unlines
                [ "PostTxsR ImportTx"
                , "  KeyRing name: " ++ unpack keyRingName
                , "  Account name: " ++ unpack name
                , "  Txid        : " ++ cs (txHashToHex (txHash tx))
                ]
            runDB $ do
                (res, newAddrs) <- importTx tx ai
                case filter ((== ai) . keyRingTxAccount) res of
                    (txRes:_) -> return (txRes, newAddrs)
                    _ -> liftIO . throwIO $ WalletException
                        "Could not import the transaction"
        SignTx txid -> do
            $(logInfo) $ format $ unlines
                [ "PostTxsR SignTx"
                , "  KeyRing name: " ++ unpack keyRingName
                , "  Account name: " ++ unpack name
                , "  Txid        : " ++ cs (txHashToHex txid)
                ]
            runDB $ do
                (res, newAddrs) <- signKeyRingTx keyRing accE txid
                case filter ((== ai) . keyRingTxAccount) res of
                    (txRes:_) -> return (txRes, newAddrs)
                    _ -> liftIO . throwIO $ WalletException
                        "Could not import the transaction"
    whenOnline $ do
        -- Update the bloom filter
        unless (null newAddrs) updateNodeFilter
        -- If the transaction is pending, broadcast it to the network
        when (keyRingTxConfidence txRes == TxPending) $
            runNode $ broadcastTxs [keyRingTxHash txRes]
    return $ Just $ toJSON JsonWithAccount
        { withAccountKeyRing = toJsonKeyRing keyRing Nothing Nothing
        , withAccountAccount = toJsonAccount acc
        , withAccountData    = toJsonTx txRes (Just height)
        }

getTxR :: (MonadLogger m, MonadBaseControl IO m, MonadIO m)
       => KeyRingName -> AccountName -> TxHash -> Handler m (Maybe Value)
getTxR keyRingName name txid = do
    $(logInfo) $ format $ unlines
        [ "GetTxR"
        , "  KeyRing name: " ++ unpack keyRingName
        , "  Account name: " ++ unpack name
        , "  Txid        : " ++ cs (txHashToHex txid)
        ]
    (keyRing, acc, res, height) <- runDB $ do
        (keyRing, Entity ai acc) <- getAccount keyRingName name
        (_, height) <- getBestBlock
        res <- getAccountTx ai txid
        return (keyRing, acc, res, height)
    return $ Just $ toJSON JsonWithAccount
        { withAccountKeyRing = toJsonKeyRing keyRing Nothing Nothing
        , withAccountAccount = toJsonAccount acc
        , withAccountData    = toJsonTx res (Just height)
        }

getBalanceR :: (MonadLogger m, MonadBaseControl IO m, MonadIO m)
            => KeyRingName -> AccountName -> Word32 -> Bool
            -> Handler m (Maybe Value)
getBalanceR keyRingName name minconf offline = do
    $(logInfo) $ format $ unlines
        [ "GetBalanceR"
        , "  KeyRing name: " ++ unpack keyRingName
        , "  Account name: " ++ unpack name
        , "  Minconf     : " ++ show minconf
        , "  Offline     : " ++ show offline
        ]
    (keyRing, acc, bal) <- runDB $ do
        (keyRing, Entity ai acc) <- getAccount keyRingName name
        bal <- accountBalance ai minconf offline
        return (keyRing, acc, bal)
    return $ Just $ toJSON JsonWithAccount
        { withAccountKeyRing = toJsonKeyRing keyRing Nothing Nothing
        , withAccountAccount = toJsonAccount acc
        , withAccountData    = bal
        }

getOfflineTxR :: ( MonadLogger m, MonadIO m, MonadBaseControl IO m
                 , MonadBase IO m, MonadThrow m, MonadResource m
                 )
              => KeyRingName -> AccountName -> TxHash -> Handler m (Maybe Value)
getOfflineTxR keyRingName accountName txid = do
    $(logInfo) $ format $ unlines
        [ "GetOfflineTxR"
        , "  KeyRing name: " ++ unpack keyRingName
        , "  Account name: " ++ unpack accountName
        , "  Txid        : " ++ cs (txHashToHex txid)
        ]
    (dat, _) <- runDB $ do
        (_, Entity ai _) <- getAccount keyRingName accountName
        getOfflineTxData ai txid
    return $ Just $ toJSON dat

postOfflineTxR :: ( MonadLogger m, MonadIO m, MonadBaseControl IO m
                  , MonadBase IO m, MonadThrow m, MonadResource m
                  )
               => KeyRingName
               -> AccountName
               -> Tx
               -> [CoinSignData]
               -> Handler m (Maybe Value)
postOfflineTxR keyRingName accountName tx signData = do
    $(logInfo) $ format $ unlines
        [ "PostTxsR SignOfflineTx"
        , "  KeyRing name: " ++ unpack keyRingName
        , "  Account name: " ++ unpack accountName
        , "  Txid        : " ++ cs (txHashToHex (txHash tx))
        ]
    (keyRing, Entity _ acc) <- runDB $ getAccount keyRingName accountName
    let signedTx = signOfflineTx keyRing acc tx signData
        complete = verifyStdTx signedTx $ map toDat signData
        toDat CoinSignData{..} = (coinSignScriptOutput, coinSignOutPoint)
    return $ Just $ toJSON $ TxCompleteRes signedTx complete

postNodeR :: (MonadLogger m, MonadBaseControl IO m, MonadIO m)
          => NodeAction -> Handler m (Maybe Value)
postNodeR action = case action of
    NodeActionRescan tM -> do
        t <- case tM of
            Just t  -> return $ adjustFCTime t
            Nothing -> do
                timeM <- runDB firstAddrTime
                maybe err (return . adjustFCTime) timeM
        $(logInfo) $ format $ unlines
            [ "NodeR Rescan"
            , "  Timestamp: " ++ show t
            ]
        whenOnline $ do
            runDB resetRescan
            runNode $ atomicallyNodeT $ rescanTs t
        return $ Just $ toJSON $ RescanRes t
    NodeActionStatus -> do
        status <- runNode $ atomicallyNodeT nodeStatus
        return $ Just $ toJSON status
  where
    err = liftIO . throwIO $ WalletException
        "No keys have been generated in the wallet"

{- Helpers -}

whenOnline :: Monad m => Handler m () -> Handler m ()
whenOnline handler = do
    mode <- configMode `liftM` S.gets handlerConfig
    when (mode == SPVOnline) handler

updateNodeFilter :: (MonadBaseControl IO m, MonadIO m, MonadLogger m)
                 => Handler m ()
updateNodeFilter = do
    $(logInfo) $ format "Sending a new bloom filter"
    (bloom, elems, _) <- runDB getBloomFilter
    runNode $ atomicallyNodeT $ sendBloomFilter bloom elems

adjustFCTime :: Timestamp -> Timestamp
adjustFCTime ts = fromInteger $ max 0 $ toInteger ts - 86400 * 7

format :: String -> Text
format str = pack $ "[ZeroMQ] " ++ str