packages feed

bitcoin-rpc-0.5.0.0: Network/BitcoinRPC/Events.hs

{-# LANGUAGE DeriveGeneric #-}
-- |
-- This module provides a mechanism by which a client of the library can
-- subscribe to new incoming Bitcoin transactions. To facilitate this, the
-- library requires an external tool to send USR1 or USR2 signals whenever
-- wallet activity takes place. The library will write its process id to a
-- provided file for this purpose. An external tool like PidNotifier (
-- <https://github.com/javgh/PidNotifier> ) can then be used in combination with
-- the -walletnotify feature of the Satoshi Bitcoin daemon to notify the
-- library. The library will then use commands like listsinceblock and an
-- internal state to figure out which transactions are new, if any, and provide
-- those to the subscribed client.
--
-- How to use:
--
-- 1. Use 'initBitcoinEventTask' to start a thread which will listen for Bitcoin
--    events.
--
-- 2. Use 'waitForBitcoinEvents' to listen for event updates. Every batch of
--    updates will also include a new state description, which can be passed to
--    'initBitcoinEventTask' when it needs to be restarted.
--
-- Note: For every new transaction you are guaranteed to receive a
-- 'NewTransaction' as well as either a 'TransactionAccepted' or
-- 'TransactionDisappeared'. Usually you will also receive 'TransactionUpdate'
-- when the number of confirmations change, but these events might not be
-- generated if you are catching up from an old state.
--
-- Example usage:
--
-- > module Main where
-- >
-- > import Control.Monad
-- > import System.Environment
-- > import System.Exit
-- >
-- > import Network.BitcoinRPC
-- > import Network.BitcoinRPC.Events
-- >
-- > acceptTest :: TransactionHeader -> Bool
-- > acceptTest txHeader = thConfirmations txHeader >= 3
-- >
-- > rpcAuth :: RPCAuth
-- > rpcAuth = RPCAuth "http://127.0.0.1:8332" "rpcuser" "localaccessonly"
-- >
-- > main :: IO ()
-- > main = do
-- >     args <- getArgs
-- >     progName <- getProgName
-- >     when (null args) $ do
-- >         putStrLn $ "Usage: " ++ progName ++ " <path to notify pid file>"
-- >         exitFailure
-- >     let pidfile = head args
-- >
-- >     betHandle <- initBitcoinEventTask Nothing rpcAuth pidfile
-- >                                         acceptTest initialEventTaskState
-- >     forever $ do
-- >         (_, events) <- waitForBitcoinEvents betHandle
-- >         mapM print events
--
-- Example output:
--
-- @
-- NewTransaction {beUTxID = UniqueTransactionID {uTxID = TransactionID {btcTxID =
-- "fc96f113e87a031de7d6e066613f216cfb18f37da9cade0c78cfd7a81272124e"}, uEntry =
-- 0}, beTx = ReceiveTx {tEntry = 0, tAmount = BitcoinAmount {btcAmount = 20741},
-- tAddress = BitcoinAddress {btcAddress = "1ATeVDCQHjEePi7R66ivSTpz3SdkbyCV2a"},
-- tConfirmations = 1867, tTxid = TransactionID {btcTxID =
-- "fc96f113e87a031de7d6e066613f216cfb18f37da9cade0c78cfd7a81272124e"}, tTime =
-- 1387825867}, beOrigins = [BitcoinAddress {btcAddress =
-- "1NhPxwJ6mCr5auoQzzqr4qVd1w65zzJw8r"}]}
-- TransactionAccepted {beUTxID = UniqueTransactionID {uTxID = TransactionID
-- {btcTxID = "fc96f113e87a031de7d6e066613f216cfb18f37da9cade0c78cfd7a81272124e"},
-- uEntry = 0}}
-- @
--

{-# LANGUAGE OverloadedStrings, CPP #-}
module Network.BitcoinRPC.Events
    ( initialEventTaskState
    , initBitcoinEventTask
    , waitForBitcoinEvents
    , killBitcoinEventTask
    , BitcoinEvent(..)
    , UniqueTransactionID(..)
    , EventTaskState
    , BitcoinEventTaskHandle
#if !PRODUCTION
    , LSBCheckpoint(..)
    , determineNewTransactions
#endif
    ) where

import Control.Concurrent
import Control.Watchdog
import Data.Serialize
import GHC.Generics
import System.Posix.Process
import System.Posix.Signals

import qualified Data.Map as M

import Network.BitcoinRPC

-- | How deep in the block chain does a block need to be so that it
-- is considered final - i.e. will not change because of a re-org.
lsbBlockDepth :: Integer
lsbBlockDepth = 100

data BitcoinEventTaskHandle = BitcoinEventTaskHandle
                                { bethChan ::
                                    Chan (EventTaskState, [BitcoinEvent])
                                , bethThreadId :: ThreadId
                                }

-- | Create identifier that can differentiate between the outgoing and incoming
-- side of the same transaction id.  Slightly confusing as this means that a
-- single Bitcoin transaction can result in multiple unique transactions.
data UniqueTransactionID = UniqueTransactionID { uTxID :: TransactionID
                                               , uEntry :: Integer
                                               }
                           deriving (Eq, Ord, Show, Read, Generic)

-- | Keep track on how we need to call 'listsinceblock' next to get
-- everything new that happened.
data LSBCheckpoint = LSBCheckpoint { lsbBlockHash :: Maybe BlockHash
                                   , lsbKnownTxIDs :: [TransactionID]
                                   }
                     deriving (Eq,Show,Read,Generic)

-- | Keeps track of confirmation count for recent unique transactions.
type UTXPool = M.Map UniqueTransactionID Integer

data EventTaskState = EventTaskState { etsLSBCheckpoint :: LSBCheckpoint
                                     , etsPool :: UTXPool
                                     }
                      deriving (Show,Generic)

data BitcoinEvent = NewTransaction { beUTxID :: UniqueTransactionID
                                   , beTx :: Transaction
                                   , beOrigins :: [BitcoinAddress]
                                   }
                  | TransactionUpdate { beUTxID :: UniqueTransactionID
                                      , beConfirmations :: Integer
                                      }
                  | TransactionAccepted { beUTxID :: UniqueTransactionID }
                  | TransactionDisappeared { beUTxID :: UniqueTransactionID }
                  deriving (Show)

instance Serialize UniqueTransactionID

instance Serialize LSBCheckpoint

instance Serialize EventTaskState

initialEventTaskState :: EventTaskState
initialEventTaskState = EventTaskState { etsLSBCheckpoint =
                                            LSBCheckpoint Nothing []
                                       , etsPool = M.empty
                                       }

getUniqueTransactionID :: Transaction -> UniqueTransactionID
getUniqueTransactionID tx = UniqueTransactionID (tTxid tx) (tEntry tx)

onlyReceive :: Transaction -> Bool
onlyReceive ReceiveTx{} = True
onlyReceive _ = False

getNewBitcoinEvents :: Maybe WatchdogLogger-> RPCAuth-> (TransactionHeader -> Bool)-> EventTaskState-> IO (EventTaskState, [BitcoinEvent])
getNewBitcoinEvents mLogger auth acceptTest (EventTaskState lsbCheckpoint utxPool) = do
    (lsbCheckpoint', newTxs) <- getNewTransactions mLogger auth lsbCheckpoint
    let newReceiveTxs = filter onlyReceive newTxs
        newPoolEntries = map (\tx -> (getUniqueTransactionID tx, 0)) newReceiveTxs
        utxPool' = utxPool `M.union` M.fromList newPoolEntries
    appearingEvents <- mapM (augmentNewTransaction mLogger auth) newReceiveTxs
    (utxPool'', updateEvents) <- updatePool mLogger auth acceptTest utxPool'
    let newState = EventTaskState lsbCheckpoint' utxPool''
    return (newState, appearingEvents ++ updateEvents)

updatePool :: Maybe WatchdogLogger-> RPCAuth-> (TransactionHeader -> Bool)-> M.Map UniqueTransactionID Integer-> IO (M.Map UniqueTransactionID Integer, [BitcoinEvent])
updatePool mLogger auth acceptTest utxPool = go utxPool (M.toList utxPool)
  where
    go pool [] = return (pool, [])
    go pool ((utxid, confs):entries) = do
        -- Note: This will sometimes make redundant calls to gettransaction,
        --       but keeps things a little simpler.
        probe <- getTransactionR mLogger auth (uTxID utxid)
        let (pool', events) = checkUpdate acceptTest pool utxid confs probe
        (pool'', moreEvents) <- go pool' entries
        return (pool'', events ++ moreEvents)

checkUpdate :: (TransactionHeader -> Bool)-> M.Map UniqueTransactionID Integer-> UniqueTransactionID-> Integer-> Maybe TransactionHeader-> (M.Map UniqueTransactionID Integer, [BitcoinEvent])
checkUpdate acceptTest pool utxid confs (Just updatedTxHeader)
    | thConfirmations updatedTxHeader == confs = (pool, [])
    | otherwise = if acceptTest updatedTxHeader
                    then (M.delete utxid pool, [TransactionAccepted utxid])
                    else let updatedConfs = thConfirmations updatedTxHeader
                         in (M.insert utxid updatedConfs pool,
                                [TransactionUpdate utxid updatedConfs])
checkUpdate _ pool utxid _ Nothing =
    (M.delete utxid pool, [TransactionDisappeared utxid])

augmentNewTransaction :: Maybe WatchdogLogger -> RPCAuth -> Transaction -> IO BitcoinEvent
augmentNewTransaction mLogger auth tx = do
    let txid = tTxid tx
        utxid = getUniqueTransactionID tx
    probe <- getOriginsR mLogger auth txid
    return $ case probe of
        Just txOrigins -> NewTransaction utxid tx (toOrigins txOrigins)
        Nothing -> NewTransaction utxid tx []

getNewTransactions :: Maybe WatchdogLogger -> RPCAuth -> LSBCheckpoint -> IO (LSBCheckpoint, [Transaction])
getNewTransactions mLogger auth lsbCheckpoint = do
    -- get (possibly) new transactions
    SinceBlockInfo txs _ <- listSinceBlockR mLogger auth (lsbBlockHash lsbCheckpoint)
    -- pick a block somewhat deep in the blockchain for the next query
    blockCount <- getBlockCountR mLogger auth
    let safeBlockHeight = max 0 (blockCount - lsbBlockDepth)
    safeBlockHash <- getBlockHashR mLogger auth safeBlockHeight
    let lsbCheckpoint' = lsbCheckpoint { lsbBlockHash = Just safeBlockHash }
    return $ determineNewTransactions lsbCheckpoint' txs

-- | Figure out which are actual new transactions and update
-- pool of known transaction ids. We simply keep the current list of
-- transactions returned by listsinceblock and check against it the next time.
-- This should work, as long as transactions never reappear after the have
-- dropped of the list. This should be true, as long as blockcount is always
-- increasing.
determineNewTransactions :: LSBCheckpoint -> [Transaction] -> (LSBCheckpoint, [Transaction])
determineNewTransactions (LSBCheckpoint mBlockHash knownTxIDs) txList =
    let newTransactions = filter (\tx -> tTxid tx `notElem` knownTxIDs) txList
        knownTxIDs' = map tTxid txList
    in ((LSBCheckpoint mBlockHash knownTxIDs'), newTransactions)

setupBitcoinNotifcation :: FilePath -> IO (MVar ())
setupBitcoinNotifcation path = do
    pid <- getProcessID
    writeFile path (show pid ++ "\n")
    installSignalHandlers

installSignalHandlers :: IO (MVar ())
installSignalHandlers = do
    semaphore <- newMVar ()
    _ <- installHandler userDefinedSignal1 (Catch $ signalHandler semaphore) Nothing
    _ <- installHandler userDefinedSignal2 (Catch $ signalHandler semaphore) Nothing
    return semaphore
  where
    signalHandler semaphore = putMVar semaphore ()

notifiedPollLoop :: MVar ()-> Maybe WatchdogLogger-> RPCAuth-> (TransactionHeader -> Bool)-> EventTaskState-> Chan (EventTaskState, [BitcoinEvent])-> IO ()
notifiedPollLoop semaphore mLogger auth acceptTest firstState chan = go firstState
  where
    go state = do
        _ <- takeMVar semaphore     -- Will succeed right away the first time,
                                    -- then later only after signals have been
                                    -- received.
        (state', events) <- getNewBitcoinEvents mLogger auth acceptTest state
        writeChan chan (state', events)
        go state'

initBitcoinEventTask :: Maybe WatchdogLogger
                     -> RPCAuth
                     -> FilePath    -- ^  PID will be written here to get
                                    -- notifications from Bitcoin daemon
                     -> (TransactionHeader -> Bool)
                                    -- ^ test to decide whether a transaction
                                    -- can be accepted
                     -> EventTaskState  -- ^ state from which to resume
                     -> IO (BitcoinEventTaskHandle)
initBitcoinEventTask mLogger auth pidfile acceptTest state = do
    chan <- newChan
    semaphore <- setupBitcoinNotifcation pidfile
    threadId <-
        forkIO $ notifiedPollLoop semaphore mLogger auth acceptTest state chan
    return $ BitcoinEventTaskHandle chan threadId

-- | Wait for new Bitcoin Events. This might sometimes return an empty list of
-- events.
waitForBitcoinEvents :: BitcoinEventTaskHandle -> IO (EventTaskState, [BitcoinEvent])
waitForBitcoinEvents betHandle = readChan (bethChan betHandle)

killBitcoinEventTask :: BitcoinEventTaskHandle -> IO ()
killBitcoinEventTask betHandle = killThread (bethThreadId betHandle)