bitcoin-rpc-0.5.0.0: Network/BitcoinRPC/MarkerAddresses.hs
{-# LANGUAGE CPP, DeriveGeneric, OverloadedStrings #-}
module Network.BitcoinRPC.MarkerAddresses
( initMarkerAddressStore
, updateMarkerAddresses
, processEvents
, listPendingTransactions
, listMarkerAdressStatus
, MAStore
, PendingReason(..)
, FilteredBitcoinEvent(..)
#if !PRODUCTION
, sumAcceptedMarkerAmounts
#endif
) where
import Data.Function
import Data.List
import Data.Maybe
import Data.Ord
import Data.Serialize
import GHC.Generics
import qualified Data.Map as M
import Network.BitcoinRPC
import Network.BitcoinRPC.Events
data FilteredBitcoinEvent = FilteredNewTransaction { fntTx :: Transaction
, fntConfs :: Integer
, fntMarkerAddress ::
Maybe BitcoinAddress
}
| MarkerAddressBreached { fntTx :: Transaction
, fntBreachedMarkerAddress ::
BitcoinAddress
}
deriving (Show)
data PendingReason = TooFewConfirmations { prConfs :: Integer }
| MarkerAddressLimitReached { prMarkerAddress :: BitcoinAddress }
deriving (Show)
data MarkerAddressDetails = MarkerAddressDetails { madActive :: Bool
, madLimit :: BitcoinAmount
, madPendingAmount :: BitcoinAmount
}
deriving (Show, Generic)
type MarkerAddressesConf = M.Map BitcoinAddress MarkerAddressDetails
data PendingStatus = StandardTransaction
| PendingMarkerTransaction { _psMarkerAddress :: BitcoinAddress }
| AcceptedMarkerTransaction { _psMarkerAddress :: BitcoinAddress }
deriving (Show, Generic)
data PendingTransaction = PendingTransaction { ptTx :: Transaction
, ptConfs :: Integer
, ptStatus :: PendingStatus
}
deriving (Show, Generic)
type PendingTransactions = M.Map UniqueTransactionID PendingTransaction
data MAStore = MAStore { masConf :: MarkerAddressesConf
, masPending :: PendingTransactions
}
deriving (Show, Generic)
instance Serialize MarkerAddressDetails
instance Serialize PendingTransaction
instance Serialize PendingStatus
instance Serialize MAStore
initMarkerAddressStore :: [(BitcoinAddress, BitcoinAmount)] -> MAStore
initMarkerAddressStore markerAddresses =
let confList = map transformMarkerAddresses markerAddresses
in MAStore (M.fromList confList) M.empty
updateMarkerAddresses :: MAStore -> [(BitcoinAddress, BitcoinAmount)] -> MAStore
updateMarkerAddresses store markerAddresses =
let confList = map transformMarkerAddresses markerAddresses
in store { masConf = M.fromList confList }
transformMarkerAddresses :: (BitcoinAddress, BitcoinAmount) -> (BitcoinAddress, MarkerAddressDetails)
transformMarkerAddresses (addr, limit) =
(addr, MarkerAddressDetails True limit 0)
listMarkerAdressStatus :: MAStore -> [(BitcoinAddress, Bool, BitcoinAmount, BitcoinAmount)]
listMarkerAdressStatus store = map format $ M.toList (masConf store)
where
format (ma, MarkerAddressDetails active limit pendingAmount) =
(ma, active, limit, pendingAmount)
listPendingTransactions :: MAStore -> [(Transaction, PendingReason)]
listPendingTransactions store =
let pendingTransactions = concatMap format $ M.toList (masPending store)
in sortBy (comparing (tTime . fst)) pendingTransactions
where
format (_, PendingTransaction tx confs status) =
case status of
StandardTransaction -> [(tx, TooFewConfirmations confs)]
PendingMarkerTransaction ma -> [(tx, MarkerAddressLimitReached ma)]
AcceptedMarkerTransaction _ -> []
processEvents :: MAStore -> [BitcoinEvent] -> (MAStore, [FilteredBitcoinEvent])
processEvents store events =
let (store', fEventsA) = foldl' updateStore (store, []) events
amountList = sumAcceptedMarkerAmounts . listAcceptedMarkerAmounts $ store'
store'' = setPendingMarkerAmounts store' amountList
(store''', fEventsB) = checkPendingMarkerTransactions store''
in (store''', fEventsA ++ fEventsB)
checkPendingMarkerTransactions :: MAStore -> (MAStore, [FilteredBitcoinEvent])
checkPendingMarkerTransactions store =
let txs = M.toList (masPending store)
in foldl' checkPendingMarkerTransaction (store, []) txs
checkPendingMarkerTransaction :: (MAStore, [FilteredBitcoinEvent])-> (UniqueTransactionID, PendingTransaction)-> (MAStore, [FilteredBitcoinEvent])
checkPendingMarkerTransaction (store, fevents) (utxid, pendingTransaction) =
case ptStatus pendingTransaction of
StandardTransaction -> (store, fevents)
AcceptedMarkerTransaction _ -> (store, fevents)
PendingMarkerTransaction ma ->
let tx = ptTx pendingTransaction
amount = tAmount tx
in if isWithinLimit store ma amount
then let store' = adjustPendingAmount store ma (+ amount)
pendingTransaction' =
pendingTransaction { ptStatus =
AcceptedMarkerTransaction ma }
masPending' = M.insert utxid pendingTransaction'
(masPending store')
confs = ptConfs pendingTransaction
fEvent = FilteredNewTransaction tx confs (Just ma)
in (store' { masPending = masPending' }, fevents ++ [fEvent])
else (store, fevents)
isWithinLimit :: MAStore -> BitcoinAddress -> BitcoinAmount -> Bool
isWithinLimit store markerAddress amount =
case M.lookup markerAddress (masConf store) of
Nothing -> False
Just details ->
madActive details && madPendingAmount details + amount <= madLimit details
setPendingMarkerAmounts :: MAStore -> [(BitcoinAddress, BitcoinAmount)] -> MAStore
setPendingMarkerAmounts = foldl' go
where
go store (ma, amount) = adjustPendingAmount store ma (\_ -> amount)
adjustPendingAmount :: MAStore-> BitcoinAddress -> (BitcoinAmount -> BitcoinAmount) -> MAStore
adjustPendingAmount store ma f =
let mDetails = M.lookup ma (masConf store)
in case mDetails of
Nothing -> store
Just details ->
let pendingAmount = madPendingAmount details
details' = details { madPendingAmount = f pendingAmount }
masConf' = M.insert ma details' (masConf store)
in store { masConf = masConf' }
listAcceptedMarkerAmounts :: MAStore -> [(BitcoinAddress, BitcoinAmount)]
listAcceptedMarkerAmounts store =
let zeroes = map (\(ma, _) -> (ma, 0)) $ M.toList (masConf store)
amounts = concatMap extract $ M.toList (masPending store)
in zeroes ++ amounts
where
extract (_, pendingTransaction) =
case ptStatus pendingTransaction of
StandardTransaction -> []
PendingMarkerTransaction _ -> []
AcceptedMarkerTransaction ma ->
[(ma, tAmount (ptTx pendingTransaction))]
sumAcceptedMarkerAmounts :: [(BitcoinAddress, BitcoinAmount)] -> [(BitcoinAddress, BitcoinAmount)]
sumAcceptedMarkerAmounts =
map (foldl1' sumAmounts) . filter (not . null) .
groupBy ((==) `on` fst) . sortBy (comparing fst)
where
sumAmounts :: (BitcoinAddress, BitcoinAmount) -> (BitcoinAddress, BitcoinAmount) -> (BitcoinAddress, BitcoinAmount)
sumAmounts (ma1, x) (ma2, y) =
if ma1 == ma2
then (ma1, x + y)
else error ("Implementation error: Expected to be summing"
++ " data for the same marker address.")
updateStore :: (MAStore, [FilteredBitcoinEvent])-> BitcoinEvent -> (MAStore, [FilteredBitcoinEvent])
updateStore (store, fEvents) (NewTransaction utxid tx origins) =
let pendingTransaction =
case isFromMarkerAddress (masConf store) origins of
Nothing -> PendingTransaction tx 0 StandardTransaction
Just origin -> PendingTransaction tx 0
(PendingMarkerTransaction origin)
masPending' = M.insert utxid pendingTransaction (masPending store)
in (store { masPending = masPending' }, fEvents)
updateStore (store, fEvents) (TransactionUpdate utxid confs) =
let pendingTransaction = lookupPendingTransaction store utxid
pendingTransaction' = pendingTransaction { ptConfs = confs }
masPending' = M.insert utxid pendingTransaction' (masPending store)
in (store { masPending = masPending' }, fEvents)
updateStore (store, fEvents) (TransactionAccepted utxid) =
let pendingTransaction = lookupPendingTransaction store utxid
tx = ptTx pendingTransaction
confs = ptConfs pendingTransaction
masPending' = M.delete utxid (masPending store)
maybeFEvents = case ptStatus pendingTransaction of
StandardTransaction ->
[FilteredNewTransaction tx confs Nothing]
PendingMarkerTransaction psMA ->
[FilteredNewTransaction tx confs (Just psMA)]
AcceptedMarkerTransaction _ ->
[] -- has already been returned earlier
in (store { masPending = masPending' }, fEvents ++ maybeFEvents)
updateStore (store, _) (TransactionDisappeared utxid) =
let pendingTransaction = lookupPendingTransaction store utxid
(store', maybeFEvents) =
case ptStatus pendingTransaction of
StandardTransaction -> (store, [])
PendingMarkerTransaction address ->
( disableMarkerAddress store address
, [MarkerAddressBreached
(ptTx pendingTransaction) address]
)
AcceptedMarkerTransaction address ->
( disableMarkerAddress store address
, [MarkerAddressBreached
(ptTx pendingTransaction) address]
)
masPending' = M.delete utxid (masPending store')
in (store' { masPending = masPending' }, maybeFEvents)
disableMarkerAddress :: MAStore -> BitcoinAddress -> MAStore
disableMarkerAddress store address =
let details = lookupMarkerAddressDetails store address
details' = details { madActive = False }
masConf' = M.insert address details' (masConf store)
in store { masConf = masConf' }
lookupMarkerAddressDetails :: MAStore -> BitcoinAddress -> MarkerAddressDetails
lookupMarkerAddressDetails store address =
fromMaybe (error ("Inconsistency: Failed to lookup details"
++ " for a marker address that is in use."))
(M.lookup address (masConf store))
lookupPendingTransaction :: MAStore -> UniqueTransactionID -> PendingTransaction
lookupPendingTransaction store utxid =
fromMaybe (error ("Inconsistency: Received an update for a"
++ " transaction that I have not seen before."))
(M.lookup utxid (masPending store))
isFromMarkerAddress :: Ord a => M.Map a b -> [a] -> Maybe a
isFromMarkerAddress conf = go
where
go [] = Nothing
go (o:os) = if M.member o conf
then Just o
else go os