haskoin-store-data 1.8.0 → 1.8.1
raw patch · 4 files changed
+581/−82 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- haskoin-store-data.cabal +1/−1
- src/Haskoin/Store/Data.hs +411/−67
- src/Haskoin/Store/WebClient.hs +58/−10
- src/Haskoin/Store/WebCommon.hs +111/−4
haskoin-store-data.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: haskoin-store-data-version: 1.8.0+version: 1.8.1 synopsis: Data for Haskoin Store description: Please see the README on GitHub at <https://github.com/jprupp/haskoin-store#readme> category: Bitcoin, Finance, Network
src/Haskoin/Store/Data.hs view
@@ -13,6 +13,35 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE NoFieldSelectors #-} +-- |+-- Module : Haskoin.Store.Data+-- Copyright : (c) Haskoin Developers+-- License : MIT+-- Maintainer : jprupp@protonmail.ch+-- Stability : experimental+-- Portability : POSIX+--+-- Data types of Haskoin Store: what it stores in its database and what its API+-- returns.+--+-- Most types have three encodings:+--+-- * binary ('Serial' and 'Binary'/'Serialize' instances), used in the database+-- and in binary API responses;+-- * JSON ('ToJSON' and 'FromJSON' instances), used in JSON API responses;+-- * for types that contain addresses, JSON functions that take the 'Network'+-- (the @marshal@ functions of 'MarshalJSON'), since the text form of an+-- address depends on the network.+--+-- Types whose names start with @Binfo@ belong to the API compatible with+-- Blockchain.info.+-- These types live in their own package, @haskoin-store-data@, so that clients of+-- the API can use them without depending on RocksDB, Redis or the server.+--+-- Values that refer to positions in the chain sort newest first: the binary+-- encoding of 'BlockRef' stores inverted heights and times, and mempool positions+-- sort before confirmed ones. That is the order in which wallets and explorers+-- want history, so database scans return it directly. module Haskoin.Store.Data ( -- * Address Balances Balance (..),@@ -185,18 +214,25 @@ import Haskoin import Web.Scotty.Trans (Parsable (..)) +-- | Type of the addresses derived from an extended public key. data DeriveType- = DeriveNormal- | DeriveP2SH- | DeriveP2WPKH+ = -- | legacy pay-to-public-key-hash addresses (@normal@)+ DeriveNormal+ | -- | pay-to-witness-public-key-hash nested in pay-to-script-hash+ -- (@compat@)+ DeriveP2SH+ | -- | native pay-to-witness-public-key-hash (@segwit@)+ DeriveP2WPKH deriving (Show, Eq, Generic, NFData) +-- | Parse a derivation type from @normal@, @compat@ or @segwit@. textToDeriveType :: Text -> Maybe DeriveType textToDeriveType "normal" = Just DeriveNormal textToDeriveType "compat" = Just DeriveP2SH textToDeriveType "segwit" = Just DeriveP2WPKH textToDeriveType _ = Nothing +-- | Text form of a derivation type: @normal@, @compat@ or @segwit@. deriveTypeToText :: DeriveType -> Text deriveTypeToText DeriveNormal = "normal" deriveTypeToText DeriveP2SH = "compat"@@ -231,8 +267,11 @@ Nothing -> Left "invalid derivation type" Just x -> Right x +-- | Extended public key with the type of the addresses to derive from it. data XPubSpec = XPubSpec- { key :: !XPubKey,+ { -- | the extended public key+ key :: !XPubKey,+ -- | type of addresses to derive deriv :: !DeriveType } deriving (Show, Eq, Generic, NFData)@@ -268,14 +307,17 @@ put = serialize get = deserialize +-- | Seconds since the Unix epoch. type UnixTime = Word64 +-- | Position of a transaction in a block. type BlockPos = Word32 -- | Binary such that ordering is inverted. putUnixTime :: (MonadPut m) => Word64 -> m () putUnixTime w = putWord64be $ maxBound - w +-- | Inverse of 'putUnixTime'. getUnixTime :: (MonadGet m) => m Word64 getUnixTime = (maxBound -) <$> getWord64be @@ -287,8 +329,10 @@ -- | position of transaction within the block position :: !Word32 }- | MemRef- { timestamp :: !UnixTime+ | -- | the transaction is in the mempool+ MemRef+ { -- | time it entered the mempool+ timestamp :: !UnixTime } deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData) @@ -322,6 +366,7 @@ put = serialize get = deserialize +-- | Whether a position is in a block rather than in the mempool. confirmed :: BlockRef -> Bool confirmed BlockRef {} = True confirmed MemRef {} = False@@ -411,7 +456,8 @@ utxo :: !Word64, -- | number of transactions txs :: !Word64,- -- | total amount from all outputs in this address+ -- | total amount from all outputs in this address, including unconfirmed+ -- and spent ones received :: !Word64 } deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData)@@ -442,6 +488,7 @@ put = serialize get = deserialize +-- | Balance of an address that was never used. zeroBalance :: Address -> Balance zeroBalance a = Balance@@ -453,6 +500,7 @@ received = 0 } +-- | Whether a balance is that of an address that was never used. nullBalance :: Balance -> Bool nullBalance Balance@@ -498,10 +546,15 @@ -- | Unspent output. data Unspent = Unspent- { block :: !BlockRef,+ { -- | position of the transaction that created it+ block :: !BlockRef,+ -- | the output outpoint :: !OutPoint,+ -- | amount in satoshi value :: !Word64,+ -- | output script script :: !ByteString,+ -- | address of the output script, if it has one address :: !(Maybe Address) } deriving (Show, Eq, Ord, Generic, Hashable, NFData)@@ -739,20 +792,34 @@ unmarshalValue net = parseJSON +-- | Transaction input with information about the output it spends. data StoreInput- = StoreCoinbase- { outpoint :: !OutPoint,+ = -- | input of a coinbase transaction+ StoreCoinbase+ { -- | null outpoint+ outpoint :: !OutPoint,+ -- | sequence number sequence :: !Word32,+ -- | coinbase data script :: !ByteString,+ -- | witness stack witness :: !WitnessStack }- | StoreInput- { outpoint :: !OutPoint,+ | -- | input that spends an output+ StoreInput+ { -- | output spent+ outpoint :: !OutPoint,+ -- | sequence number sequence :: !Word32,+ -- | input script script :: !ByteString,+ -- | script of the output spent pkscript :: !ByteString,+ -- | amount of the output spent, in satoshi value :: !Word64,+ -- | witness stack witness :: !WitnessStack,+ -- | address of the output spent, if it has one address :: !(Maybe Address) } deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData)@@ -801,9 +868,11 @@ put = serialize get = deserialize +-- | Whether a transaction is a coinbase: it has one input, which spends the null outpoint. isCoinbaseTx :: Tx -> Bool isCoinbaseTx = all ((== nullOutPoint) . (.outpoint)) . (.inputs) +-- | Whether an input is that of a coinbase transaction. isCoinbase :: StoreInput -> Bool isCoinbase StoreCoinbase {} = True isCoinbase StoreInput {} = False@@ -878,6 +947,7 @@ >>= maybe (return Nothing) (optional . unmarshalValue net) return StoreInput {..} +-- | Parse a hex string in JSON. jsonHex :: Text -> Parser ByteString jsonHex s = case decodeHex s of@@ -927,9 +997,13 @@ -- | Output information. data StoreOutput = StoreOutput- { value :: !Word64,+ { -- | amount in satoshi+ value :: !Word64,+ -- | output script script :: !ByteString,+ -- | input that spends it, if spent spender :: !(Maybe Spender),+ -- | address of the output script, if it has one address :: !(Maybe Address) } deriving (Show, Read, Eq, Ord, Generic, Hashable, NFData)@@ -985,8 +1059,11 @@ >>= maybe (return Nothing) (optional . unmarshalValue net) return StoreOutput {..} +-- | Output spent by an input, as stored with the transaction. data Prev = Prev- { script :: !ByteString,+ { -- | output script+ script :: !ByteString,+ -- | amount in satoshi value :: !Word64 } deriving (Show, Eq, Ord, Generic, Hashable, NFData)@@ -1008,6 +1085,7 @@ put = serialize get = deserialize +-- | Input with information about the output it spends, which is missing for a coinbase. toInput :: Network -> Ctx -> TxIn -> Maybe Prev -> WitnessStack -> StoreInput toInput _ _ i Nothing w = StoreCoinbase@@ -1027,6 +1105,7 @@ address = eitherToMaybe (scriptToAddressBS net ctx p.script) } +-- | Output with its address and spender. toOutput :: Network -> Ctx -> TxOut -> Maybe Spender -> StoreOutput toOutput net ctx o s = StoreOutput@@ -1036,13 +1115,21 @@ address = eitherToMaybe (scriptToAddressBS net ctx o.script) } +-- | Transaction as stored in the database. data TxData = TxData- { block :: !BlockRef,+ { -- | position in the chain or mempool+ block :: !BlockRef,+ -- | the transaction tx :: !Tx,+ -- | outputs spent by each input, by input index prevs :: !(IntMap Prev),+ -- | the transaction was removed, for example by a double spend deleted :: !Bool,+ -- | the transaction can be replaced by fee rbf :: !Bool,+ -- | time it was first seen, or time of its block timestamp :: !Word64,+ -- | inputs that spend each output, by output index spenders :: !(IntMap Spender) } deriving (Show, Eq, Ord, Generic, NFData)@@ -1074,6 +1161,7 @@ put = serialize get = deserialize +-- | Fee of a transaction: inputs minus outputs, or zero for a coinbase. txDataFee :: TxData -> Word64 txDataFee t = if isCoinbaseTx t.tx@@ -1124,6 +1212,7 @@ f n i w = toInput net ctx i (IntMap.lookup n t.prevs) w g n o = toOutput net ctx o $ IntMap.lookup n t.spenders +-- | Inverse of 'toTransaction'. fromTransaction :: Transaction -> TxData fromTransaction t = TxData@@ -1215,6 +1304,7 @@ put = serialize get = deserialize +-- | The transaction without the information added by the store. transactionData :: Transaction -> Tx transactionData t = Tx@@ -1358,7 +1448,10 @@ -- | Address balances for an extended public key. data XPubBal = XPubBal- { path :: ![KeyIndex],+ { -- | derivation path below the xpub: chain (@0@ external, @1@ change) and+ -- index+ path :: ![KeyIndex],+ -- | balance of the address balance :: !Balance } deriving (Show, Ord, Eq, Generic, NFData)@@ -1402,7 +1495,9 @@ -- | Unspent transaction for extended public key. data XPubUnspent = XPubUnspent- { unspent :: !Unspent,+ { -- | the unspent output+ unspent :: !Unspent,+ -- | derivation path of its address below the xpub path :: ![KeyIndex] } deriving (Show, Eq, Ord, Generic, NFData)@@ -1444,12 +1539,19 @@ path <- o .: "path" return XPubUnspent {..} +-- | Summary of the balances of an extended public key. data XPubSummary = XPubSummary- { confirmed :: !Word64,+ { -- | confirmed balance in satoshi+ confirmed :: !Word64,+ -- | unconfirmed balance in satoshi unconfirmed :: !Word64,+ -- | total received by external addresses, in satoshi received :: !Word64,+ -- | number of unspent outputs utxo :: !Word64,+ -- | index of the last used external address external :: !Word32,+ -- | index of the last used change address change :: !Word32 } deriving (Eq, Show, Generic, NFData)@@ -1527,12 +1629,18 @@ external <- i .: "external" return XPubSummary {..} +-- | Result of a health check. class Healthy a where+ -- | Whether the check passed. isOK :: a -> Bool +-- | Whether the database keeps up with the headers. data BlockHealth = BlockHealth- { headers :: !BlockHeight,+ { -- | height of the best header+ headers :: !BlockHeight,+ -- | height of the best block in the database blocks :: !BlockHeight,+ -- | blocks the database may lag behind max :: !Int32 } deriving (Show, Eq, Generic, NFData)@@ -1587,8 +1695,11 @@ max <- o .: "max" return BlockHealth {..} +-- | Whether something happened recently enough. data TimeHealth = TimeHealth- { age :: !Int64,+ { -- | seconds since it happened+ age :: !Int64,+ -- | maximum seconds allowed max :: !Int64 } deriving (Show, Eq, Generic, NFData)@@ -1633,8 +1744,11 @@ max <- o .: "max" return TimeHealth {..} +-- | Whether there are enough of something. data CountHealth = CountHealth- { count :: !Int64,+ { -- | how many there are+ count :: !Int64,+ -- | fewest allowed min :: !Int64 } deriving (Show, Eq, Generic, NFData)@@ -1678,8 +1792,11 @@ min <- o .: "min" return CountHealth {..} +-- | Whether there are few enough of something. data MaxHealth = MaxHealth- { count :: !Int64,+ { -- | how many there are+ count :: !Int64,+ -- | most allowed max :: !Int64 } deriving (Show, Eq, Generic, NFData)@@ -1723,14 +1840,24 @@ max <- o .: "max" return MaxHealth {..} +-- | Health of the store, as returned by @\/health@. It is healthy if every check+-- passes. data HealthCheck = HealthCheck- { blocks :: !BlockHealth,+ { -- | the database keeps up with the headers+ blocks :: !BlockHealth,+ -- | the best header is recent lastBlock :: !TimeHealth,+ -- | a transaction or block arrived recently lastTx :: !TimeHealth,+ -- | few transactions wait to be imported pendingTxs :: !MaxHealth,+ -- | enough peers are connected peers :: !CountHealth,+ -- | network name network :: !String,+ -- | version of the software version :: !String,+ -- | time of the check time :: !Word64 } deriving (Show, Eq, Generic, NFData)@@ -1806,9 +1933,12 @@ let time = round $ utcTimeToPOSIXSeconds utcTime return HealthCheck {..} +-- | Event streamed by @\/events@. data Event- = EventBlock !BlockHash- | EventTx !TxHash+ = -- | a new best block+ EventBlock !BlockHash+ | -- | a transaction entered or left the mempool+ EventTx !TxHash deriving (Show, Eq, Generic, NFData) instance Serial Event where@@ -1851,6 +1981,7 @@ return $ EventBlock i _ -> fail $ "Could not recognize event type: " <> t +-- | Response that wraps a single value, encoded in JSON as @{\"result\": value}@. newtype GenericResult a = GenericResult {get :: a} deriving (Show, Eq, Generic, NFData) @@ -1874,6 +2005,8 @@ parseJSON = A.withObject "GenericResult" $ \o -> GenericResult <$> o .: "result" +-- | Response with a value in its peer-to-peer serialization, encoded in JSON as+-- a hex string in @{\"result\": hex}@. newtype RawResult a = RawResult {get :: a} deriving (Show, Eq, Generic, NFData) @@ -1904,6 +2037,7 @@ =<< decodeHex res maybe mzero (return . RawResult) m +-- | List response. In binary, a length followed by the items. newtype SerialList a = SerialList {get :: [a]} deriving (Show, Eq, Generic, NFData) @@ -1924,6 +2058,8 @@ instance (FromJSON a) => FromJSON (SerialList a) where parseJSON = fmap SerialList . parseJSON +-- | List of values in their peer-to-peer serialization, encoded in JSON as a+-- list of hex strings. newtype RawResultList a = RawResultList {get :: [a]} deriving (Show, Eq, Generic, NFData) @@ -1967,6 +2103,8 @@ A.withText "RawResultListItem" $ \t -> maybe mzero return (decodeHex t >>= deser) +-- | Hash of a transaction, as returned when posting one, encoded in JSON as+-- @{\"txid\": hash}@. newtype TxId = TxId TxHash deriving (Show, Eq, Generic, NFData)@@ -1990,15 +2128,24 @@ instance FromJSON TxId where parseJSON = A.withObject "txid" $ \o -> TxId <$> o .: "txid" +-- | Errors returned by the API. data Except- = ThingNotFound- | ServerError- | BadRequest- | UserError !String- | StringError !String- | TxIndexConflict ![TxHash]- | ServerTimeout- | RequestTooLarge+ = -- | the resource does not exist (404)+ ThingNotFound+ | -- | internal error (500)+ ServerError+ | -- | the request is malformed (400)+ BadRequest+ | -- | the request is invalid, with a reason (400)+ UserError !String+ | -- | other error, with a message (400)+ StringError !String+ | -- | a short transaction number matches these transactions (409)+ TxIndexConflict ![TxHash]+ | -- | the request took too long (500)+ ServerTimeout+ | -- | the request asks for too much (413)+ RequestTooLarge deriving (Show, Eq, Ord, Generic, NFData) instance Exception Except@@ -2070,15 +2217,21 @@ -- Blockchain.info API Compatibility -- --------------------------------------- +-- | Short numeric identifier of a transaction, as used by Blockchain.info: the+-- first eight bytes of its hash, read as a big-endian number, shifted right by+-- eleven bits. toIntTxId :: TxHash -> Word64 toIntTxId h = let bs = runPutS (serialize h) Right w64 = runGetS getWord64be bs in w64 `shift` (-11) +-- | Block identified by hash or by height. data BinfoBlockId- = BinfoBlockHash !BlockHash- | BinfoBlockIndex !Word32+ = -- | by hash+ BinfoBlockHash !BlockHash+ | -- | by height+ BinfoBlockIndex !Word32 deriving (Eq, Show, Read, Generic, NFData) instance Parsable BinfoBlockId where@@ -2090,11 +2243,15 @@ Just h -> Right $ BinfoBlockHash h igr = BinfoBlockIndex <$> parseParam t +-- | Transaction identified by hash or by short number; see 'toIntTxId'. data BinfoTxId- = BinfoTxIdHash !TxHash- | BinfoTxIdIndex !Word64+ = -- | by hash+ BinfoTxIdHash !TxHash+ | -- | by short number+ BinfoTxIdIndex !Word64 deriving (Eq, Show, Read, Generic, NFData) +-- | Identify a transaction by short number if the flag is 'True', or by hash otherwise. encodeBinfoTxId :: Bool -> TxHash -> BinfoTxId encodeBinfoTxId False = BinfoTxIdHash encodeBinfoTxId True = BinfoTxIdIndex . toIntTxId@@ -2120,13 +2277,20 @@ BinfoTxIdHash <$> parseJSON v <|> BinfoTxIdIndex <$> parseJSON v +-- | Which transactions to list in a Blockchain.info response (@filter@). data BinfoFilter- = BinfoFilterAll- | BinfoFilterSent- | BinfoFilterReceived- | BinfoFilterMoved- | BinfoFilterConfirmed- | BinfoFilterMempool+ = -- | all of them (@0@ or @6@)+ BinfoFilterAll+ | -- | those that decrease the balance (@1@)+ BinfoFilterSent+ | -- | those that increase the balance (@2@)+ BinfoFilterReceived+ | -- | those that leave the balance unchanged except for fees (@3@)+ BinfoFilterMoved+ | -- | confirmed ones (@5@)+ BinfoFilterConfirmed+ | -- | unconfirmed ones (@7@)+ BinfoFilterMempool deriving (Eq, Show, Generic, NFData) instance Parsable BinfoFilter where@@ -2141,12 +2305,19 @@ 7 -> return BinfoFilterMempool _ -> Left "could not parse filter parameter" +-- | Response of @\/blockchain\/multiaddr@. data BinfoMultiAddr = BinfoMultiAddr- { addresses :: ![BinfoBalance],+ { -- | balance of each address and xpub requested+ addresses :: ![BinfoBalance],+ -- | totals of all of them wallet :: !BinfoWallet,+ -- | page of their transactions, newest first txs :: ![BinfoTx],+ -- | information about the chain and prices info :: !BinfoInfo,+ -- | always 'True' recommendFee :: !Bool,+ -- | addresses are shown in CashAddr format cashAddr :: !Bool } deriving (Eq, Show, Generic, NFData)@@ -2187,13 +2358,21 @@ where net = if not m.cashAddr && net' == bch then btc else net' +-- | Response of @\/blockchain\/rawaddr\/:addr@. data BinfoRawAddr = BinfoRawAddr- { address :: !BinfoAddr,+ { -- | address or xpub+ address :: !BinfoAddr,+ -- | balance in satoshi balance :: !Word64,+ -- | number of transactions ntx :: !Word64,+ -- | number of unspent outputs utxo :: !Word64,+ -- | total received in satoshi received :: !Word64,+ -- | total sent in satoshi sent :: !Int64,+ -- | page of transactions, newest first txs :: ![BinfoTx] } deriving (Eq, Show, Generic, NFData)@@ -2256,9 +2435,13 @@ txs <- mapM (unmarshalValue (net, ctx)) =<< o .: "txs" return BinfoRawAddr {..} +-- | Balance of an address or xpub in @\/blockchain\/balance@. data BinfoShortBal = BinfoShortBal- { final :: !Word64,+ { -- | balance in satoshi+ final :: !Word64,+ -- | number of transactions ntx :: !Word64,+ -- | total received in satoshi received :: !Word64 } deriving (Eq, Show, Read, Generic, NFData)@@ -2286,21 +2469,36 @@ received <- o .: "total_received" return BinfoShortBal {..} +-- | Balance of an address or xpub in @\/blockchain\/multiaddr@. data BinfoBalance- = BinfoAddrBalance- { address :: !Address,+ = -- | balance of an address+ BinfoAddrBalance+ { -- | the address+ address :: !Address,+ -- | number of transactions txs :: !Word64,+ -- | total received in satoshi received :: !Word64,+ -- | total sent in satoshi sent :: !Word64,+ -- | balance in satoshi balance :: !Word64 }- | BinfoXPubBalance- { xpub :: !XPubKey,+ | -- | balance of an xpub+ BinfoXPubBalance+ { -- | the xpub+ xpub :: !XPubKey,+ -- | number of transactions txs :: !Word64,+ -- | total received in satoshi received :: !Word64,+ -- | total sent in satoshi sent :: !Word64,+ -- | balance in satoshi balance :: !Word64,+ -- | index of the last used external address external :: !Word32,+ -- | index of the last used change address change :: !Word32 } deriving (Eq, Show, Generic, NFData)@@ -2375,11 +2573,17 @@ A.withArray "[BinfoBalance]" $ fmap V.toList . mapM (unmarshalValue (net, ctx)) +-- | Totals of all addresses and xpubs in @\/blockchain\/multiaddr@. data BinfoWallet = BinfoWallet- { balance :: !Word64,+ { -- | balance in satoshi+ balance :: !Word64,+ -- | number of transactions txs :: !Word64,+ -- | number of transactions that pass the filter filtered :: !Word64,+ -- | total received in satoshi received :: !Word64,+ -- | total sent in satoshi sent :: !Word64 } deriving (Eq, Show, Generic, NFData)@@ -2413,6 +2617,7 @@ sent <- o .: "total_sent" return BinfoWallet {..} +-- | Amount as a big-endian hex string without leading zeros, as Blockchain.info shows values of unspent outputs. binfoHexValue :: Word64 -> Text binfoHexValue w64 = encodeHex $@@ -2425,13 +2630,21 @@ runPutS $ serialize w64 +-- | Unspent output in @\/blockchain\/unspent@. data BinfoUnspent = BinfoUnspent- { txid :: !TxHash,+ { -- | hash of the transaction+ txid :: !TxHash,+ -- | index of the output index :: !Word32,+ -- | output script script :: !ByteString,+ -- | amount in satoshi value :: !Word64,+ -- | number of confirmations; @0@ if unconfirmed confirmations :: !Int32,+ -- | identifier of the transaction txidx :: !BinfoTxId,+ -- | xpub and path of its address, if it belongs to an xpub requested xpub :: !(Maybe BinfoXPubPath) } deriving (Eq, Show, Generic, NFData)@@ -2477,6 +2690,7 @@ xpub <- mapM (unmarshalValue (net, ctx)) =<< o .:? "xpub" return BinfoUnspent {..} +-- | Response of @\/blockchain\/unspent@. newtype BinfoUnspents = BinfoUnspents [BinfoUnspent] deriving (Eq, Show, Generic, NFData) @@ -2499,6 +2713,7 @@ us <- mapM (unmarshalValue (net, ctx)) =<< o .: "unspent_outputs" return (BinfoUnspents us) +-- | Block in the Blockchain.info format, from its data, its transactions and the hashes of its children. toBinfoBlock :: BlockData -> [BinfoTx] -> [BlockHash] -> BinfoBlock toBinfoBlock b transactions next_blocks = BinfoBlock@@ -2520,22 +2735,39 @@ txs = transactions } +-- | Block in the Blockchain.info format. data BinfoBlock = BinfoBlock- { hash :: !BlockHash,+ { -- | block hash+ hash :: !BlockHash,+ -- | block version version :: !Word32,+ -- | hash of the previous block prev :: !BlockHash,+ -- | Merkle root of the transactions merkle :: !Hash256,+ -- | block timestamp timestamp :: !Word32,+ -- | difficulty target in compact form bits :: !Word32,+ -- | hashes of the children of the block next :: ![BlockHash],+ -- | total fees in satoshi fee :: !Word64,+ -- | nonce nonce :: !Word32,+ -- | number of transactions ntx :: !Word32,+ -- | size in bytes size :: !Word32,+ -- | block index; the height index :: !Word32,+ -- | whether it is in the main chain main :: !Bool,+ -- | height height :: !Word32,+ -- | weight weight :: !Word32,+ -- | transactions txs :: ![BinfoTx] } deriving (Eq, Show, Generic, NFData)@@ -2613,24 +2845,44 @@ A.withObject "blocks" $ \o -> mapM (unmarshalValue (net, ctx)) =<< o .: "blocks" +-- | Transaction in the Blockchain.info format. data BinfoTx = BinfoTx- { txid :: !TxHash,+ { -- | hash+ txid :: !TxHash,+ -- | version version :: !Word32,+ -- | number of inputs inputCount :: !Word32,+ -- | number of outputs outputCount :: !Word32,+ -- | size in bytes size :: !Word32,+ -- | weight weight :: !Word32,+ -- | fee in satoshi fee :: !Word64,+ -- | IP address of the peer that relayed it; always @0.0.0.0@ relayed :: !ByteString,+ -- | lock time locktime :: !Word32,+ -- | identifier index :: !BinfoTxId,+ -- | whether it was removed by a double spend doubleSpend :: !Bool,+ -- | whether it can be replaced by fee rbf :: !Bool,+ -- | net change to the balance of the addresses requested, and the+ -- balance after it; only in responses about addresses balance :: !(Maybe (Int64, Int64)),+ -- | time first seen, or time of its block timestamp :: !Word64,+ -- | height of its block, if confirmed blockIndex :: !(Maybe Word32),+ -- | height of its block, if confirmed blockHeight :: !(Maybe Word32),+ -- | inputs inputs :: ![BinfoTxInput],+ -- | outputs outputs :: ![BinfoTxOutput] } deriving (Eq, Show, Generic, NFData)@@ -2726,11 +2978,17 @@ A.withArray "[BinfoTx]" $ fmap V.toList . mapM (unmarshalValue (net, ctx)) +-- | Transaction input in the Blockchain.info format. data BinfoTxInput = BinfoTxInput- { sequence :: !Word32,+ { -- | sequence number+ sequence :: !Word32,+ -- | serialized witness stack witness :: !ByteString,+ -- | input script script :: !ByteString,+ -- | index of the input index :: !Word32,+ -- | output that it spends output :: !BinfoTxOutput } deriving (Eq, Show, Generic, NFData)@@ -2770,15 +3028,25 @@ >>= unmarshalValue (net, ctx) return BinfoTxInput {..} +-- | Transaction output in the Blockchain.info format. data BinfoTxOutput = BinfoTxOutput- { typ :: !Int,+ { -- | type; always @0@+ typ :: !Int,+ -- | whether it is spent spent :: !Bool,+ -- | amount in satoshi value :: !Word64,+ -- | index of the output index :: !Word32,+ -- | identifier of its transaction txidx :: !BinfoTxId,+ -- | output script script :: !ByteString,+ -- | inputs that spend it spenders :: ![BinfoSpender],+ -- | address of the output script, if it has one address :: !(Maybe Address),+ -- | xpub and path of its address, if it belongs to an xpub requested xpub :: !(Maybe BinfoXPubPath) } deriving (Eq, Show, Generic, NFData)@@ -2832,8 +3100,11 @@ xpub <- o .:? "xpub" >>= mapM (unmarshalValue (net, ctx)) return BinfoTxOutput {..} +-- | Input that spends an output, in the Blockchain.info format. data BinfoSpender = BinfoSpender- { txidx :: !BinfoTxId,+ { -- | identifier of the spending transaction+ txidx :: !BinfoTxId,+ -- | index of the input input :: !Word32 } deriving (Eq, Show, Generic, NFData)@@ -2858,8 +3129,11 @@ input <- o .: "n" return BinfoSpender {..} +-- | Xpub that an address belongs to, with its derivation path. data BinfoXPubPath = BinfoXPubPath- { key :: !XPubKey,+ { -- | the xpub+ key :: !XPubKey,+ -- | derivation path of the address below the xpub deriv :: !SoftPath } deriving (Eq, Show, Generic, NFData)@@ -2892,11 +3166,17 @@ deriv <- fromMaybe "bad xpub path" . parseSoft <$> o .: "path" return BinfoXPubPath {..} +-- | Information about the chain and prices in @\/blockchain\/multiaddr@. data BinfoInfo = BinfoInfo- { connected :: !Word32,+ { -- | number of connected peers+ connected :: !Word32,+ -- | satoshi per unit of the fiat currency conversion :: !Double,+ -- | fiat currency fiat :: !BinfoSymbol,+ -- | coin of the network crypto :: !BinfoSymbol,+ -- | best block head :: !BinfoBlockInfo } deriving (Eq, Show, Generic, NFData)@@ -2930,10 +3210,15 @@ head <- o .: "latest_block" return BinfoInfo {..} +-- | Short information about a block in the Blockchain.info format. data BinfoBlockInfo = BinfoBlockInfo- { hash :: !BlockHash,+ { -- | block hash+ hash :: !BlockHash,+ -- | height height :: !BlockHeight,+ -- | block timestamp timestamp :: !Word32,+ -- | block index; the height index :: !BlockHeight } deriving (Eq, Show, Generic, NFData)@@ -2964,6 +3249,7 @@ index <- o .: "block_index" return BinfoBlockInfo {..} +-- | Short information about a block. toBinfoBlockInfo :: BlockData -> BinfoBlockInfo toBinfoBlockInfo d = BinfoBlockInfo@@ -2973,9 +3259,13 @@ index = d.height } +-- | Historical price of the coin of the network, from the price service. data BinfoRate = BinfoRate- { timestamp :: !Word64,+ { -- | time of the price+ timestamp :: !Word64,+ -- | price in the fiat currency price :: !Double,+ -- | volume in the 24 hours before vol24 :: !Double } deriving (Eq, Show, Generic, NFData)@@ -3003,15 +3293,25 @@ vol24 <- o .: "volume24h" return BinfoRate {..} +-- | Entry of @\/blockchain\/export-history@. data BinfoHistory = BinfoHistory- { date :: !Text,+ { -- | date, as @YYYY-MM-DD@+ date :: !Text,+ -- | time of day time :: !Text,+ -- | @sent@ or @received@ typ :: !Text,+ -- | amount in coins amount :: !Double,+ -- | value in fiat at the time valueThen :: !Double,+ -- | value in fiat now valueNow :: !Double,+ -- | price at the time rateThen :: !Double,+ -- | transaction hash txid :: !TxHash,+ -- | fee in coins fee :: !Double } deriving (Eq, Show, Generic, NFData)@@ -3057,6 +3357,8 @@ fee <- o .: "fee" return BinfoHistory {..} +-- | History entry from a net amount in satoshi, a time, the price then and now, a+-- fee in satoshi, and a transaction hash. toBinfoHistory :: Int64 -> Word64 ->@@ -3086,6 +3388,8 @@ v1 = v * r1 v2 = v * r2 +-- | Date given as @DD-MM-YYYY@ or @DD\/MM\/YYYY@ in a request, as seconds+-- since the Unix epoch. newtype BinfoDate = BinfoDate Word64 deriving (Eq, Show, Read, Generic, NFData) @@ -3098,11 +3402,18 @@ s = LazyText.unpack t p fmt = parseTimeM False defaultTimeLocale fmt s +-- | Price of the coin of the network in a fiat currency, from the ticker+-- service. data BinfoTicker = BinfoTicker- { fifteen :: !Double,+ { -- | price delayed by fifteen minutes+ fifteen :: !Double,+ -- | last price last :: !Double,+ -- | buy price buy :: !Double,+ -- | sell price sell :: !Double,+ -- | currency symbol symbol :: !Text } deriving (Eq, Show, Generic, NFData)@@ -3146,12 +3457,19 @@ last <- o .: "last" return BinfoTicker {..} +-- | Currency in the Blockchain.info format. data BinfoSymbol = BinfoSymbol- { code :: !Text,+ { -- | currency code, such as @USD@+ code :: !Text,+ -- | symbol, such as @$@ symbol :: !Text,+ -- | name name :: !Text,+ -- | satoshi per unit conversion :: !Double,+ -- | whether the symbol goes after the amount after :: !Bool,+ -- | whether it is a fiat currency local :: !Bool } deriving (Eq, Show, Generic, NFData)@@ -3199,6 +3517,8 @@ local <- o .: "local" return BinfoSymbol {..} +-- | Transactions to fetch to show a transaction: those it spends, and, if outputs+-- are pruned, those that spend its outputs to other addresses. relevantTxs :: HashSet Address -> Bool ->@@ -3221,6 +3541,7 @@ g StoreInput {outpoint = OutPoint h i} = Just h ins = mapMaybe g t.inputs +-- | Balances of addresses and xpubs in the Blockchain.info format. toBinfoAddrs :: HashMap Address Balance -> HashMap XPubSpec [XPubBal] ->@@ -3262,6 +3583,7 @@ in BinfoAddrBalance {..} in map f $ HashMap.elems onlyAddrs +-- | Transaction in the Blockchain.info format, without balance information. toBinfoTxSimple :: Bool -> Transaction ->@@ -3269,6 +3591,7 @@ toBinfoTxSimple numtxid = toBinfoTx numtxid HashMap.empty False 0 +-- | Inputs of a transaction in the Blockchain.info format. toBinfoTxInputs :: Bool -> HashMap Address (Maybe BinfoXPubPath) ->@@ -3297,11 +3620,16 @@ putVarInt (B.length bs) putByteString bs +-- | Height of the block of a transaction, or 'Nothing' if unconfirmed. transactionHeight :: Transaction -> Maybe BlockHeight transactionHeight Transaction {deleted = True} = Nothing transactionHeight Transaction {block = MemRef _} = Nothing transactionHeight Transaction {block = BlockRef h _} = Just h +-- | Transaction in the Blockchain.info format. Arguments: identify transactions+-- by short number; address book of the addresses requested and their xpubs;+-- prune outputs to other addresses of a transaction that increases the balance;+-- balance after the transaction. toBinfoTx :: Bool -> HashMap Address (Maybe BinfoXPubPath) ->@@ -3341,6 +3669,7 @@ aset = HashMap.keysSet abook simple = HashMap.null abook && bal == 0 +-- | Net change that a transaction makes to the balance of a set of addresses. getTxResult :: HashSet Address -> Transaction -> Int64 getTxResult aset t = inputSum + outputSum@@ -3364,6 +3693,7 @@ then fromIntegral value else 0 +-- | Output in the Blockchain.info format, or 'Nothing' if it is pruned. toBinfoTxOutput :: Bool -> HashMap Address (Maybe BinfoXPubPath) ->@@ -3391,6 +3721,7 @@ where notInBook = isNothing $ o.address >>= flip HashMap.lookup abook +-- | Spender in the Blockchain.info format. toBinfoSpender :: Bool -> Spender -> BinfoSpender toBinfoSpender numtxid s = BinfoSpender@@ -3398,6 +3729,7 @@ input = s.index } +-- | Output spent by an input, in the Blockchain.info format. inputToBinfoTxOutput :: Bool -> HashMap Address (Maybe BinfoXPubPath) ->@@ -3434,11 +3766,15 @@ StoreInput {address} -> address >>= join . flip HashMap.lookup abook } +-- | Address or xpub in a Blockchain.info request. data BinfoAddr- = BinfoAddr !Address- | BinfoXpub !XPubKey+ = -- | an address+ BinfoAddr !Address+ | -- | an xpub+ BinfoXpub !XPubKey deriving (Eq, Show, Generic, Hashable, NFData) +-- | Parse addresses and xpubs separated by pipes or commas. Returns 'Nothing' if any is invalid. parseBinfoAddr :: Network -> Ctx -> Text -> Maybe [BinfoAddr] parseBinfoAddr net ctx "" = Just [] parseBinfoAddr net ctx s =@@ -3449,11 +3785,17 @@ BinfoAddr <$> textToAddr net x <|> BinfoXpub <$> xPubImport net ctx x +-- | Block header with its transactions in the Blockchain.info format. data BinfoHeader = BinfoHeader- { hash :: !BlockHash,+ { -- | block hash+ hash :: !BlockHash,+ -- | block timestamp timestamp :: !Timestamp,+ -- | block index; the height index :: !Word32,+ -- | height height :: !BlockHeight,+ -- | identifiers of its transactions txids :: ![BinfoTxId] } deriving (Eq, Show, Generic, NFData)@@ -3487,6 +3829,7 @@ txids <- o .: "txIndexes" return BinfoHeader {..} +-- | Response of @\/blockchain\/unconfirmed-transactions@. newtype BinfoMempool = BinfoMempool {get :: [BinfoTx]} deriving (Eq, Show, Generic, NFData) @@ -3501,6 +3844,7 @@ A.withObject "BinfoMempool" $ \o -> BinfoMempool <$> (mapM (unmarshalValue (net, ctx)) =<< o .: "txs") +-- | List of short block information. newtype BinfoBlockInfos = BinfoBlockInfos {get :: [BinfoBlockInfo]} deriving (Eq, Show, Generic, NFData)
src/Haskoin/Store/WebClient.hs view
@@ -8,11 +8,39 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NoFieldSelectors #-} +-- |+-- Module : Haskoin.Store.WebClient+-- Copyright : (c) Haskoin Developers+-- License : MIT+-- Maintainer : jprupp@protonmail.ch+-- Stability : experimental+-- Portability : POSIX+--+-- Client for the REST API of Haskoin Store.+--+-- Build a request value, such as 'GetAddrBalance', and pass it to 'apiCall'.+-- The response type follows from the request type. Requests and responses use+-- the compact binary encoding of the API. Errors returned by the server are+-- thrown as 'Store.Except' in 'MonadError'.+--+-- @+-- import Control.Monad.Except (runExceptT)+-- import Data.Default (def)+--+-- balance :: Ctx -> Address -> IO (Either Except Balance)+-- balance ctx addr = runExceptT $ 'apiCall' ctx def ('GetAddrBalance' addr)+-- @+--+-- Requests that take long lists of addresses, transactions or blocks can be+-- split into several calls with 'apiBatch'.+-- The client always uses the binary encoding, which is smaller and faster than+-- JSON and decodes straight into the types of "Haskoin.Store.Data". module Haskoin.Store.WebClient- ( ApiConfig (..),+ ( -- * Calls+ ApiConfig (..), apiCall, apiBatch,- -- Blocks+ -- * Blocks GetBlock (..), GetBlocks (..), GetBlockRaw (..),@@ -24,7 +52,7 @@ GetBlockHeightRaw (..), GetBlockTime (..), GetBlockTimeRaw (..),- -- Transactions+ -- * Transactions GetTx (..), GetTxs (..), GetTxRaw (..),@@ -35,7 +63,7 @@ PostTx (..), GetMempool (..), GetEvents (..),- -- Address+ -- * Addresses GetAddrTxs (..), GetAddrsTxs (..), GetAddrTxsFull (..),@@ -44,17 +72,17 @@ GetAddrsBalance (..), GetAddrUnspent (..), GetAddrsUnspent (..),- -- XPubs+ -- * Extended public keys GetXPub (..), GetXPubTxs (..), GetXPubTxsFull (..), GetXPubBalances (..), GetXPubUnspent (..), DelCachedXPub (..),- -- Network+ -- * Network GetPeers (..), GetHealth (..),- -- Params+ -- * Parameters StartParam (..), OffsetParam (..), LimitParam (..),@@ -105,7 +133,9 @@ -- } -- @ data ApiConfig = ApiConfig- { net :: !Network,+ { -- | network to query; its name is appended to the host+ net :: !Network,+ -- | base URL of the API, ending in a slash host :: !String } deriving (Eq, Show)@@ -125,7 +155,9 @@ -- -- With options: ----- > apiCall ctx def $ GetAddrsUnspent addrs def{ paramLimit = Just 10 }+-- > apiCall ctx def $ GetAddrsUnspent addrs def {limit = Just 10}+--+-- Only @GET@ and @POST@ endpoints are supported. apiCall :: (ApiResource a b, MonadIO m, MonadError Store.Except m) => Ctx ->@@ -146,7 +178,10 @@ -- | Batch commands that have a large list of arguments: ----- > apiBatch 20 def (GetAddrsTxs addrs def)+-- > apiBatch ctx 20 def (GetAddrsTxs addrs def)+--+-- The list is split into chunks of the given size, one call is made per chunk,+-- and the results are concatenated. apiBatch :: (Batchable a b, MonadIO m, MonadError Store.Except m) => Ctx ->@@ -157,7 +192,10 @@ apiBatch ctx i conf res = mconcat <$> mapM (apiCall ctx conf) (resourceBatch i res) +-- | Requests that can be split into several smaller ones whose results are+-- concatenated. class (ApiResource a b, Monoid b) => Batchable a b where+ -- | Split a request into requests with lists of at most this size. resourceBatch :: Natural -> a -> [a] instance Batchable GetBlocks (Store.SerialList Store.BlockData) where@@ -197,6 +235,7 @@ -- API Internal -- ------------------ +-- | HTTP options carrying the query string parameters of a request. toOptions :: (ApiResource a b) => Network ->@@ -208,6 +247,7 @@ where f (ParamBox p) = toOption net ctx p +-- | HTTP option for one query string parameter. toOption :: (Param a) => Network ->@@ -220,9 +260,11 @@ encodeParam net ctx a return $ applyOpt (paramLabel a) res +-- | Set a query string parameter to comma-separated values. applyOpt :: Text -> [Text] -> Endo HTTP.Options applyOpt p t = Endo $ HTTP.param p .~ [Text.intercalate "," t] +-- | Make a @GET@ request and decode the binary response. getBinary :: (Serial a) => Endo HTTP.Options ->@@ -234,6 +276,7 @@ res <- resE return . runGetL deserialize $ res ^. HTTP.responseBody +-- | Make a @POST@ request with a binary body and decode the binary response. postBinary :: (Serial a, Serial r) => Endo HTTP.Options ->@@ -246,6 +289,7 @@ res <- resE return . runGetL deserialize $ res ^. HTTP.responseBody +-- | Request binary responses, and turn error responses into exceptions. binaryOpts :: Endo HTTP.Options -> HTTP.Options binaryOpts opts = appEndo (opts <> accept <> stat) HTTP.defaults@@ -253,6 +297,9 @@ accept = Endo $ HTTP.header "Accept" .~ ["application/octet-stream"] stat = Endo $ HTTP.checkResponse ?~ checkStatus +-- | Throw the error in the body of an unsuccessful response as 'Store.Except'.+-- A health check that answers 503 is not an error: its body says what is+-- wrong. checkStatus :: ResponseChecker checkStatus req res | statusIsSuccessful status = return ()@@ -273,6 +320,7 @@ -- Utilities -- --------------- +-- | Split a list into chunks of at most this size. chunksOf :: Natural -> [a] -> [[a]] chunksOf n xs | null xs = []
src/Haskoin/Store/WebCommon.hs view
@@ -10,6 +10,27 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NoFieldSelectors #-} +-- |+-- Module : Haskoin.Store.WebCommon+-- Copyright : (c) Haskoin Developers+-- License : MIT+-- Maintainer : jprupp@protonmail.ch+-- Stability : experimental+-- Portability : POSIX+--+-- Description of the REST API, shared by the server ("Haskoin.Store.Web") and+-- the client ("Haskoin.Store.WebClient").+--+-- Each endpoint is a type, such as 'GetBlock', with an 'ApiResource' instance+-- that gives its HTTP method, path, parameters and response type. Parameters+-- are types with a 'Param' instance, which knows how to encode them in a URL+-- and parse them back.+--+-- Endpoints that return lists take a limit, an offset and a start+-- ('LimitsParam'). Results are ordered from newest to oldest.+-- Describing each endpoint once, as a type, keeps the server and the client from+-- drifting apart: the path, parameters and response type that the server routes+-- are the same ones the client builds requests from. module Haskoin.Store.WebCommon where import Control.Applicative ((<|>))@@ -42,21 +63,34 @@ -- API Resources -- ------------------- +-- | An endpoint of the API. The request type @a@ determines the response type+-- @b@. class (Serial b) => ApiResource a b | a -> b where+ -- | HTTP method; 'GET' by default. resourceMethod :: Proxy a -> StdMethod resourceMethod _ = GET+ -- | Path of the endpoint, given the encoded values of the parameters that+ -- are part of the path. resourcePath :: Proxy a -> ([Text] -> Text)+ -- | Parameters of a request: those that go in the path, and those that go in+ -- the query string. queryParams :: a -> ([ParamBox], [ParamBox]) -- (resource, querystring) queryParams _ = ([], [])+ -- | Types of the parameters that go in the path, used to build the route+ -- pattern of the server. captureParams :: Proxy a -> [ProxyBox] captureParams _ = []+ -- | Body of the request, if any. resourceBody :: a -> Maybe PostBox resourceBody = const Nothing +-- | Request body of any serializable type. data PostBox = forall s. (Serial s) => PostBox !s +-- | Parameter value of any type. data ParamBox = forall p. (Eq p, Param p) => ParamBox !p +-- | Parameter type, without a value. data ProxyBox = forall p. (Param p) => ProxyBox !(Proxy p) --------------------@@ -64,86 +98,125 @@ -------------------- -- Blocks+-- | Request for a block by hash: @GET \/block\/:block@. data GetBlock = GetBlock !BlockHash !NoTx +-- | Request for several blocks by hash: @GET \/blocks?blocks=…@. data GetBlocks = GetBlocks ![BlockHash] !NoTx +-- | Request for a block by hash, serialized as in the peer-to-peer protocol: @GET \/block\/:block\/raw@. newtype GetBlockRaw = GetBlockRaw BlockHash +-- | Request for the best block: @GET \/block\/best@. newtype GetBlockBest = GetBlockBest NoTx +-- | Request for the best block, serialized: @GET \/block\/best\/raw@. data GetBlockBestRaw = GetBlockBestRaw +-- | Request for the best block and its ancestors, up to 100 blocks: @GET \/block\/latest@. newtype GetBlockLatest = GetBlockLatest NoTx +-- | Request for the blocks at a height: @GET \/block\/height\/:height@. data GetBlockHeight = GetBlockHeight !HeightParam !NoTx +-- | Request for the blocks at several heights: @GET \/block\/heights?heights=…@. data GetBlockHeights = GetBlockHeights !HeightsParam !NoTx +-- | Request for the blocks at a height, serialized: @GET \/block\/height\/:height\/raw@. newtype GetBlockHeightRaw = GetBlockHeightRaw HeightParam +-- | Request for the last block with a timestamp at or before a time: @GET \/block\/time\/:time@. data GetBlockTime = GetBlockTime !TimeParam !NoTx +-- | Request for the last block with a timestamp at or before a time, serialized: @GET \/block\/time\/:time\/raw@. newtype GetBlockTimeRaw = GetBlockTimeRaw TimeParam +-- | Request for the first block with a median time past at or after a time: @GET \/block\/mtp\/:time@. data GetBlockMTP = GetBlockMTP !TimeParam !NoTx +-- | Request for the first block with a median time past at or after a time, serialized: @GET \/block\/mtp\/:time\/raw@. newtype GetBlockMTPRaw = GetBlockMTPRaw TimeParam -- Transactions+-- | Request for a transaction: @GET \/transaction\/:txid@. newtype GetTx = GetTx TxHash +-- | Request for several transactions: @GET \/transactions?txids=…@. newtype GetTxs = GetTxs [TxHash] +-- | Request for a transaction, serialized: @GET \/transaction\/:txid\/raw@. newtype GetTxRaw = GetTxRaw TxHash +-- | Request for several transactions, serialized: @GET \/transactions\/raw?txids=…@. newtype GetTxsRaw = GetTxsRaw [TxHash] +-- | Request for the transactions of a block: @GET \/transactions\/block\/:block@. newtype GetTxsBlock = GetTxsBlock BlockHash +-- | Request for the transactions of a block, serialized: @GET \/transactions\/block\/:block\/raw@. newtype GetTxsBlockRaw = GetTxsBlockRaw BlockHash +-- | Request for whether any ancestor of a transaction is a coinbase confirmed after a height, or null if it cannot be computed within limits: @GET \/transaction\/:txid\/after\/:height@. data GetTxAfter = GetTxAfter !TxHash !HeightParam +-- | Request to broadcast a transaction: @POST \/transactions@. newtype PostTx = PostTx Tx +-- | Request for hashes of mempool transactions, newest first: @GET \/mempool@. data GetMempool = GetMempool !(Maybe LimitParam) !OffsetParam +-- | Request for stream of new blocks and mempool transactions: @GET \/events@. data GetEvents = GetEvents -- Address+-- | Request for transactions of an address: @GET \/address\/:address\/transactions@. data GetAddrTxs = GetAddrTxs !Address !LimitsParam +-- | Request for transactions of several addresses: @GET \/address\/transactions?addresses=…@. data GetAddrsTxs = GetAddrsTxs ![Address] !LimitsParam +-- | Request for full transactions of an address: @GET \/address\/:address\/transactions\/full@. data GetAddrTxsFull = GetAddrTxsFull !Address !LimitsParam +-- | Request for full transactions of several addresses: @GET \/address\/transactions\/full?addresses=…@. data GetAddrsTxsFull = GetAddrsTxsFull ![Address] !LimitsParam +-- | Request for balance of an address: @GET \/address\/:address\/balance@. newtype GetAddrBalance = GetAddrBalance Address +-- | Request for balances of several addresses: @GET \/address\/balances?addresses=…@. newtype GetAddrsBalance = GetAddrsBalance [Address] +-- | Request for unspent outputs of an address: @GET \/address\/:address\/unspent@. data GetAddrUnspent = GetAddrUnspent !Address !LimitsParam +-- | Request for unspent outputs of several addresses: @GET \/address\/unspent?addresses=…@. data GetAddrsUnspent = GetAddrsUnspent ![Address] !LimitsParam -- XPubs+-- | Request for summary of an extended public key: @GET \/xpub\/:xpub@. data GetXPub = GetXPub !XPubKey !Store.DeriveType !NoCache +-- | Request for transactions of an extended public key: @GET \/xpub\/:xpub\/transactions@. data GetXPubTxs = GetXPubTxs !XPubKey !Store.DeriveType !LimitsParam !NoCache +-- | Request for full transactions of an extended public key: @GET \/xpub\/:xpub\/transactions\/full@. data GetXPubTxsFull = GetXPubTxsFull !XPubKey !Store.DeriveType !LimitsParam !NoCache +-- | Request for balances of the used addresses of an extended public key: @GET \/xpub\/:xpub\/balances@. data GetXPubBalances = GetXPubBalances !XPubKey !Store.DeriveType !NoCache +-- | Request for unspent outputs of an extended public key: @GET \/xpub\/:xpub\/unspent@. data GetXPubUnspent = GetXPubUnspent !XPubKey !Store.DeriveType !LimitsParam !NoCache +-- | Request to remove an extended public key from the cache: @DELETE \/xpub\/:xpub@. data DelCachedXPub = DelCachedXPub !XPubKey !Store.DeriveType -- Network+-- | Request for connected peers: @GET \/peers@. data GetPeers = GetPeers +-- | Request for health of the store: @GET \/health@. data GetHealth = GetHealth ------------@@ -365,30 +438,38 @@ -- Helpers -- ------------- +-- | Path made of a prefix followed by one parameter. (<:>) :: Text -> [Text] -> Text (<:>) = (<+> "") +-- | Path made of a prefix, one parameter and a suffix. (<+>) :: Text -> Text -> [Text] -> Text a <+> b = fill 1 [a, b] +-- | Path made of a prefix, a parameter, an infix and another parameter. (<++>) :: Text -> Text -> [Text] -> Text a <++> b = fill 2 [a, b] +-- | Interleave path fragments with this many parameters. Calls 'error' if the number of parameters is wrong. fill :: Int -> [Text] -> [Text] -> Text fill i a b | length b /= i = error "Invalid query parameters" | otherwise = mconcat $ uncurry (<>) <$> zip a (b <> repeat "") +-- | Parameter to send, unless it has its default value. noDefBox :: (Default p, Param p, Eq p) => p -> [ParamBox] noDefBox p = [ParamBox p | p /= def] +-- | Parameter to send, if any. noMaybeBox :: (Param p, Eq p) => Maybe p -> [ParamBox] noMaybeBox (Just p) = [ParamBox p] noMaybeBox _ = [] +-- | Proxy for the type of a value. asProxy :: a -> Proxy a asProxy = const Proxy +-- | Path of a request, with its path parameters filled in. queryPath :: (ApiResource a b) => Network -> Ctx -> a -> Text queryPath net ctx a = f $ encParam <$> fst (queryParams a) where@@ -398,6 +479,7 @@ Just [res] -> res _ -> error "Invalid query param" +-- | Route pattern of an endpoint for the server, with a named capture for each path parameter. capturePath :: (ApiResource a b) => Proxy a -> Scotty.RoutePattern capturePath proxy = fromString $ cs $ f $ toLabel <$> captureParams proxy@@ -405,6 +487,7 @@ f = resourcePath proxy toLabel (ProxyBox p) = ":" <> proxyLabel p +-- | Name of a parameter in URLs. paramLabel :: (Param p) => p -> Text paramLabel = proxyLabel . asProxy @@ -412,9 +495,14 @@ -- Options -- ------------- +-- | Type that can be a parameter of a request. class Param a where+ -- | Name of the parameter in URLs. proxyLabel :: Proxy a -> Text+ -- | Encode a value as one or more strings. A list parameter is sent as+ -- comma-separated values. encodeParam :: Network -> Ctx -> a -> Maybe [Text]+ -- | Parse a value from one or more strings. parseParam :: Network -> Ctx -> [Text] -> Maybe a instance Param Address where@@ -428,10 +516,16 @@ encodeParam net ctx = mapM (addrToText net) parseParam net ctx = mapM (textToAddr net) +-- | Where a page of results starts. In URLs it is a transaction hash, a block+-- height, or a Unix timestamp. Numbers up to 1230768000 are heights and larger+-- ones are timestamps. data StartParam- = StartParamHash {hash :: Hash256}- | StartParamHeight {height :: Natural}- | StartParamTime {time :: Store.UnixTime}+ = -- | start at a transaction+ StartParamHash {hash :: Hash256}+ | -- | start at a block height+ StartParamHeight {height :: Natural}+ | -- | start at the last block at or before a time+ StartParamTime {time :: Store.UnixTime} deriving (Eq, Show) instance Param StartParam where@@ -462,6 +556,7 @@ return $ StartParamTime x parseParam net ctx _ = Nothing +-- | Number of results to skip (@offset@). newtype OffsetParam = OffsetParam {get :: Natural} deriving (Eq, Show, Read, Enum, Ord, Num, Real, Integral) @@ -474,6 +569,7 @@ parseParam net ctx [s] = OffsetParam <$> readMaybe (cs s) parseParam net ctx _ = Nothing +-- | Maximum number of results (@limit@). newtype LimitParam = LimitParam {get :: Natural} deriving (Eq, Show, Read, Enum, Ord, Num, Real, Integral) @@ -483,9 +579,14 @@ parseParam net ctx [s] = LimitParam <$> readMaybe (cs s) parseParam net ctx _ = Nothing +-- | Pagination of a list of results. data LimitsParam = LimitsParam- { limit :: Maybe LimitParam, -- 0 means maximum+ { -- | maximum number of results; @0@ means the maximum the server allows,+ -- and 'Nothing' the server default+ limit :: Maybe LimitParam,+ -- | number of results to skip after the start offset :: OffsetParam,+ -- | where to start, or from the newest result start :: Maybe StartParam } deriving (Eq, Show)@@ -493,6 +594,7 @@ instance Default LimitsParam where def = LimitsParam Nothing def Nothing +-- | Block height (@height@). newtype HeightParam = HeightParam {get :: Natural} deriving (Eq, Show, Read, Enum, Ord, Num, Real, Integral) @@ -502,6 +604,7 @@ parseParam net ctx [s] = HeightParam <$> readMaybe (cs s) parseParam net ctx _ = Nothing +-- | List of block heights (@heights@). newtype HeightsParam = HeightsParam {get :: [Natural]} deriving (Eq, Show, Read) @@ -510,6 +613,7 @@ encodeParam net ctx (HeightsParam hs) = Just $ cs . show <$> hs parseParam net ctx xs = HeightsParam <$> mapM (readMaybe . cs) xs +-- | Unix timestamp in seconds (@time@). newtype TimeParam = TimeParam {get :: Store.UnixTime} deriving (Eq, Show, Read, Enum, Ord, Num, Real, Integral) @@ -537,6 +641,7 @@ guard (net.segWit || res == Store.DeriveNormal) return res +-- | Do not use the cache to answer an xpub query (@nocache@). newtype NoCache = NoCache {get :: Bool} deriving (Eq, Show, Read) @@ -552,6 +657,8 @@ ["false"] -> Just $ NoCache False _ -> Nothing +-- | Leave out the transaction hashes of a block, except that of the coinbase+-- (@notx@). newtype NoTx = NoTx {get :: Bool} deriving (Eq, Show, Read)