{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE NoFieldSelectors #-} module Haskoin.Store.Data ( -- * Address Balances Balance (..), zeroBalance, nullBalance, -- * Block Data BlockData (..), confirmed, -- * Transactions TxRef (..), TxData (..), txDataFee, isCoinbaseTx, Transaction (..), transactionData, fromTransaction, toTransaction, StoreInput (..), isCoinbase, StoreOutput (..), Prev (..), Spender (..), BlockRef (..), UnixTime, getUnixTime, putUnixTime, BlockPos, -- * Unspent Outputs Unspent (..), -- * Extended Public Keys XPubSpec (..), XPubBal (..), XPubUnspent (..), XPubSummary (..), DeriveType (..), textToDeriveType, deriveTypeToText, -- * Other Data TxId (..), GenericResult (..), SerialList (..), RawResult (..), RawResultList (..), PeerInfo (..), Healthy (..), BlockHealth (..), TimeHealth (..), CountHealth (..), MaxHealth (..), HealthCheck (..), Event (..), Except (..), -- * Blockchain.info API BinfoInfo (..), BinfoBlockId (..), BinfoTxId (..), encodeBinfoTxId, BinfoFilter (..), BinfoMultiAddr (..), BinfoShortBal (..), BinfoBalance (..), toBinfoAddrs, BinfoRawAddr (..), BinfoAddr (..), parseBinfoAddr, BinfoWallet (..), BinfoUnspent (..), binfoHexValue, BinfoUnspents (..), BinfoBlock (..), toBinfoBlock, BinfoTx (..), relevantTxs, toBinfoTx, toBinfoTxSimple, BinfoTxInput (..), BinfoTxOutput (..), BinfoSpender (..), BinfoXPubPath (..), BinfoBlockInfo (..), toBinfoBlockInfo, BinfoSymbol (..), BinfoTicker (..), BinfoRate (..), BinfoHistory (..), toBinfoHistory, BinfoDate (..), BinfoHeader (..), BinfoMempool (..), BinfoBlockInfos (..), ) where import Control.Applicative (optional, (<|>)) import Control.DeepSeq (NFData) import Control.Exception (Exception) import Control.Monad (guard, join, mzero, unless, (<=<)) import Data.Aeson ( Encoding, FromJSON (..), ToJSON (..), Value (..), (.!=), (.:), (.:?), (.=), ) import Data.Aeson qualified as A import Data.Aeson.Encoding qualified as A import Data.Aeson.Types (Parser) import Data.Binary (Binary (get, put)) import Data.Bits (Bits (..)) import Data.Bool (bool) import Data.ByteString (ByteString) import Data.ByteString qualified as B import Data.ByteString.Builder qualified as Builder import Data.Bytes.Get import Data.Bytes.Get qualified as Bytes.Get import Data.Bytes.Put import Data.Bytes.Serial import Data.Default (Default (..)) import Data.Foldable (toList) import Data.Function (on) import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict qualified as HashMap import Data.HashSet (HashSet) import Data.HashSet qualified as HashSet import Data.Hashable (Hashable (..)) import Data.Int (Int32, Int64) import Data.IntMap qualified as IntMap import Data.IntMap.Strict (IntMap) import Data.Maybe ( catMaybes, fromMaybe, isJust, isNothing, mapMaybe, maybeToList, ) import Data.Serialize (Serialize (..)) import Data.String.Conversions (cs) import Data.Text (Text) import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.Text.Lazy qualified as LazyText import Data.Text.Lazy.Encoding qualified as LazyText import Data.Time (UTCTime (UTCTime), rfc822DateFormat) import Data.Time.Clock.POSIX ( posixSecondsToUTCTime, utcTimeToPOSIXSeconds, ) import Data.Time.Format ( defaultTimeLocale, formatTime, parseTimeM, ) import Data.Time.Format.ISO8601 ( iso8601ParseM, iso8601Show, ) import Data.Vector qualified as V import Data.Word (Word32, Word64) import GHC.Generics (Generic) import Haskoin import Web.Scotty.Trans (Parsable (..)) data DeriveType = DeriveNormal | DeriveP2SH | DeriveP2WPKH deriving (Show, Eq, Generic, NFData) textToDeriveType :: Text -> Maybe DeriveType textToDeriveType "normal" = Just DeriveNormal textToDeriveType "compat" = Just DeriveP2SH textToDeriveType "segwit" = Just DeriveP2WPKH textToDeriveType _ = Nothing deriveTypeToText :: DeriveType -> Text deriveTypeToText DeriveNormal = "normal" deriveTypeToText DeriveP2SH = "compat" deriveTypeToText DeriveP2WPKH = "segwit" instance Serial DeriveType where serialize DeriveNormal = putWord8 0x00 serialize DeriveP2SH = putWord8 0x01 serialize DeriveP2WPKH = putWord8 0x02 deserialize = getWord8 >>= \case 0x00 -> return DeriveNormal 0x01 -> return DeriveP2SH 0x02 -> return DeriveP2WPKH _ -> return DeriveNormal instance Binary DeriveType where put = serialize get = deserialize instance Serialize DeriveType where put = serialize get = deserialize instance Default DeriveType where def = DeriveNormal instance Parsable DeriveType where parseParam txt = case textToDeriveType (LazyText.toStrict txt) of Nothing -> Left "invalid derivation type" Just x -> Right x data XPubSpec = XPubSpec { key :: !XPubKey, deriv :: !DeriveType } deriving (Show, Eq, Generic, NFData) instance Hashable XPubSpec where hashWithSalt i = hashWithSalt i . (.key.key) instance Serial XPubSpec where serialize XPubSpec {key = k, deriv = t} = do putWord8 k.depth serialize k.parent putWord32be k.index serialize k.chain putByteString k.key.get serialize t deserialize = do depth <- getWord8 parent <- deserialize index <- getWord32be chain <- deserialize key <- PubKey <$> getByteString 64 deriv <- deserialize return XPubSpec {key = XPubKey {..}, deriv} instance Serialize XPubSpec where put = serialize get = deserialize instance Binary XPubSpec where put = serialize get = deserialize type UnixTime = Word64 type BlockPos = Word32 -- | Binary such that ordering is inverted. putUnixTime :: (MonadPut m) => Word64 -> m () putUnixTime w = putWord64be $ maxBound - w getUnixTime :: (MonadGet m) => m Word64 getUnixTime = (maxBound -) <$> getWord64be -- | Reference to a block where a transaction is stored. data BlockRef = BlockRef { -- | block height in the chain height :: !BlockHeight, -- | position of transaction within the block position :: !Word32 } | MemRef { timestamp :: !UnixTime } deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData) -- | Serial entities will sort in reverse order. instance Serial BlockRef where serialize MemRef {timestamp = t} = do putWord8 0x00 putUnixTime t serialize BlockRef {height = h, position = p} = do putWord8 0x01 putWord32be (maxBound - h) putWord32be (maxBound - p) deserialize = getWord8 >>= \case 0x00 -> getmemref 0x01 -> getblockref _ -> fail "Cannot decode BlockRef" where getmemref = do MemRef <$> getUnixTime getblockref = do h <- (maxBound -) <$> getWord32be p <- (maxBound -) <$> getWord32be return BlockRef {height = h, position = p} instance Serialize BlockRef where put = serialize get = deserialize instance Binary BlockRef where put = serialize get = deserialize confirmed :: BlockRef -> Bool confirmed BlockRef {} = True confirmed MemRef {} = False instance ToJSON BlockRef where toJSON BlockRef {height = h, position = p} = A.object [ "height" .= h, "position" .= p ] toJSON MemRef {timestamp = t} = A.object ["mempool" .= t] toEncoding BlockRef {height = h, position = p} = A.pairs $ mconcat [ "height" `A.pair` A.word32 h, "position" `A.pair` A.word32 p ] toEncoding MemRef {timestamp = t} = A.pairs $ "mempool" `A.pair` A.word64 t instance FromJSON BlockRef where parseJSON = A.withObject "BlockRef" $ \o -> b o <|> m o where b o = do height <- o .: "height" position <- o .: "position" return BlockRef {..} m o = MemRef <$> o .: "mempool" -- | Transaction in relation to an address. data TxRef = TxRef { -- | block information block :: !BlockRef, -- | transaction hash txid :: !TxHash } deriving (Show, Eq, Ord, Generic, Hashable, NFData) instance Serial TxRef where serialize (TxRef b h) = do serialize b serialize h deserialize = TxRef <$> deserialize <*> deserialize instance Binary TxRef where put = serialize get = deserialize instance Serialize TxRef where put = serialize get = deserialize instance ToJSON TxRef where toJSON x = A.object [ "txid" .= x.txid, "block" .= x.block ] toEncoding btx = A.pairs $ mconcat [ "txid" `A.pair` toEncoding btx.txid, "block" `A.pair` toEncoding btx.block ] instance FromJSON TxRef where parseJSON = A.withObject "TxRef" $ \o -> do txid <- o .: "txid" block <- o .: "block" return TxRef {..} -- | Address balance information. data Balance = Balance { -- | address balance address :: !Address, -- | confirmed balance confirmed :: !Word64, -- | unconfirmed balance unconfirmed :: !Word64, -- | number of unspent outputs utxo :: !Word64, -- | number of transactions txs :: !Word64, -- | total amount from all outputs in this address received :: !Word64 } deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData) instance Serial Balance where serialize b = do serialize b.address putWord64be b.confirmed putWord64be b.unconfirmed putWord64be b.utxo putWord64be b.txs putWord64be b.received deserialize = do address <- deserialize confirmed <- getWord64be unconfirmed <- getWord64be utxo <- getWord64be txs <- getWord64be received <- getWord64be return Balance {..} instance Binary Balance where put = serialize get = deserialize instance Serialize Balance where put = serialize get = deserialize zeroBalance :: Address -> Balance zeroBalance a = Balance { address = a, confirmed = 0, unconfirmed = 0, utxo = 0, txs = 0, received = 0 } nullBalance :: Balance -> Bool nullBalance Balance { confirmed = 0, unconfirmed = 0, utxo = 0, txs = 0, received = 0 } = True nullBalance _ = False instance MarshalJSON Network Balance where marshalValue net b = A.object [ "address" .= marshalValue net b.address, "confirmed" .= b.confirmed, "unconfirmed" .= b.unconfirmed, "utxo" .= b.utxo, "txs" .= b.txs, "received" .= b.received ] marshalEncoding net b = A.pairs $ mconcat [ "address" `A.pair` marshalEncoding net b.address, "confirmed" `A.pair` A.word64 b.confirmed, "unconfirmed" `A.pair` A.word64 b.unconfirmed, "utxo" `A.pair` A.word64 b.utxo, "txs" `A.pair` A.word64 b.txs, "received" `A.pair` A.word64 b.received ] unmarshalValue net = A.withObject "Balance" $ \o -> do confirmed <- o .: "confirmed" unconfirmed <- o .: "unconfirmed" utxo <- o .: "utxo" txs <- o .: "txs" received <- o .: "received" address <- unmarshalValue net =<< o .: "address" return Balance {..} -- | Unspent output. data Unspent = Unspent { block :: !BlockRef, outpoint :: !OutPoint, value :: !Word64, script :: !ByteString, address :: !(Maybe Address) } deriving (Show, Eq, Ord, Generic, Hashable, NFData) instance Serial Unspent where serialize u = do serialize u.block serialize u.outpoint putWord64be u.value putLengthBytes u.script putMaybe serialize u.address deserialize = do block <- deserialize outpoint <- deserialize value <- getWord64be script <- getLengthBytes address <- getMaybe deserialize return Unspent {..} instance Binary Unspent where put = serialize get = deserialize instance Serialize Unspent where put = serialize get = deserialize instance Coin Unspent where coinValue = (.value) instance MarshalJSON Network Unspent where marshalValue net u = A.object [ "address" .= fmap (marshalValue net) u.address, "block" .= u.block, "txid" .= u.outpoint.hash, "index" .= u.outpoint.index, "pkscript" .= encodeHex u.script, "value" .= u.value ] marshalEncoding net u = A.pairs $ mconcat [ "address" `A.pair` maybe A.null_ (marshalEncoding net) u.address, "block" `A.pair` toEncoding u.block, "txid" `A.pair` toEncoding u.outpoint.hash, "index" `A.pair` A.word32 u.outpoint.index, "pkscript" `A.pair` hexEncoding (B.fromStrict u.script), "value" `A.pair` A.word64 u.value ] unmarshalValue net = A.withObject "Unspent" $ \o -> do block <- o .: "block" hash <- o .: "txid" index <- o .: "index" value <- o .: "value" script <- o .: "pkscript" >>= jsonHex address <- o .: "address" >>= maybe (return Nothing) (optional . unmarshalValue net) return Unspent {outpoint = OutPoint {..}, ..} -- | Database value for a block entry. data BlockData = BlockData { -- | height of the block in the chain height :: !BlockHeight, -- | is this block in the main chain? main :: !Bool, -- | accumulated work in that block work :: !Integer, -- | block header header :: !BlockHeader, -- | size of the block including witnesses size :: !Word32, -- | weight of this block (for segwit networks) weight :: !Word32, -- | block transactions txs :: ![TxHash], -- | sum of all transaction outputs outputs :: !Word64, -- | sum of all transaction fee fee :: !Word64, -- | block subsidy subsidy :: !Word64 } deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData) instance Serial BlockData where serialize b = do putWord32be b.height serialize b.main putInteger b.work serialize b.header putWord32be b.size putWord32be b.weight putList serialize b.txs putWord64be b.outputs putWord64be b.fee putWord64be b.subsidy deserialize = do height <- getWord32be main <- deserialize work <- getInteger header <- deserialize size <- getWord32be weight <- getWord32be txs <- getList deserialize outputs <- getWord64be fee <- getWord64be subsidy <- getWord64be return BlockData {..} instance Serialize BlockData where put = serialize get = deserialize instance Binary BlockData where put = serialize get = deserialize instance FromJSON BlockData where parseJSON = A.withObject "BlockData" $ \o -> do height <- o .: "height" main <- o .: "mainchain" prev <- o .: "previous" timestamp <- o .: "time" version <- o .: "version" bits <- o .: "bits" nonce <- o .: "nonce" size <- o .: "size" txs <- o .: "tx" TxHash merkle <- o .: "merkle" subsidy <- o .: "subsidy" fee <- o .: "fees" outputs <- o .: "outputs" work <- o .: "work" weight <- o .:? "weight" .!= 0 return BlockData {header = BlockHeader {..}, ..} instance MarshalJSON Network BlockData where marshalValue net b = A.object $ [ "hash" .= headerHash b.header, "height" .= b.height, "mainchain" .= b.main, "previous" .= b.header.prev, "time" .= b.header.timestamp, "version" .= b.header.version, "bits" .= b.header.bits, "nonce" .= b.header.nonce, "size" .= b.size, "tx" .= b.txs, "merkle" .= TxHash b.header.merkle, "subsidy" .= b.subsidy, "fees" .= b.fee, "outputs" .= b.outputs, "work" .= b.work ] <> ["weight" .= b.weight | net.segWit] marshalEncoding net b = A.pairs $ mconcat [ "hash" `A.pair` toEncoding (headerHash b.header), "height" `A.pair` A.word32 b.height, "mainchain" `A.pair` A.bool b.main, "previous" `A.pair` toEncoding b.header.prev, "time" `A.pair` A.word32 b.header.timestamp, "version" `A.pair` A.word32 b.header.version, "bits" `A.pair` A.word32 b.header.bits, "nonce" `A.pair` A.word32 b.header.nonce, "size" `A.pair` A.word32 b.size, "tx" `A.pair` A.list toEncoding b.txs, "merkle" `A.pair` toEncoding (TxHash b.header.merkle), "subsidy" `A.pair` A.word64 b.subsidy, "fees" `A.pair` A.word64 b.fee, "outputs" `A.pair` A.word64 b.outputs, "work" `A.pair` A.integer b.work, bool mempty ("weight" `A.pair` A.word32 b.weight) net.segWit ] unmarshalValue net = parseJSON data StoreInput = StoreCoinbase { outpoint :: !OutPoint, sequence :: !Word32, script :: !ByteString, witness :: !WitnessStack } | StoreInput { outpoint :: !OutPoint, sequence :: !Word32, script :: !ByteString, pkscript :: !ByteString, value :: !Word64, witness :: !WitnessStack, address :: !(Maybe Address) } deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData) instance Serial StoreInput where serialize StoreCoinbase {..} = do putWord8 0x00 serialize outpoint putWord32be sequence putLengthBytes script putList putLengthBytes witness serialize StoreInput {..} = do putWord8 0x01 serialize outpoint putWord32be sequence putLengthBytes script putLengthBytes pkscript putWord64be value putList putLengthBytes witness putMaybe serialize address deserialize = getWord8 >>= \case 0x00 -> do outpoint <- deserialize sequence <- getWord32be script <- getLengthBytes witness <- getList getLengthBytes return StoreCoinbase {..} 0x01 -> do outpoint <- deserialize sequence <- getWord32be script <- getLengthBytes pkscript <- getLengthBytes value <- getWord64be witness <- getList getLengthBytes address <- getMaybe deserialize return StoreInput {..} x -> fail $ "Unknown input id: " <> cs (show x) instance Serialize StoreInput where put = serialize get = deserialize instance Binary StoreInput where put = serialize get = deserialize isCoinbaseTx :: Tx -> Bool isCoinbaseTx = all ((== nullOutPoint) . (.outpoint)) . (.inputs) isCoinbase :: StoreInput -> Bool isCoinbase StoreCoinbase {} = True isCoinbase StoreInput {} = False instance MarshalJSON Network StoreInput where marshalValue net StoreInput {..} = A.object [ "coinbase" .= False, "txid" .= outpoint.hash, "output" .= outpoint.index, "sigscript" .= String (encodeHex script), "sequence" .= sequence, "pkscript" .= String (encodeHex pkscript), "value" .= value, "address" .= (marshalValue net <$> address), "witness" .= map encodeHex witness ] marshalValue net StoreCoinbase {..} = A.object [ "coinbase" .= True, "txid" .= outpoint.hash, "output" .= outpoint.index, "sigscript" .= String (encodeHex script), "sequence" .= sequence, "pkscript" .= Null, "value" .= Null, "address" .= Null, "witness" .= map encodeHex witness ] marshalEncoding net StoreInput {..} = A.pairs $ mconcat [ "coinbase" `A.pair` A.bool False, "txid" `A.pair` toEncoding outpoint.hash, "output" `A.pair` A.word32 outpoint.index, "sigscript" `A.pair` hexEncoding (B.fromStrict script), "sequence" `A.pair` A.word32 sequence, "pkscript" `A.pair` hexEncoding (B.fromStrict pkscript), "value" `A.pair` A.word64 value, "address" `A.pair` maybe A.null_ (marshalEncoding net) address, "witness" `A.pair` A.list (hexEncoding . B.fromStrict) witness ] marshalEncoding net StoreCoinbase {..} = A.pairs $ mconcat [ "coinbase" .= True, "txid" `A.pair` toEncoding outpoint.hash, "output" `A.pair` A.word32 outpoint.index, "sigscript" `A.pair` hexEncoding (B.fromStrict script), "sequence" `A.pair` A.word32 sequence, "pkscript" `A.pair` A.null_, "value" `A.pair` A.null_, "address" `A.pair` A.null_, "witness" `A.pair` A.list (hexEncoding . B.fromStrict) witness ] unmarshalValue net = A.withObject "StoreInput" $ \o -> do coinbase <- o .: "coinbase" outpoint <- OutPoint <$> o .: "txid" <*> o .: "output" sequence <- o .: "sequence" witness <- mapM jsonHex =<< o .:? "witness" .!= [] script <- o .: "sigscript" >>= jsonHex if coinbase then return StoreCoinbase {..} else do pkscript <- o .: "pkscript" >>= jsonHex value <- o .: "value" address <- o .: "address" >>= maybe (return Nothing) (optional . unmarshalValue net) return StoreInput {..} jsonHex :: Text -> Parser ByteString jsonHex s = case decodeHex s of Nothing -> fail "Could not decode hex" Just b -> return b -- | Information about input spending output. data Spender = Spender { -- | input transaction hash txid :: !TxHash, -- | input position in transaction index :: !Word32 } deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData) instance Serial Spender where serialize s = do serialize s.txid putWord32be s.index deserialize = Spender <$> deserialize <*> getWord32be instance Serialize Spender where put = serialize get = deserialize instance Binary Spender where put = serialize get = deserialize instance ToJSON Spender where toJSON s = A.object [ "txid" .= s.txid, "input" .= s.index ] toEncoding s = A.pairs $ mconcat [ "txid" `A.pair` toEncoding s.txid, "input" `A.pair` A.word32 s.index ] instance FromJSON Spender where parseJSON = A.withObject "Spender" $ \o -> Spender <$> o .: "txid" <*> o .: "input" -- | Output information. data StoreOutput = StoreOutput { value :: !Word64, script :: !ByteString, spender :: !(Maybe Spender), address :: !(Maybe Address) } deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData) instance Serial StoreOutput where serialize o = do putWord64be o.value putLengthBytes o.script putMaybe serialize o.spender putMaybe serialize o.address deserialize = do value <- getWord64be script <- getLengthBytes spender <- getMaybe deserialize address <- getMaybe deserialize return StoreOutput {..} instance Serialize StoreOutput where put = serialize get = deserialize instance Binary StoreOutput where put = serialize get = deserialize instance MarshalJSON Network StoreOutput where marshalValue net o = A.object [ "address" .= (marshalValue net <$> o.address), "pkscript" .= encodeHex o.script, "value" .= o.value, "spent" .= isJust o.spender, "spender" .= o.spender ] marshalEncoding net o = A.pairs $ mconcat [ "address" `A.pair` maybe A.null_ (marshalEncoding net) o.address, "pkscript" `A.pair` hexEncoding (B.fromStrict o.script), "value" `A.pair` A.word64 o.value, "spent" `A.pair` A.bool (isJust o.spender), "spender" `A.pair` toEncoding o.spender ] unmarshalValue net = A.withObject "StoreOutput" $ \o -> do value <- o .: "value" script <- o .: "pkscript" >>= jsonHex spender <- o .: "spender" address <- o .: "address" >>= maybe (return Nothing) (optional . unmarshalValue net) return StoreOutput {..} data Prev = Prev { script :: !ByteString, value :: !Word64 } deriving (Show, Eq, Ord, Generic, Hashable, NFData) instance Serial Prev where serialize p = do putLengthBytes p.script putWord64be p.value deserialize = do script <- getLengthBytes value <- getWord64be return Prev {..} instance Binary Prev where put = serialize get = deserialize instance Serialize Prev where put = serialize get = deserialize toInput :: Ctx -> TxIn -> Maybe Prev -> WitnessStack -> StoreInput toInput ctx i Nothing w = StoreCoinbase { outpoint = i.outpoint, sequence = i.sequence, script = i.script, witness = w } toInput ctx i (Just p) w = StoreInput { outpoint = i.outpoint, sequence = i.sequence, script = i.script, pkscript = p.script, value = p.value, witness = w, address = eitherToMaybe (scriptToAddressBS ctx p.script) } toOutput :: Ctx -> TxOut -> Maybe Spender -> StoreOutput toOutput ctx o s = StoreOutput { value = o.value, script = o.script, spender = s, address = eitherToMaybe (scriptToAddressBS ctx o.script) } data TxData = TxData { block :: !BlockRef, tx :: !Tx, prevs :: !(IntMap Prev), deleted :: !Bool, rbf :: !Bool, timestamp :: !Word64, spenders :: !(IntMap Spender) } deriving (Show, Eq, Ord, Generic, NFData) instance Serial TxData where serialize t = do serialize t.block serialize t.tx putIntMap (putWord64be . fromIntegral) serialize t.prevs serialize t.deleted serialize t.rbf putWord64be t.timestamp putIntMap (putWord64be . fromIntegral) serialize t.spenders deserialize = do block <- deserialize tx <- deserialize prevs <- getIntMap (fromIntegral <$> getWord64be) deserialize deleted <- deserialize rbf <- deserialize timestamp <- getWord64be spenders <- getIntMap (fromIntegral <$> getWord64be) deserialize return TxData {..} instance Serialize TxData where put = serialize get = deserialize instance Binary TxData where put = serialize get = deserialize txDataFee :: TxData -> Word64 txDataFee t = if isCoinbaseTx t.tx then 0 else inputs - outputs where inputs = sum $ map (.value) $ IntMap.elems t.prevs outputs = sum $ map (.value) t.tx.outputs toTransaction :: Ctx -> TxData -> Transaction toTransaction ctx t = Transaction { version = t.tx.version, locktime = t.tx.locktime, block = t.block, deleted = t.deleted, rbf = t.rbf, timestamp = t.timestamp, txid = txHash t.tx, inputs = ins, outputs = outs, size = fromIntegral $ B.length $ runPutS $ serialize t.tx, weight = let b = B.length $ runPutS $ serialize witless x = B.length $ runPutS $ serialize t.tx in fromIntegral $ b * 3 + x, fee = if any isCoinbase ins then 0 else inv - outv } where ins = zipWith f [0 ..] t.tx.inputs witless = let Tx {..} = t.tx in Tx {witness = [], ..} inv = sum (map (.value) ins) outs = zipWith g [0 ..] t.tx.outputs outv = sum $ map (.value) outs ws = take (length t.tx.inputs) $ t.tx.witness <> repeat [] f n i = toInput ctx i (IntMap.lookup n t.prevs) (ws !! n) g n o = toOutput ctx o $ IntMap.lookup n t.spenders fromTransaction :: Transaction -> TxData fromTransaction t = TxData { block = t.block, tx = tx, deleted = t.deleted, rbf = t.rbf, timestamp = t.timestamp, prevs = IntMap.fromList $ catMaybes $ zipWith f [0 ..] t.inputs, spenders = IntMap.fromList $ catMaybes $ zipWith g [0 ..] t.outputs } where tx = transactionData t f _ StoreCoinbase {} = Nothing f n StoreInput {script = s, value = v} = Just (n, Prev {script = s, value = v}) g _ StoreOutput {spender = Nothing} = Nothing g n StoreOutput {spender = Just s} = Just (n, s) -- | Detailed transaction information. data Transaction = Transaction { -- | block information for this transaction block :: !BlockRef, -- | transaction version version :: !Word32, -- | lock time locktime :: !Word32, -- | transaction inputs inputs :: ![StoreInput], -- | transaction outputs outputs :: ![StoreOutput], -- | this transaction has been deleted and is no longer valid deleted :: !Bool, -- | this transaction can be replaced in the mempool rbf :: !Bool, -- | time the transaction was first seen or time of block timestamp :: !Word64, -- | transaction id txid :: !TxHash, -- | serialized transaction size (includes witness data) size :: !Word32, -- | transaction weight weight :: !Word32, -- | fees that this transaction pays (0 for coinbase) fee :: !Word64 } deriving (Show, Eq, Ord, Generic, Hashable, NFData) instance Serial Transaction where serialize t = do serialize t.block putWord32be t.version putWord32be t.locktime putList serialize t.inputs putList serialize t.outputs serialize t.deleted serialize t.rbf putWord64be t.timestamp serialize t.txid putWord32be t.size putWord32be t.weight putWord64be t.fee deserialize = do block <- deserialize version <- getWord32be locktime <- getWord32be inputs <- getList deserialize outputs <- getList deserialize deleted <- deserialize rbf <- deserialize timestamp <- getWord64be txid <- deserialize size <- getWord32be weight <- getWord32be fee <- getWord64be return Transaction {..} instance Serialize Transaction where put = serialize get = deserialize instance Binary Transaction where put = serialize get = deserialize transactionData :: Transaction -> Tx transactionData t = Tx { inputs = map i t.inputs, outputs = map o t.outputs, version = t.version, locktime = t.locktime, witness = w $ map (.witness) t.inputs } where i StoreCoinbase {..} = TxIn {..} i StoreInput {..} = TxIn {..} o StoreOutput {..} = TxOut {..} w xs | all null xs = [] | otherwise = xs instance MarshalJSON Network Transaction where marshalValue net t = A.object [ "txid" .= t.txid, "size" .= t.size, "version" .= t.version, "locktime" .= t.locktime, "fee" .= t.fee, "inputs" .= map (marshalValue net) t.inputs, "outputs" .= map (marshalValue net) t.outputs, "block" .= t.block, "deleted" .= t.deleted, "time" .= t.timestamp, "rbf" .= t.rbf, "weight" .= t.weight ] marshalEncoding net t = A.pairs $ mconcat [ "txid" `A.pair` toEncoding t.txid, "size" `A.pair` A.word32 t.size, "version" `A.pair` A.word32 t.version, "locktime" `A.pair` A.word32 t.locktime, "fee" `A.pair` A.word64 t.fee, "inputs" `A.pair` A.list (marshalEncoding net) t.inputs, "outputs" `A.pair` A.list (marshalEncoding net) t.outputs, "block" `A.pair` toEncoding t.block, "deleted" `A.pair` A.bool t.deleted, "time" `A.pair` A.word64 t.timestamp, "rbf" `A.pair` A.bool t.rbf, "weight" `A.pair` A.word32 t.weight ] unmarshalValue net = A.withObject "Transaction" $ \o -> do version <- o .: "version" locktime <- o .: "locktime" inputs <- o .: "inputs" >>= mapM (unmarshalValue net) outputs <- o .: "outputs" >>= mapM (unmarshalValue net) block <- o .: "block" deleted <- o .: "deleted" timestamp <- o .: "time" rbf <- o .:? "rbf" .!= False weight <- o .:? "weight" .!= 0 size <- o .: "size" txid <- o .: "txid" fee <- o .: "fee" return Transaction {..} -- | Information about a connected peer. data PeerInfo = PeerInfo { -- | user agent string userAgent :: !ByteString, -- | network address address :: !String, -- | version number version :: !Word32, -- | services field services :: !Word64, -- | will relay transactions relay :: !Bool } deriving (Show, Eq, Ord, Generic, NFData) instance Serial PeerInfo where serialize p = do putLengthBytes p.userAgent putLengthBytes $ T.encodeUtf8 $ T.pack p.address putWord32be p.version putWord64be p.services serialize p.relay deserialize = do userAgent <- getLengthBytes address <- T.unpack . T.decodeUtf8 <$> getLengthBytes version <- getWord32be services <- getWord64be relay <- deserialize return PeerInfo {..} instance Serialize PeerInfo where put = serialize get = deserialize instance Binary PeerInfo where put = serialize get = deserialize instance ToJSON PeerInfo where toJSON p = A.object [ "useragent" .= String (T.decodeUtf8 p.userAgent), "address" .= p.address, "version" .= p.version, "services" .= String (encodeHex $ runPutS $ serialize p.services), "relay" .= p.relay ] toEncoding p = A.pairs $ mconcat [ "useragent" `A.pair` A.text (T.decodeUtf8 p.userAgent), "address" `A.pair` toEncoding p.address, "version" `A.pair` A.word32 p.version, "services" `A.pair` hexEncoding (runPutL $ serialize p.services), "relay" `A.pair` A.bool p.relay ] instance FromJSON PeerInfo where parseJSON = A.withObject "PeerInfo" $ \o -> do String userAgentText <- o .: "useragent" let userAgent = T.encodeUtf8 userAgentText address <- o .: "address" version <- o .: "version" services <- o .: "services" >>= jsonHex >>= \b -> case runGetS deserialize b of Left e -> fail $ "Could not decode services: " <> e Right s -> return s relay <- o .: "relay" return PeerInfo {..} -- | Address balances for an extended public key. data XPubBal = XPubBal { path :: ![KeyIndex], balance :: !Balance } deriving (Show, Ord, Eq, Generic, NFData) instance Serial XPubBal where serialize b = do putList putWord32be b.path serialize b.balance deserialize = do path <- getList getWord32be balance <- deserialize return XPubBal {..} instance Serialize XPubBal where put = serialize get = deserialize instance Binary XPubBal where put = serialize get = deserialize instance MarshalJSON Network XPubBal where marshalValue net b = A.object [ "path" .= b.path, "balance" .= marshalValue net b.balance ] marshalEncoding net b = A.pairs $ mconcat [ "path" `A.pair` A.list A.word32 b.path, "balance" `A.pair` marshalEncoding net b.balance ] unmarshalValue net = A.withObject "XPubBal" $ \o -> do path <- o .: "path" balance <- unmarshalValue net =<< o .: "balance" return XPubBal {..} -- | Unspent transaction for extended public key. data XPubUnspent = XPubUnspent { unspent :: !Unspent, path :: ![KeyIndex] } deriving (Show, Eq, Ord, Generic, NFData) instance Serial XPubUnspent where serialize u = do putList putWord32be u.path serialize u.unspent deserialize = do path <- getList getWord32be unspent <- deserialize return XPubUnspent {..} instance Serialize XPubUnspent where put = serialize get = deserialize instance Binary XPubUnspent where put = serialize get = deserialize instance MarshalJSON Network XPubUnspent where marshalValue net u = A.object [ "unspent" .= marshalValue net u.unspent, "path" .= u.path ] marshalEncoding net u = A.pairs $ mconcat [ "unspent" `A.pair` marshalEncoding net u.unspent, "path" `A.pair` A.list A.word32 u.path ] unmarshalValue net = A.withObject "XPubUnspent" $ \o -> do unspent <- o .: "unspent" >>= unmarshalValue net path <- o .: "path" return XPubUnspent {..} data XPubSummary = XPubSummary { confirmed :: !Word64, unconfirmed :: !Word64, received :: !Word64, utxo :: !Word64, external :: !Word32, change :: !Word32 } deriving (Eq, Show, Generic, NFData) instance Serial XPubSummary where serialize s = do putWord64be s.confirmed putWord64be s.unconfirmed putWord64be s.received putWord64be s.utxo putWord32be s.external putWord32be s.change deserialize = do confirmed <- getWord64be unconfirmed <- getWord64be received <- getWord64be utxo <- getWord64be external <- getWord32be change <- getWord32be return XPubSummary {..} instance Binary XPubSummary where put = serialize get = deserialize instance Serialize XPubSummary where put = serialize get = deserialize instance ToJSON XPubSummary where toJSON s = A.object [ "balance" .= A.object [ "confirmed" .= s.confirmed, "unconfirmed" .= s.unconfirmed, "received" .= s.received, "utxo" .= s.utxo ], "indices" .= A.object [ "change" .= s.change, "external" .= s.external ] ] toEncoding s = A.pairs $ mconcat [ A.pair "balance" $ A.pairs $ mconcat [ "confirmed" `A.pair` A.word64 s.confirmed, "unconfirmed" `A.pair` A.word64 s.unconfirmed, "received" `A.pair` A.word64 s.received, "utxo" `A.pair` A.word64 s.utxo ], A.pair "indices" $ A.pairs $ mconcat [ "change" `A.pair` A.word32 s.change, "external" `A.pair` A.word32 s.external ] ] instance FromJSON XPubSummary where parseJSON = A.withObject "XPubSummary" $ \o -> do b <- o .: "balance" i <- o .: "indices" confirmed <- b .: "confirmed" unconfirmed <- b .: "unconfirmed" received <- b .: "received" utxo <- b .: "utxo" change <- i .: "change" external <- i .: "external" return XPubSummary {..} class Healthy a where isOK :: a -> Bool data BlockHealth = BlockHealth { headers :: !BlockHeight, blocks :: !BlockHeight, max :: !Int32 } deriving (Show, Eq, Generic, NFData) instance Serial BlockHealth where serialize h = do serialize (isOK h) putWord32be h.headers putWord32be h.blocks putInt32be h.max deserialize = do k <- deserialize headers <- getWord32be blocks <- getWord32be max <- getInt32be let h = BlockHealth {..} unless (k == isOK h) $ fail "Inconsistent health check" return h instance Serialize BlockHealth where put = serialize get = deserialize instance Binary BlockHealth where put = serialize get = deserialize instance Healthy BlockHealth where isOK x = h - b <= x.max where h = fromIntegral x.headers b = fromIntegral x.blocks instance ToJSON BlockHealth where toJSON h = A.object [ "headers" .= h.headers, "blocks" .= h.blocks, "diff" .= diff, "max" .= h.max, "ok" .= isOK h ] where diff = toInteger h.headers - toInteger h.blocks instance FromJSON BlockHealth where parseJSON = A.withObject "BlockHealth" $ \o -> do headers <- o .: "headers" blocks <- o .: "blocks" max <- o .: "max" return BlockHealth {..} data TimeHealth = TimeHealth { age :: !Int64, max :: !Int64 } deriving (Show, Eq, Generic, NFData) instance Serial TimeHealth where serialize h = do serialize (isOK h) putInt64be h.age putInt64be h.max deserialize = do k <- deserialize age <- getInt64be max <- getInt64be let t = TimeHealth {..} unless (k == isOK t) $ fail "Inconsistent health check" return t instance Binary TimeHealth where put = serialize get = deserialize instance Serialize TimeHealth where put = serialize get = deserialize instance Healthy TimeHealth where isOK TimeHealth {..} = age <= max instance ToJSON TimeHealth where toJSON h = A.object [ "age" .= h.age, "max" .= h.max, "ok" .= isOK h ] instance FromJSON TimeHealth where parseJSON = A.withObject "TimeHealth" $ \o -> do age <- o .: "age" max <- o .: "max" return TimeHealth {..} data CountHealth = CountHealth { count :: !Int64, min :: !Int64 } deriving (Show, Eq, Generic, NFData) instance Serial CountHealth where serialize h = do serialize (isOK h) putInt64be h.count putInt64be h.min deserialize = do k <- deserialize count <- getInt64be min <- getInt64be let c = CountHealth {..} unless (k == isOK c) $ fail "Inconsistent health check" return c instance Serialize CountHealth where put = serialize get = deserialize instance Binary CountHealth where put = serialize get = deserialize instance Healthy CountHealth where isOK CountHealth {..} = min <= count instance ToJSON CountHealth where toJSON h = A.object [ "count" .= h.count, "min" .= h.min, "ok" .= isOK h ] instance FromJSON CountHealth where parseJSON = A.withObject "CountHealth" $ \o -> do count <- o .: "count" min <- o .: "min" return CountHealth {..} data MaxHealth = MaxHealth { count :: !Int64, max :: !Int64 } deriving (Show, Eq, Generic, NFData) instance Serial MaxHealth where serialize h = do serialize $ isOK h putInt64be h.count putInt64be h.max deserialize = do k <- deserialize count <- getInt64be max <- getInt64be let h = MaxHealth {..} unless (k == isOK h) $ fail "Inconsistent health check" return h instance Binary MaxHealth where put = serialize get = deserialize instance Serialize MaxHealth where put = serialize get = deserialize instance Healthy MaxHealth where isOK MaxHealth {..} = count <= max instance ToJSON MaxHealth where toJSON h = A.object [ "count" .= h.count, "max" .= h.max, "ok" .= isOK h ] instance FromJSON MaxHealth where parseJSON = A.withObject "MaxHealth" $ \o -> do count <- o .: "count" max <- o .: "max" return MaxHealth {..} data HealthCheck = HealthCheck { blocks :: !BlockHealth, lastBlock :: !TimeHealth, lastTx :: !TimeHealth, pendingTxs :: !MaxHealth, peers :: !CountHealth, network :: !String, version :: !String, time :: !Word64 } deriving (Show, Eq, Generic, NFData) instance Serial HealthCheck where serialize h = do serialize $ isOK h serialize h.blocks serialize h.lastBlock serialize h.lastTx serialize h.pendingTxs serialize h.peers putLengthBytes $ T.encodeUtf8 $ T.pack h.network putLengthBytes $ T.encodeUtf8 $ T.pack h.version putWord64be h.time deserialize = do k <- deserialize blocks <- deserialize lastBlock <- deserialize lastTx <- deserialize pendingTxs <- deserialize peers <- deserialize network <- T.unpack . T.decodeUtf8 <$> getLengthBytes version <- T.unpack . T.decodeUtf8 <$> getLengthBytes time <- getWord64be let h = HealthCheck {..} unless (k == isOK h) $ fail "Inconsistent health check" return h instance Binary HealthCheck where put = serialize get = deserialize instance Serialize HealthCheck where put = serialize get = deserialize instance Healthy HealthCheck where isOK h = and [ isOK h.blocks, isOK h.lastBlock, isOK h.lastTx, isOK h.pendingTxs, isOK h.peers ] instance ToJSON HealthCheck where toJSON h = A.object [ "blocks" .= h.blocks, "last-block" .= h.lastBlock, "last-tx" .= h.lastTx, "pending-txs" .= h.pendingTxs, "peers" .= h.peers, "net" .= h.network, "version" .= h.version, "time" .= show (posixSecondsToUTCTime $ fromIntegral h.time), "ok" .= isOK h ] instance FromJSON HealthCheck where parseJSON = A.withObject "HealthCheck" $ \o -> do blocks <- o .: "blocks" lastBlock <- o .: "last-block" lastTx <- o .: "last-tx" pendingTxs <- o .: "pending-txs" peers <- o .: "peers" network <- o .: "net" version <- o .: "version" utcTime <- read <$> o .: "time" let time = round $ utcTimeToPOSIXSeconds utcTime return HealthCheck {..} data Event = EventBlock !BlockHash | EventTx !TxHash deriving (Show, Eq, Generic, NFData) instance Serial Event where serialize (EventBlock bh) = putWord8 0x00 >> serialize bh serialize (EventTx th) = putWord8 0x01 >> serialize th deserialize = getWord8 >>= \case 0x00 -> EventBlock <$> deserialize 0x01 -> EventTx <$> deserialize _ -> fail "Not an Event" instance Serialize Event where put = serialize get = deserialize instance Binary Event where put = serialize get = deserialize instance ToJSON Event where toJSON (EventTx h) = A.object ["type" .= String "tx", "id" .= h] toJSON (EventBlock h) = A.object ["type" .= String "block", "id" .= h] toEncoding (EventTx h) = A.pairs ("type" `A.pair` A.text "tx" <> "id" `A.pair` toEncoding h) toEncoding (EventBlock h) = A.pairs ("type" `A.pair` A.text "block" <> "id" `A.pair` toEncoding h) instance FromJSON Event where parseJSON = A.withObject "event" $ \o -> do t <- o .: "type" case t of "tx" -> do i <- o .: "id" return $ EventTx i "block" -> do i <- o .: "id" return $ EventBlock i _ -> fail $ "Could not recognize event type: " <> t newtype GenericResult a = GenericResult {get :: a} deriving (Show, Eq, Generic, NFData) instance (Serial a) => Serial (GenericResult a) where serialize (GenericResult x) = serialize x deserialize = GenericResult <$> deserialize instance (Serial a) => Serialize (GenericResult a) where put = serialize get = deserialize instance (Serial a) => Binary (GenericResult a) where put = serialize get = deserialize instance (ToJSON a) => ToJSON (GenericResult a) where toJSON (GenericResult b) = A.object ["result" .= b] toEncoding (GenericResult b) = A.pairs ("result" `A.pair` toEncoding b) instance (FromJSON a) => FromJSON (GenericResult a) where parseJSON = A.withObject "GenericResult" $ \o -> GenericResult <$> o .: "result" newtype RawResult a = RawResult {get :: a} deriving (Show, Eq, Generic, NFData) instance (Serial a) => Serial (RawResult a) where serialize (RawResult x) = serialize x deserialize = RawResult <$> deserialize instance (Serial a) => Serialize (RawResult a) where put = serialize get = deserialize instance (Serial a) => Binary (RawResult a) where put = serialize get = deserialize instance (Serial a) => ToJSON (RawResult a) where toJSON (RawResult b) = A.object ["result" .= String (encodeHex $ runPutS $ serialize b)] toEncoding (RawResult b) = A.pairs $ "result" `A.pair` hexEncoding (runPutL $ serialize b) instance (Serial a) => FromJSON (RawResult a) where parseJSON = A.withObject "RawResult" $ \o -> do res <- o .: "result" let m = eitherToMaybe . Bytes.Get.runGetS deserialize =<< decodeHex res maybe mzero (return . RawResult) m newtype SerialList a = SerialList {get :: [a]} deriving (Show, Eq, Generic, NFData) instance Semigroup (SerialList a) where SerialList a <> SerialList b = SerialList (a <> b) instance Monoid (SerialList a) where mempty = SerialList mempty instance (Serial a) => Serial (SerialList a) where serialize (SerialList ls) = putList serialize ls deserialize = SerialList <$> getList deserialize instance (ToJSON a) => ToJSON (SerialList a) where toJSON (SerialList ls) = toJSON ls toEncoding (SerialList ls) = A.list toEncoding ls instance (FromJSON a) => FromJSON (SerialList a) where parseJSON = fmap SerialList . parseJSON newtype RawResultList a = RawResultList {get :: [a]} deriving (Show, Eq, Generic, NFData) instance (Serial a) => Serial (RawResultList a) where serialize (RawResultList xs) = mapM_ serialize xs deserialize = RawResultList <$> go where go = isEmpty >>= \case True -> return [] False -> (:) <$> deserialize <*> go instance (Serial a) => Serialize (RawResultList a) where put = serialize get = deserialize instance (Serial a) => Binary (RawResultList a) where put = serialize get = deserialize instance Semigroup (RawResultList a) where (RawResultList a) <> (RawResultList b) = RawResultList $ a <> b instance Monoid (RawResultList a) where mempty = RawResultList mempty instance (Serial a) => ToJSON (RawResultList a) where toJSON (RawResultList xs) = toJSON $ String . encodeHex . runPutS . serialize <$> xs toEncoding (RawResultList xs) = A.list (hexEncoding . runPutL . serialize) xs instance (Serial a) => FromJSON (RawResultList a) where parseJSON = A.withArray "RawResultList" $ \vec -> RawResultList <$> mapM parseElem (toList vec) where deser = eitherToMaybe . runGetS deserialize parseElem = A.withText "RawResultListItem" $ \t -> maybe mzero return (decodeHex t >>= deser) newtype TxId = TxId TxHash deriving (Show, Eq, Generic, NFData) instance Serial TxId where serialize (TxId h) = serialize h deserialize = TxId <$> deserialize instance Serialize TxId where put = serialize get = deserialize instance Binary TxId where put = serialize get = deserialize instance ToJSON TxId where toJSON (TxId h) = A.object ["txid" .= h] toEncoding (TxId h) = A.pairs ("txid" `A.pair` toEncoding h) instance FromJSON TxId where parseJSON = A.withObject "txid" $ \o -> TxId <$> o .: "txid" data Except = ThingNotFound | ServerError | BadRequest | UserError !String | StringError !String | TxIndexConflict ![TxHash] | ServerTimeout | RequestTooLarge deriving (Show, Eq, Ord, Generic, NFData) instance Exception Except instance ToJSON Except where toJSON e = A.object $ case e of ThingNotFound -> [ "error" .= String "not-found-or-invalid-arg", "message" .= String "Item not found or argument invalid" ] ServerError -> [ "error" .= String "server-error", "message" .= String "Server error" ] BadRequest -> [ "error" .= String "bad-request", "message" .= String "Invalid request" ] UserError msg' -> [ "error" .= String "user-error", "message" .= String (cs msg') ] StringError msg' -> [ "error" .= String "string-error", "message" .= String (cs msg') ] TxIndexConflict txids -> [ "error" .= String "multiple-tx-index", "message" .= String "Multiple txs match that index", "txids" .= txids ] ServerTimeout -> [ "error" .= String "server-timeout", "message" .= String "Request is taking too long" ] RequestTooLarge -> [ "error" .= String "request-too-large", "message" .= String "Request body too large" ] instance FromJSON Except where parseJSON = A.withObject "Except" $ \o -> do ctr <- o .: "error" msg' <- o .:? "message" .!= "" case ctr of String "not-found-or-invalid-arg" -> return ThingNotFound String "server-error" -> return ServerError String "bad-request" -> return BadRequest String "user-error" -> return $ UserError msg' String "string-error" -> return $ StringError msg' String "multiple-tx-index" -> do txids <- o .: "txids" return $ TxIndexConflict txids String "server-timeout" -> return ServerTimeout String "request-too-large" -> return RequestTooLarge _ -> mzero --------------------------------------- -- Blockchain.info API Compatibility -- --------------------------------------- toIntTxId :: TxHash -> Word64 toIntTxId h = let bs = runPutS (serialize h) Right w64 = runGetS getWord64be bs in w64 `shift` (-11) data BinfoBlockId = BinfoBlockHash !BlockHash | BinfoBlockIndex !Word32 deriving (Eq, Show, Read, Generic, NFData) instance Parsable BinfoBlockId where parseParam t = hex <> igr where hex = case hexToBlockHash (LazyText.toStrict t) of Nothing -> Left "could not decode txid" Just h -> Right $ BinfoBlockHash h igr = BinfoBlockIndex <$> parseParam t data BinfoTxId = BinfoTxIdHash !TxHash | BinfoTxIdIndex !Word64 deriving (Eq, Show, Read, Generic, NFData) encodeBinfoTxId :: Bool -> TxHash -> BinfoTxId encodeBinfoTxId False = BinfoTxIdHash encodeBinfoTxId True = BinfoTxIdIndex . toIntTxId instance Parsable BinfoTxId where parseParam t = hex <> igr where hex = case hexToTxHash (LazyText.toStrict t) of Nothing -> Left "could not decode txid" Just h -> Right $ BinfoTxIdHash h igr = BinfoTxIdIndex <$> parseParam t instance ToJSON BinfoTxId where toJSON (BinfoTxIdHash h) = toJSON h toJSON (BinfoTxIdIndex i) = toJSON i toEncoding (BinfoTxIdHash h) = toEncoding h toEncoding (BinfoTxIdIndex i) = A.word64 i instance FromJSON BinfoTxId where parseJSON v = BinfoTxIdHash <$> parseJSON v <|> BinfoTxIdIndex <$> parseJSON v data BinfoFilter = BinfoFilterAll | BinfoFilterSent | BinfoFilterReceived | BinfoFilterMoved | BinfoFilterConfirmed | BinfoFilterMempool deriving (Eq, Show, Generic, NFData) instance Parsable BinfoFilter where parseParam t = parseParam t >>= \case (0 :: Int) -> return BinfoFilterAll 1 -> return BinfoFilterSent 2 -> return BinfoFilterReceived 3 -> return BinfoFilterMoved 5 -> return BinfoFilterConfirmed 6 -> return BinfoFilterAll 7 -> return BinfoFilterMempool _ -> Left "could not parse filter parameter" data BinfoMultiAddr = BinfoMultiAddr { addresses :: ![BinfoBalance], wallet :: !BinfoWallet, txs :: ![BinfoTx], info :: !BinfoInfo, recommendFee :: !Bool, cashAddr :: !Bool } deriving (Eq, Show, Generic, NFData) instance MarshalJSON (Network, Ctx) BinfoMultiAddr where marshalValue (net', ctx) m = A.object $ [ "addresses" .= map (marshalValue (net, ctx)) m.addresses, "wallet" .= m.wallet, "txs" .= map (marshalValue (net, ctx)) m.txs, "info" .= m.info, "recommend_include_fee" .= m.recommendFee ] ++ ["cash_addr" .= True | m.cashAddr] where net = if not m.cashAddr && net' == bch then btc else net' unmarshalValue (net, ctx) = A.withObject "BinfoMultiAddr" $ \o -> do addresses <- mapM (unmarshalValue (net, ctx)) =<< o .: "addresses" wallet <- o .: "wallet" txs <- mapM (unmarshalValue (net, ctx)) =<< o .: "txs" info <- o .: "info" recommendFee <- o .: "recommend_include_fee" cashAddr <- o .:? "cash_addr" .!= False return BinfoMultiAddr {..} marshalEncoding (net', ctx) m = A.pairs $ mconcat [ "addresses" `A.pair` A.list (marshalEncoding (net, ctx)) m.addresses, "wallet" `A.pair` toEncoding m.wallet, "txs" `A.pair` A.list (marshalEncoding (net, ctx)) m.txs, "info" `A.pair` toEncoding m.info, "recommend_include_fee" `A.pair` A.bool m.recommendFee, if m.cashAddr then "cash_addr" `A.pair` A.bool True else mempty ] where net = if not m.cashAddr && net' == bch then btc else net' data BinfoRawAddr = BinfoRawAddr { address :: !BinfoAddr, balance :: !Word64, ntx :: !Word64, utxo :: !Word64, received :: !Word64, sent :: !Int64, txs :: ![BinfoTx] } deriving (Eq, Show, Generic, NFData) instance MarshalJSON (Network, Ctx) BinfoRawAddr where marshalValue (net, ctx) r = A.object [ "hash160" .= h160, "address" .= address, "n_tx" .= r.ntx, "n_unredeemed" .= r.utxo, "total_received" .= r.received, "total_sent" .= r.sent, "final_balance" .= r.balance, "txs" .= map (marshalValue (net, ctx)) r.txs ] where address = case r.address of BinfoAddr a -> marshalValue net a BinfoXpub x -> marshalValue (net, ctx) x h160 = encodeHex . runPutS . serialize <$> case r.address of BinfoAddr a -> case a of PubKeyAddress h -> Just h ScriptAddress h -> Just h WitnessPubKeyAddress h -> Just h _ -> Nothing _ -> Nothing marshalEncoding (net, ctx) r = A.pairs $ mconcat [ "hash160" `A.pair` fromMaybe A.null_ h160, "address" `A.pair` address, "n_tx" `A.pair` A.word64 r.ntx, "n_unredeemed" `A.pair` A.word64 r.utxo, "total_received" `A.pair` A.word64 r.received, "total_sent" `A.pair` A.int64 r.sent, "final_balance" `A.pair` A.word64 r.balance, "txs" `A.pair` A.list (marshalEncoding (net, ctx)) r.txs ] where address = case r.address of BinfoAddr a -> marshalEncoding net a BinfoXpub x -> marshalEncoding (net, ctx) x h160 = hexEncoding . runPutL . serialize <$> case r.address of BinfoAddr a -> case a of PubKeyAddress h -> Just h ScriptAddress h -> Just h WitnessPubKeyAddress h -> Just h _ -> Nothing _ -> Nothing unmarshalValue (net, ctx) = A.withObject "BinfoRawAddr" $ \o -> do addr <- o .: "address" address <- BinfoAddr <$> unmarshalValue net addr <|> BinfoXpub <$> unmarshalValue (net, ctx) addr balance <- o .: "final_balance" utxo <- o .: "n_unredeemed" ntx <- o .: "n_tx" received <- o .: "total_received" sent <- o .: "total_sent" txs <- mapM (unmarshalValue (net, ctx)) =<< o .: "txs" return BinfoRawAddr {..} data BinfoShortBal = BinfoShortBal { final :: !Word64, ntx :: !Word64, received :: !Word64 } deriving (Eq, Show, Read, Generic, NFData) instance ToJSON BinfoShortBal where toJSON b = A.object [ "final_balance" .= b.final, "n_tx" .= b.ntx, "total_received" .= b.received ] toEncoding b = A.pairs $ mconcat [ "final_balance" `A.pair` A.word64 b.final, "n_tx" `A.pair` A.word64 b.ntx, "total_received" `A.pair` A.word64 b.received ] instance FromJSON BinfoShortBal where parseJSON = A.withObject "BinfoShortBal" $ \o -> do final <- o .: "final_balance" ntx <- o .: "n_tx" received <- o .: "total_received" return BinfoShortBal {..} data BinfoBalance = BinfoAddrBalance { address :: !Address, txs :: !Word64, received :: !Word64, sent :: !Word64, balance :: !Word64 } | BinfoXPubBalance { xpub :: !XPubKey, txs :: !Word64, received :: !Word64, sent :: !Word64, balance :: !Word64, external :: !Word32, change :: !Word32 } deriving (Eq, Show, Generic, NFData) instance MarshalJSON (Network, Ctx) BinfoBalance where marshalValue (net, ctx) b@BinfoAddrBalance {} = A.object [ "address" .= marshalValue net b.address, "final_balance" .= b.balance, "n_tx" .= b.txs, "total_received" .= b.received, "total_sent" .= b.sent ] marshalValue (net, ctx) b@BinfoXPubBalance {} = A.object [ "address" .= marshalValue (net, ctx) b.xpub, "change_index" .= b.change, "account_index" .= b.external, "final_balance" .= b.balance, "n_tx" .= b.txs, "total_received" .= b.received, "total_sent" .= b.sent ] unmarshalValue (net, ctx) = A.withObject "BinfoBalance" $ \o -> x o <|> a o where x o = do xpub <- unmarshalValue (net, ctx) =<< o .: "address" change <- o .: "change_index" external <- o .: "account_index" balance <- o .: "final_balance" txs <- o .: "n_tx" received <- o .: "total_received" sent <- o .: "total_sent" return BinfoXPubBalance {..} a o = do address <- unmarshalValue net =<< o .: "address" balance <- o .: "final_balance" txs <- o .: "n_tx" received <- o .: "total_received" sent <- o .: "total_sent" return BinfoAddrBalance {..} marshalEncoding (net, ctx) b@BinfoAddrBalance {} = A.pairs $ mconcat [ "address" `A.pair` marshalEncoding net b.address, "final_balance" `A.pair` A.word64 b.balance, "n_tx" `A.pair` A.word64 b.txs, "total_received" `A.pair` A.word64 b.received, "total_sent" `A.pair` A.word64 b.sent ] marshalEncoding (net, ctx) b@BinfoXPubBalance {} = A.pairs $ mconcat [ "address" `A.pair` marshalEncoding (net, ctx) b.xpub, "change_index" `A.pair` A.word32 b.change, "account_index" `A.pair` A.word32 b.external, "final_balance" `A.pair` A.word64 b.balance, "n_tx" `A.pair` A.word64 b.txs, "total_received" `A.pair` A.word64 b.received, "total_sent" `A.pair` A.word64 b.sent ] instance MarshalJSON (Network, Ctx) [BinfoBalance] where marshalValue (net, ctx) addrs = toJSON $ map (marshalValue (net, ctx)) addrs marshalEncoding (net, ctx) = A.list (marshalEncoding (net, ctx)) unmarshalValue (net, ctx) = A.withArray "[BinfoBalance]" $ fmap V.toList . mapM (unmarshalValue (net, ctx)) data BinfoWallet = BinfoWallet { balance :: !Word64, txs :: !Word64, filtered :: !Word64, received :: !Word64, sent :: !Word64 } deriving (Eq, Show, Generic, NFData) instance ToJSON BinfoWallet where toJSON w = A.object [ "final_balance" .= w.balance, "n_tx" .= w.txs, "n_tx_filtered" .= w.filtered, "total_received" .= w.received, "total_sent" .= w.sent ] toEncoding w = A.pairs $ mconcat [ "final_balance" `A.pair` A.word64 w.balance, "n_tx" `A.pair` A.word64 w.txs, "n_tx_filtered" `A.pair` A.word64 w.filtered, "total_received" `A.pair` A.word64 w.received, "total_sent" `A.pair` A.word64 w.sent ] instance FromJSON BinfoWallet where parseJSON = A.withObject "BinfoWallet" $ \o -> do balance <- o .: "final_balance" txs <- o .: "n_tx" filtered <- o .: "n_tx_filtered" received <- o .: "total_received" sent <- o .: "total_sent" return BinfoWallet {..} binfoHexValue :: Word64 -> Text binfoHexValue w64 = encodeHex $ if B.null bs || B.head bs `testBit` 7 then B.cons 0x00 bs else bs where bs = B.dropWhile (== 0x00) $ runPutS $ serialize w64 data BinfoUnspent = BinfoUnspent { txid :: !TxHash, index :: !Word32, script :: !ByteString, value :: !Word64, confirmations :: !Int32, txidx :: !BinfoTxId, xpub :: !(Maybe BinfoXPubPath) } deriving (Eq, Show, Generic, NFData) instance MarshalJSON (Network, Ctx) BinfoUnspent where marshalValue (net, ctx) u = A.object $ [ "tx_hash_big_endian" .= u.txid, "tx_hash" .= encodeHex (runPutS (serialize u.txid.get)), "tx_output_n" .= u.index, "script" .= encodeHex u.script, "value" .= u.value, "value_hex" .= binfoHexValue u.value, "confirmations" .= u.confirmations, "tx_index" .= u.txidx ] <> [ "xpub" .= marshalValue (net, ctx) x | x <- maybeToList u.xpub ] marshalEncoding (net, ctx) u = A.pairs $ mconcat [ "tx_hash_big_endian" `A.pair` toEncoding u.txid, "tx_hash" `A.pair` hexEncoding (runPutL (serialize u.txid.get)), "tx_output_n" `A.pair` A.word32 u.index, "script" `A.pair` hexEncoding (B.fromStrict u.script), "value" `A.pair` A.word64 u.value, "value_hex" `A.pair` A.text (binfoHexValue u.value), "confirmations" `A.pair` A.int32 u.confirmations, "tx_index" `A.pair` toEncoding u.txidx, maybe mempty (("xpub" `A.pair`) . marshalEncoding (net, ctx)) u.xpub ] unmarshalValue (net, ctx) = A.withObject "BinfoUnspent" $ \o -> do txid <- o .: "tx_hash_big_endian" index <- o .: "tx_output_n" script <- maybe mzero return . decodeHex =<< o .: "script" value <- o .: "value" confirmations <- o .: "confirmations" txidx <- o .: "tx_index" xpub <- mapM (unmarshalValue (net, ctx)) =<< o .:? "xpub" return BinfoUnspent {..} newtype BinfoUnspents = BinfoUnspents [BinfoUnspent] deriving (Eq, Show, Generic, NFData) instance MarshalJSON (Network, Ctx) BinfoUnspents where marshalValue (net, ctx) (BinfoUnspents us) = A.object [ "notice" .= T.empty, "unspent_outputs" .= map (marshalValue (net, ctx)) us ] marshalEncoding (net, ctx) (BinfoUnspents us) = A.pairs $ mconcat [ "notice" `A.pair` A.text T.empty, "unspent_outputs" `A.pair` A.list (marshalEncoding (net, ctx)) us ] unmarshalValue (net, ctx) = A.withObject "BinfoUnspents" $ \o -> do us <- mapM (unmarshalValue (net, ctx)) =<< o .: "unspent_outputs" return (BinfoUnspents us) toBinfoBlock :: BlockData -> [BinfoTx] -> [BlockHash] -> BinfoBlock toBinfoBlock b transactions next_blocks = BinfoBlock { hash = headerHash b.header, version = b.header.version, prev = b.header.prev, merkle = b.header.merkle, timestamp = b.header.timestamp, bits = b.header.bits, next = next_blocks, fee = b.fee, nonce = b.header.nonce, ntx = fromIntegral (length transactions), size = b.size, index = b.height, main = b.main, height = b.height, weight = b.weight, txs = transactions } data BinfoBlock = BinfoBlock { hash :: !BlockHash, version :: !Word32, prev :: !BlockHash, merkle :: !Hash256, timestamp :: !Word32, bits :: !Word32, next :: ![BlockHash], fee :: !Word64, nonce :: !Word32, ntx :: !Word32, size :: !Word32, index :: !Word32, main :: !Bool, height :: !Word32, weight :: !Word32, txs :: ![BinfoTx] } deriving (Eq, Show, Generic, NFData) instance MarshalJSON (Network, Ctx) BinfoBlock where marshalValue (net, ctx) b = A.object [ "hash" .= b.hash, "ver" .= b.version, "prev_block" .= b.prev, "mrkl_root" .= TxHash b.merkle, "time" .= b.timestamp, "bits" .= b.bits, "next_block" .= b.next, "fee" .= b.fee, "nonce" .= b.nonce, "n_tx" .= b.ntx, "size" .= b.size, "block_index" .= b.index, "main_chain" .= b.main, "height" .= b.height, "weight" .= b.weight, "tx" .= map (marshalValue (net, ctx)) b.txs ] marshalEncoding (net, ctx) b = A.pairs $ mconcat [ "hash" `A.pair` toEncoding b.hash, "ver" `A.pair` A.word32 b.version, "prev_block" `A.pair` toEncoding b.prev, "mrkl_root" `A.pair` toEncoding (TxHash b.merkle), "time" `A.pair` A.word32 b.timestamp, "bits" `A.pair` A.word32 b.bits, "next_block" `A.pair` toEncoding b.next, "fee" `A.pair` A.word64 b.fee, "nonce" `A.pair` A.word32 b.nonce, "n_tx" `A.pair` A.word32 b.ntx, "size" `A.pair` A.word32 b.size, "block_index" `A.pair` A.word32 b.index, "main_chain" `A.pair` A.bool b.main, "height" `A.pair` A.word32 b.height, "weight" `A.pair` A.word32 b.weight, "tx" `A.pair` A.list (marshalEncoding (net, ctx)) b.txs ] unmarshalValue (net, ctx) = A.withObject "BinfoBlock" $ \o -> do hash <- o .: "hash" version <- o .: "ver" prev <- o .: "prev_block" merkle <- (\(TxHash h) -> h) <$> o .: "mrkl_root" timestamp <- o .: "time" bits <- o .: "bits" next <- o .: "next_block" fee <- o .: "fee" nonce <- o .: "nonce" ntx <- o .: "n_tx" size <- o .: "size" index <- o .: "block_index" main <- o .: "main_chain" height <- o .: "height" weight <- o .: "weight" txs <- o .: "tx" >>= mapM (unmarshalValue (net, ctx)) return BinfoBlock {..} instance MarshalJSON (Network, Ctx) [BinfoBlock] where marshalValue (net, ctx) blocks = A.object ["blocks" .= map (marshalValue (net, ctx)) blocks] marshalEncoding (net, ctx) blocks = A.pairs $ "blocks" `A.pair` A.list (marshalEncoding (net, ctx)) blocks unmarshalValue (net, ctx) = A.withObject "blocks" $ \o -> mapM (unmarshalValue (net, ctx)) =<< o .: "blocks" data BinfoTx = BinfoTx { txid :: !TxHash, version :: !Word32, inputCount :: !Word32, outputCount :: !Word32, size :: !Word32, weight :: !Word32, fee :: !Word64, relayed :: !ByteString, locktime :: !Word32, index :: !BinfoTxId, doubleSpend :: !Bool, rbf :: !Bool, balance :: !(Maybe (Int64, Int64)), timestamp :: !Word64, blockIndex :: !(Maybe Word32), blockHeight :: !(Maybe Word32), inputs :: ![BinfoTxInput], outputs :: ![BinfoTxOutput] } deriving (Eq, Show, Generic, NFData) instance MarshalJSON (Network, Ctx) BinfoTx where marshalValue (net, ctx) t = A.object $ [ "hash" .= t.txid, "ver" .= t.version, "vin_sz" .= t.inputCount, "vout_sz" .= t.outputCount, "size" .= t.size, "weight" .= t.weight, "fee" .= t.fee, "relayed_by" .= T.decodeUtf8 t.relayed, "lock_time" .= t.locktime, "tx_index" .= t.index, "double_spend" .= t.doubleSpend, "time" .= t.timestamp, "block_index" .= t.blockIndex, "block_height" .= t.blockHeight, "inputs" .= map (marshalValue (net, ctx)) t.inputs, "out" .= map (marshalValue (net, ctx)) t.outputs ] ++ bal ++ rbf where bal = case t.balance of Nothing -> [] Just (r, b) -> ["result" .= r, "balance" .= b] rbf = ["rbf" .= True | t.rbf] marshalEncoding (net, ctx) t = A.pairs $ mconcat [ "hash" `A.pair` toEncoding t.txid, "ver" `A.pair` A.word32 t.version, "vin_sz" `A.pair` A.word32 t.inputCount, "vout_sz" `A.pair` A.word32 t.outputCount, "size" `A.pair` A.word32 t.size, "weight" `A.pair` A.word32 t.weight, "fee" `A.pair` A.word64 t.fee, "relayed_by" `A.pair` A.text (T.decodeUtf8 t.relayed), "lock_time" `A.pair` A.word32 t.locktime, "tx_index" `A.pair` toEncoding t.index, "double_spend" `A.pair` A.bool t.doubleSpend, "time" `A.pair` A.word64 t.timestamp, "block_index" `A.pair` toEncoding t.blockIndex, "block_height" `A.pair` maybe A.null_ A.word32 t.blockHeight, "inputs" `A.pair` A.list (marshalEncoding (net, ctx)) t.inputs, "out" `A.pair` A.list (marshalEncoding (net, ctx)) t.outputs, bal, rbf ] where bal = case t.balance of Nothing -> mempty Just (r, b) -> "result" `A.pair` A.int64 r <> "balance" `A.pair` A.int64 b rbf = if t.rbf then "rbf" .= True else mempty unmarshalValue (net, ctx) = A.withObject "BinfoTx" $ \o -> do txid <- o .: "hash" version <- o .: "ver" inputCount <- o .: "vin_sz" outputCount <- o .: "vout_sz" size <- o .: "size" weight <- o .: "weight" fee <- o .: "fee" relayed <- T.encodeUtf8 <$> o .: "relayed_by" locktime <- o .: "lock_time" index <- o .: "tx_index" doubleSpend <- o .: "double_spend" timestamp <- o .: "time" blockIndex <- o .: "block_index" blockHeight <- o .: "block_height" inputs <- o .: "inputs" >>= mapM (unmarshalValue (net, ctx)) outputs <- o .: "out" >>= mapM (unmarshalValue (net, ctx)) rbf <- o .:? "rbf" .!= False res <- o .:? "result" bal <- o .:? "balance" let balance = (,) <$> res <*> bal return BinfoTx {..} instance MarshalJSON (Network, Ctx) [BinfoTx] where marshalValue (net, ctx) txs = toJSON $ map (marshalValue (net, ctx)) txs marshalEncoding (net, ctx) = A.list (marshalEncoding (net, ctx)) unmarshalValue (net, ctx) = A.withArray "[BinfoTx]" $ fmap V.toList . mapM (unmarshalValue (net, ctx)) data BinfoTxInput = BinfoTxInput { sequence :: !Word32, witness :: !ByteString, script :: !ByteString, index :: !Word32, output :: !BinfoTxOutput } deriving (Eq, Show, Generic, NFData) instance MarshalJSON (Network, Ctx) BinfoTxInput where marshalValue (net, ctx) i = A.object [ "sequence" .= i.sequence, "witness" .= encodeHex i.witness, "script" .= encodeHex i.script, "index" .= i.index, "prev_out" .= marshalValue (net, ctx) i.output ] marshalEncoding (net, ctx) i = A.pairs $ mconcat [ "sequence" `A.pair` A.word32 i.sequence, "witness" `A.pair` hexEncoding (B.fromStrict i.witness), "script" `A.pair` hexEncoding (B.fromStrict i.script), "index" `A.pair` A.word32 i.index, "prev_out" `A.pair` marshalEncoding (net, ctx) i.output ] unmarshalValue (net, ctx) = A.withObject "BinfoTxInput" $ \o -> do sequence <- o .: "sequence" witness <- maybe mzero return . decodeHex =<< o .: "witness" script <- maybe mzero return . decodeHex =<< o .: "script" index <- o .: "index" output <- o .: "prev_out" >>= unmarshalValue (net, ctx) return BinfoTxInput {..} data BinfoTxOutput = BinfoTxOutput { typ :: !Int, spent :: !Bool, value :: !Word64, index :: !Word32, txidx :: !BinfoTxId, script :: !ByteString, spenders :: ![BinfoSpender], address :: !(Maybe Address), xpub :: !(Maybe BinfoXPubPath) } deriving (Eq, Show, Generic, NFData) instance MarshalJSON (Network, Ctx) BinfoTxOutput where marshalValue (net, ctx) o = A.object $ [ "type" .= o.typ, "spent" .= o.spent, "value" .= o.value, "spending_outpoints" .= o.spenders, "n" .= o.index, "tx_index" .= o.txidx, "script" .= encodeHex o.script ] <> [ "addr" .= marshalValue net a | a <- maybeToList o.address ] <> [ "xpub" .= marshalValue (net, ctx) x | x <- maybeToList o.xpub ] marshalEncoding (net, ctx) o = A.pairs $ mconcat $ [ "type" `A.pair` A.int o.typ, "spent" `A.pair` A.bool o.spent, "value" `A.pair` A.word64 o.value, "spending_outpoints" `A.pair` A.list toEncoding o.spenders, "n" `A.pair` A.word32 o.index, "tx_index" `A.pair` toEncoding o.txidx, "script" `A.pair` hexEncoding (B.fromStrict o.script) ] <> [ "addr" `A.pair` marshalEncoding net a | a <- maybeToList o.address ] <> [ "xpub" `A.pair` marshalEncoding (net, ctx) x | x <- maybeToList o.xpub ] unmarshalValue (net, ctx) = A.withObject "BinfoTxOutput" $ \o -> do typ <- o .: "type" spent <- o .: "spent" value <- o .: "value" spenders <- o .: "spending_outpoints" index <- o .: "n" txidx <- o .: "tx_index" script <- maybe mzero return . decodeHex =<< o .: "script" address <- o .:? "addr" >>= mapM (unmarshalValue net) xpub <- o .:? "xpub" >>= mapM (unmarshalValue (net, ctx)) return BinfoTxOutput {..} data BinfoSpender = BinfoSpender { txidx :: !BinfoTxId, input :: !Word32 } deriving (Eq, Show, Generic, NFData) instance ToJSON BinfoSpender where toJSON s = A.object [ "tx_index" .= s.txidx, "n" .= s.input ] toEncoding s = A.pairs $ mconcat [ "tx_index" `A.pair` toEncoding s.txidx, "n" `A.pair` A.word32 s.input ] instance FromJSON BinfoSpender where parseJSON = A.withObject "BinfoSpender" $ \o -> do txidx <- o .: "tx_index" input <- o .: "n" return BinfoSpender {..} data BinfoXPubPath = BinfoXPubPath { key :: !XPubKey, deriv :: !SoftPath } deriving (Eq, Show, Generic, NFData) instance Ord BinfoXPubPath where compare = compare `on` f where f b = ( b.key.parent, b.deriv ) instance MarshalJSON (Network, Ctx) BinfoXPubPath where marshalValue (net, ctx) p = A.object [ "m" .= marshalValue (net, ctx) p.key, "path" .= ("M" ++ pathToStr p.deriv) ] marshalEncoding (net, ctx) p = A.pairs $ mconcat [ "m" `A.pair` marshalEncoding (net, ctx) p.key, "path" `A.pair` A.string ("M" ++ pathToStr p.deriv) ] unmarshalValue (net, ctx) = A.withObject "BinfoXPubPath" $ \o -> do key <- o .: "m" >>= unmarshalValue (net, ctx) deriv <- fromMaybe "bad xpub path" . parseSoft <$> o .: "path" return BinfoXPubPath {..} data BinfoInfo = BinfoInfo { connected :: !Word32, conversion :: !Double, fiat :: !BinfoSymbol, crypto :: !BinfoSymbol, head :: !BinfoBlockInfo } deriving (Eq, Show, Generic, NFData) instance ToJSON BinfoInfo where toJSON i = A.object [ "nconnected" .= i.connected, "conversion" .= i.conversion, "symbol_local" .= i.fiat, "symbol_btc" .= i.crypto, "latest_block" .= i.head ] toEncoding i = A.pairs $ mconcat [ "nconnected" `A.pair` A.word32 i.connected, "conversion" `A.pair` A.double i.conversion, "symbol_local" `A.pair` toEncoding i.fiat, "symbol_btc" `A.pair` toEncoding i.crypto, "latest_block" `A.pair` toEncoding i.head ] instance FromJSON BinfoInfo where parseJSON = A.withObject "BinfoInfo" $ \o -> do connected <- o .: "nconnected" conversion <- o .: "conversion" fiat <- o .: "symbol_local" crypto <- o .: "symbol_btc" head <- o .: "latest_block" return BinfoInfo {..} data BinfoBlockInfo = BinfoBlockInfo { hash :: !BlockHash, height :: !BlockHeight, timestamp :: !Word32, index :: !BlockHeight } deriving (Eq, Show, Generic, NFData) instance ToJSON BinfoBlockInfo where toJSON i = A.object [ "hash" .= i.hash, "height" .= i.height, "time" .= i.timestamp, "block_index" .= i.index ] toEncoding i = A.pairs $ mconcat [ "hash" `A.pair` toEncoding i.hash, "height" `A.pair` A.word32 i.height, "time" `A.pair` A.word32 i.timestamp, "block_index" `A.pair` A.word32 i.index ] instance FromJSON BinfoBlockInfo where parseJSON = A.withObject "BinfoBlockInfo" $ \o -> do hash <- o .: "hash" height <- o .: "height" timestamp <- o .: "time" index <- o .: "block_index" return BinfoBlockInfo {..} toBinfoBlockInfo :: BlockData -> BinfoBlockInfo toBinfoBlockInfo d = BinfoBlockInfo { hash = headerHash d.header, height = d.height, timestamp = d.header.timestamp, index = d.height } data BinfoRate = BinfoRate { timestamp :: !Word64, price :: !Double, vol24 :: !Double } deriving (Eq, Show, Generic, NFData) instance ToJSON BinfoRate where toJSON r = A.object [ "timestamp" .= r.timestamp, "price" .= r.price, "volume24h" .= r.vol24 ] toEncoding r = A.pairs $ mconcat [ "timestamp" `A.pair` A.word64 r.timestamp, "price" `A.pair` A.double r.price, "volume24h" `A.pair` A.double r.vol24 ] instance FromJSON BinfoRate where parseJSON = A.withObject "BinfoRate" $ \o -> do timestamp <- o .: "timestamp" price <- o .: "price" vol24 <- o .: "volume24h" return BinfoRate {..} data BinfoHistory = BinfoHistory { date :: !Text, time :: !Text, typ :: !Text, amount :: !Double, valueThen :: !Double, valueNow :: !Double, rateThen :: !Double, txid :: !TxHash, fee :: !Double } deriving (Eq, Show, Generic, NFData) instance ToJSON BinfoHistory where toJSON h = A.object [ "date" .= h.date, "time" .= h.time, "type" .= h.typ, "amount" .= h.amount, "value_then" .= h.valueThen, "value_now" .= h.valueNow, "exchange_rate_then" .= h.rateThen, "tx" .= h.txid, "fee" .= h.fee ] toEncoding h = A.pairs $ mconcat [ "date" `A.pair` A.text h.date, "time" `A.pair` A.text h.time, "type" `A.pair` A.text h.typ, "amount" `A.pair` A.double h.amount, "value_then" `A.pair` A.double h.valueThen, "value_now" `A.pair` A.double h.valueNow, "exchange_rate_then" `A.pair` A.double h.rateThen, "tx" `A.pair` toEncoding h.txid, "fee" `A.pair` A.double h.fee ] instance FromJSON BinfoHistory where parseJSON = A.withObject "BinfoHistory" $ \o -> do date <- o .: "date" time <- o .: "time" typ <- o .: "type" amount <- o .: "amount" valueThen <- o .: "value_then" valueNow <- o .: "value_now" rateThen <- o .: "exchange_rate_then" txid <- o .: "tx" fee <- o .: "fee" return BinfoHistory {..} toBinfoHistory :: Int64 -> Word64 -> Double -> Double -> Word64 -> TxHash -> BinfoHistory toBinfoHistory satoshi timestamp rateThen rateNow fee txid = BinfoHistory { date = T.pack $ formatTime defaultTimeLocale "%Y-%m-%d" t, time = T.pack $ formatTime defaultTimeLocale "%H:%M:%S GMT %Ez" t, typ = if satoshi <= 0 then "sent" else "received", amount = fromRational v, fee = fromRational f, valueThen = fromRational v1, valueNow = fromRational v2, rateThen, txid } where t = posixSecondsToUTCTime (realToFrac timestamp) v = toRational satoshi / (100 * 1000 * 1000) r1 = toRational rateThen r2 = toRational rateNow f = toRational fee / (100 * 1000 * 1000) v1 = v * r1 v2 = v * r2 newtype BinfoDate = BinfoDate Word64 deriving (Eq, Show, Read, Generic, NFData) instance Parsable BinfoDate where parseParam t = maybeToEither "Cannot parse date" . fmap (BinfoDate . round . utcTimeToPOSIXSeconds) $ p "%d-%m-%Y" <|> p "%d/%m/%Y" where s = LazyText.unpack t p fmt = parseTimeM False defaultTimeLocale fmt s data BinfoTicker = BinfoTicker { fifteen :: !Double, last :: !Double, buy :: !Double, sell :: !Double, symbol :: !Text } deriving (Eq, Show, Generic, NFData) instance Default BinfoTicker where def = BinfoTicker { symbol = "XXX", fifteen = 0.0, last = 0.0, buy = 0.0, sell = 0.0 } instance ToJSON BinfoTicker where toJSON t = A.object [ "symbol" .= t.symbol, "sell" .= t.sell, "buy" .= t.buy, "last" .= t.last, "15m" .= t.fifteen ] toEncoding t = A.pairs $ mconcat [ "symbol" `A.pair` A.text t.symbol, "sell" `A.pair` A.double t.sell, "buy" `A.pair` A.double t.buy, "last" `A.pair` A.double t.last, "15m" `A.pair` A.double t.fifteen ] instance FromJSON BinfoTicker where parseJSON = A.withObject "BinfoTicker" $ \o -> do symbol <- o .: "symbol" fifteen <- o .: "15m" sell <- o .: "sell" buy <- o .: "buy" last <- o .: "last" return BinfoTicker {..} data BinfoSymbol = BinfoSymbol { code :: !Text, symbol :: !Text, name :: !Text, conversion :: !Double, after :: !Bool, local :: !Bool } deriving (Eq, Show, Generic, NFData) instance Default BinfoSymbol where def = BinfoSymbol { code = "XXX", symbol = "ยค", name = "No currency", conversion = 0.0, after = False, local = True } instance ToJSON BinfoSymbol where toJSON s = A.object [ "code" .= s.code, "symbol" .= s.symbol, "name" .= s.name, "conversion" .= s.conversion, "symbolAppearsAfter" .= s.after, "local" .= s.local ] toEncoding s = A.pairs $ mconcat [ "code" `A.pair` A.text s.code, "symbol" `A.pair` A.text s.symbol, "name" `A.pair` A.text s.name, "conversion" `A.pair` A.double s.conversion, "symbolAppearsAfter" `A.pair` A.bool s.after, "local" `A.pair` A.bool s.local ] instance FromJSON BinfoSymbol where parseJSON = A.withObject "BinfoSymbol" $ \o -> do code <- o .: "code" symbol <- o .: "symbol" name <- o .: "name" conversion <- o .: "conversion" after <- o .: "symbolAppearsAfter" local <- o .: "local" return BinfoSymbol {..} relevantTxs :: HashSet Address -> Bool -> Transaction -> HashSet TxHash relevantTxs addrs prune t = HashSet.fromList $ ins <> outs where p a = prune && getTxResult addrs t > 0 && not (HashSet.member a addrs) f o = do Spender {txid} <- o.spender a <- o.address guard $ p a return txid outs = mapMaybe f t.outputs g StoreCoinbase {} = Nothing g StoreInput {outpoint = OutPoint h i} = Just h ins = mapMaybe g t.inputs toBinfoAddrs :: HashMap Address Balance -> HashMap XPubSpec [XPubBal] -> HashMap XPubSpec Word64 -> [BinfoBalance] toBinfoAddrs onlyAddrs onlyXpubs xpubTxs = xpubBals <> addrBals where xpubBal k xs = let f x = case x.path of [0, _] -> x.balance.received _ -> 0 g x = x.balance.confirmed + x.balance.unconfirmed i m x = case x.path of [m', n] | m == m' -> n + 1 _ -> 0 received = sum $ map f xs bal = fromIntegral $ sum $ map g xs sent = if bal <= received then received - bal else 0 count = HashMap.lookupDefault 0 k xpubTxs ax = foldl max 0 $ map (i 0) xs cx = foldl max 0 $ map (i 1) xs in BinfoXPubBalance { xpub = k.key, txs = count, received = received, sent = sent, balance = bal, external = ax, change = cx } xpubBals = map (uncurry xpubBal) $ HashMap.toList onlyXpubs addrBals = let f Balance {..} = let sent = received - balance balance = confirmed + unconfirmed in BinfoAddrBalance {..} in map f $ HashMap.elems onlyAddrs toBinfoTxSimple :: Bool -> Transaction -> BinfoTx toBinfoTxSimple numtxid = toBinfoTx numtxid HashMap.empty False 0 toBinfoTxInputs :: Bool -> HashMap Address (Maybe BinfoXPubPath) -> Transaction -> [BinfoTxInput] toBinfoTxInputs numtxid abook t = zipWith f [0 ..] t.inputs where f n i = BinfoTxInput { index = n, sequence = i.sequence, script = i.script, witness = wit i, output = prev n i } wit i = case i.witness of [] -> B.empty ws -> runPutS (put_witness ws) prev = inputToBinfoTxOutput numtxid abook t put_witness ws = do putVarInt (length ws) mapM_ put_item ws put_item bs = do putVarInt (B.length bs) putByteString bs transactionHeight :: Transaction -> Maybe BlockHeight transactionHeight Transaction {deleted = True} = Nothing transactionHeight Transaction {block = MemRef _} = Nothing transactionHeight Transaction {block = BlockRef h _} = Just h toBinfoTx :: Bool -> HashMap Address (Maybe BinfoXPubPath) -> Bool -> Int64 -> Transaction -> BinfoTx toBinfoTx numtxid abook prune bal t = BinfoTx { version = t.version, weight = t.weight, relayed = "0.0.0.0", txid = txHash tx, index = encodeBinfoTxId numtxid (txHash tx), inputCount = fromIntegral $ length t.inputs, outputCount = fromIntegral $ length t.outputs, blockIndex = transactionHeight t, blockHeight = transactionHeight t, doubleSpend = t.deleted, balance = if simple then Nothing else Just (getTxResult aset t, bal), outputs = let p = prune && getTxResult aset t > 0 f = toBinfoTxOutput numtxid abook p t in catMaybes $ zipWith f [0 ..] t.outputs, inputs = toBinfoTxInputs numtxid abook t, size = t.size, rbf = t.rbf, fee = t.fee, locktime = t.locktime, timestamp = t.timestamp } where tx = transactionData t aset = HashMap.keysSet abook simple = HashMap.null abook && bal == 0 getTxResult :: HashSet Address -> Transaction -> Int64 getTxResult aset t = inputSum + outputSum where inputSum = sum $ map inputValue t.inputs inputValue StoreCoinbase {} = 0 inputValue StoreInput {address, value} = case address of Nothing -> 0 Just a -> if testAddr a then negate $ fromIntegral value else 0 testAddr a = HashSet.member a aset outputSum = sum $ map outValue t.outputs outValue StoreOutput {address, value} = case address of Nothing -> 0 Just a -> if testAddr a then fromIntegral value else 0 toBinfoTxOutput :: Bool -> HashMap Address (Maybe BinfoXPubPath) -> Bool -> Transaction -> Word32 -> StoreOutput -> Maybe BinfoTxOutput toBinfoTxOutput numtxid abook prune t index o = if prune && notInBook then Nothing else Just BinfoTxOutput { typ = 0, spent = isJust o.spender, value = o.value, index = index, txidx = encodeBinfoTxId numtxid $ txHash $ transactionData t, script = o.script, spenders = maybeToList $ toBinfoSpender numtxid <$> o.spender, address = o.address, xpub = o.address >>= join . flip HashMap.lookup abook } where notInBook = isNothing $ o.address >>= flip HashMap.lookup abook toBinfoSpender :: Bool -> Spender -> BinfoSpender toBinfoSpender numtxid s = BinfoSpender { txidx = encodeBinfoTxId numtxid s.txid, input = s.index } inputToBinfoTxOutput :: Bool -> HashMap Address (Maybe BinfoXPubPath) -> Transaction -> Word32 -> StoreInput -> BinfoTxOutput inputToBinfoTxOutput numtxid abook t n i = BinfoTxOutput { typ = 0, spent = True, txidx = encodeBinfoTxId numtxid i.outpoint.hash, value = case i of StoreCoinbase {} -> 0 StoreInput {value} -> value, script = case i of StoreCoinbase {} -> B.empty StoreInput {pkscript} -> pkscript, address = case i of StoreCoinbase {} -> Nothing StoreInput {address} -> address, index = i.outpoint.index, spenders = [ BinfoSpender (encodeBinfoTxId numtxid (txHash (transactionData t))) n ], xpub = case i of StoreCoinbase {} -> Nothing StoreInput {address} -> address >>= join . flip HashMap.lookup abook } data BinfoAddr = BinfoAddr !Address | BinfoXpub !XPubKey deriving (Eq, Show, Generic, Hashable, NFData) parseBinfoAddr :: Network -> Ctx -> Text -> Maybe [BinfoAddr] parseBinfoAddr net ctx "" = Just [] parseBinfoAddr net ctx s = mapM f $ concatMap (filter (not . T.null) . T.splitOn ",") (T.splitOn "|" s) where f x = BinfoAddr <$> textToAddr net x <|> BinfoXpub <$> xPubImport net ctx x data BinfoHeader = BinfoHeader { hash :: !BlockHash, timestamp :: !Timestamp, index :: !Word32, height :: !BlockHeight, txids :: ![BinfoTxId] } deriving (Eq, Show, Generic, NFData) instance ToJSON BinfoHeader where toJSON h = A.object [ "hash" .= h.hash, "time" .= h.timestamp, "block_index" .= h.index, "height" .= h.height, "txIndexes" .= h.txids ] toEncoding h = A.pairs $ mconcat [ "hash" `A.pair` toEncoding h.hash, "time" `A.pair` A.word32 h.timestamp, "block_index" `A.pair` A.word32 h.index, "height" `A.pair` A.word32 h.height, "txIndexes" `A.pair` A.list toEncoding h.txids ] instance FromJSON BinfoHeader where parseJSON = A.withObject "BinfoHeader" $ \o -> do hash <- o .: "hash" timestamp <- o .: "time" index <- o .: "block_index" height <- o .: "height" txids <- o .: "txIndexes" return BinfoHeader {..} newtype BinfoMempool = BinfoMempool {get :: [BinfoTx]} deriving (Eq, Show, Generic, NFData) instance MarshalJSON (Network, Ctx) BinfoMempool where marshalValue (net, ctx) (BinfoMempool txs) = A.object ["txs" .= map (marshalValue (net, ctx)) txs] marshalEncoding (net, ctx) (BinfoMempool txs) = A.pairs $ A.pair "txs" $ A.list (marshalEncoding (net, ctx)) txs unmarshalValue (net, ctx) = A.withObject "BinfoMempool" $ \o -> BinfoMempool <$> (mapM (unmarshalValue (net, ctx)) =<< o .: "txs") newtype BinfoBlockInfos = BinfoBlockInfos {get :: [BinfoBlockInfo]} deriving (Eq, Show, Generic, NFData) instance ToJSON BinfoBlockInfos where toJSON b = A.object ["blocks" .= b.get] toEncoding b = A.pairs $ A.pair "blocks" $ A.list toEncoding b.get instance FromJSON BinfoBlockInfos where parseJSON = A.withObject "BinfoBlockInfos" $ \o -> BinfoBlockInfos <$> o .: "blocks"