packages feed

haskoin-store 1.0.1 → 1.1.0

raw patch · 15 files changed

+3224/−5768 lines, 15 filesdep +statsd-ruppdep −ekg-coredep −ekg-statsddep −foldldep ~haskoin-store-dataPVP ok

version bump matches the API change (PVP)

Dependencies added: statsd-rupp

Dependencies removed: ekg-core, ekg-statsd, foldl, wai-extra

Dependency ranges changed: haskoin-store-data

API changes (from Hackage documentation)

- Haskoin.Store: [$sel:atBlockHeight:AtTx] :: Start -> !BlockHeight
- Haskoin.Store: [$sel:atTxHash:AtTx] :: Start -> !TxHash
- Haskoin.Store: [$sel:statsStore:StoreConfig] :: StoreConfig -> !Maybe Store
- Haskoin.Store.BlockStore: [$sel:statsStore:BlockStoreConfig] :: BlockStoreConfig -> !Maybe Store
- Haskoin.Store.Cache: instance (Control.Monad.IO.Unlift.MonadUnliftIO m, Control.Monad.Logger.MonadLoggerIO m, Haskoin.Store.Common.StoreReadBase m) => Haskoin.Store.Common.StoreReadBase (Haskoin.Store.Cache.CacheT m)
- Haskoin.Store.Common: DataMetrics :: !Counter -> !Counter -> !Counter -> !Counter -> !Counter -> !Counter -> !Counter -> !Counter -> !Counter -> !Counter -> !Counter -> DataMetrics
- Haskoin.Store.Common: [$sel:atBlockHeight:AtTx] :: Start -> !BlockHeight
- Haskoin.Store.Common: [$sel:atTxHash:AtTx] :: Start -> !TxHash
- Haskoin.Store.Common: [$sel:dataAddrTxCount:DataMetrics] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [$sel:dataBalanceCount:DataMetrics] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [$sel:dataBestCount:DataMetrics] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [$sel:dataBlockCount:DataMetrics] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [$sel:dataMempoolCount:DataMetrics] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [$sel:dataTxCount:DataMetrics] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [$sel:dataUnspentCount:DataMetrics] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [$sel:dataXPubBals:DataMetrics] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [$sel:dataXPubTxCount:DataMetrics] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [$sel:dataXPubTxs:DataMetrics] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [$sel:dataXPubUnspents:DataMetrics] :: DataMetrics -> !Counter
- Haskoin.Store.Common: createDataMetrics :: MonadIO m => Store -> m DataMetrics
- Haskoin.Store.Common: data DataMetrics
- Haskoin.Store.Manager: [$sel:statsStore:StoreConfig] :: StoreConfig -> !Maybe Store
- Haskoin.Store.Stats: addClientError :: MonadIO m => StatDist -> m ()
- Haskoin.Store.Stats: addServerError :: MonadIO m => StatDist -> m ()
- Haskoin.Store.Stats: addStatItems :: MonadIO m => StatDist -> Int64 -> m ()
- Haskoin.Store.Stats: addStatQuery :: MonadIO m => StatDist -> m ()
- Haskoin.Store.Stats: addStatTime :: MonadIO m => StatDist -> Int64 -> m ()
- Haskoin.Store.Stats: createStatDist :: MonadIO m => Text -> Store -> m StatDist
- Haskoin.Store.Stats: data StatDist
- Haskoin.Store.Stats: withStats :: MonadIO m => Text -> Int -> Text -> (Store -> m a) -> m a
- Haskoin.Store.Web: WebTimeouts :: !Word64 -> !Word64 -> WebTimeouts
- Haskoin.Store.Web: [$sel:block:WebTimeouts] :: WebTimeouts -> !Word64
- Haskoin.Store.Web: [$sel:maxBodySize:WebLimits] :: WebLimits -> !Word32
- Haskoin.Store.Web: [$sel:requestTimeout:WebLimits] :: WebLimits -> !Word32
- Haskoin.Store.Web: [$sel:statsStore:WebConfig] :: WebConfig -> !Maybe Store
- Haskoin.Store.Web: [$sel:timeouts:WebConfig] :: WebConfig -> !WebTimeouts
- Haskoin.Store.Web: [$sel:tx:WebTimeouts] :: WebTimeouts -> !Word64
- Haskoin.Store.Web: data WebTimeouts
- Haskoin.Store.Web: instance (Control.Monad.IO.Unlift.MonadUnliftIO m, Control.Monad.Logger.MonadLoggerIO m) => Haskoin.Store.Common.StoreReadBase (Control.Monad.Trans.Reader.ReaderT Haskoin.Store.Web.WebState m)
- Haskoin.Store.Web: instance (Control.Monad.IO.Unlift.MonadUnliftIO m, Control.Monad.Logger.MonadLoggerIO m) => Haskoin.Store.Common.StoreReadBase (Haskoin.Store.Web.WebT m)
- Haskoin.Store.Web: instance Data.Default.Class.Default Haskoin.Store.Web.WebTimeouts
- Haskoin.Store.Web: instance GHC.Classes.Eq Haskoin.Store.Web.WebTimeouts
- Haskoin.Store.Web: instance GHC.Show.Show Haskoin.Store.Web.WebTimeouts
+ Haskoin.Store: [$sel:stats:StoreConfig] :: StoreConfig -> !Maybe Stats
+ Haskoin.Store.BlockStore: [$sel:stats:BlockStoreConfig] :: BlockStoreConfig -> !Maybe Stats
+ Haskoin.Store.Cache: instance (Control.Monad.IO.Unlift.MonadUnliftIO m, Control.Monad.Logger.MonadLoggerIO m, Haskoin.Store.Common.StoreReadExtra m) => Haskoin.Store.Common.StoreReadExtra (Haskoin.Store.Cache.CacheX m)
+ Haskoin.Store.Cache: instance (Control.Monad.IO.Unlift.MonadUnliftIO m, Haskoin.Store.Common.StoreReadBase m) => Haskoin.Store.Common.StoreReadBase (Haskoin.Store.Cache.CacheT m)
+ Haskoin.Store.Cache: instance (Control.Monad.IO.Unlift.MonadUnliftIO m, Haskoin.Store.Common.StoreReadBase m) => Haskoin.Store.Common.StoreReadBase (Haskoin.Store.Cache.CacheX m)
+ Haskoin.Store.Database.Reader: createDataMetrics :: MonadIO m => Stats -> m DataMetrics
+ Haskoin.Store.Database.Reader: data DataMetrics
+ Haskoin.Store.Database.Reader: withDB :: DatabaseReader -> DatabaseReaderT m a -> m a
+ Haskoin.Store.Manager: [$sel:stats:StoreConfig] :: StoreConfig -> !Maybe Stats
+ Haskoin.Store.Web: [$sel:blockTimeout:WebLimits] :: WebLimits -> !Word64
+ Haskoin.Store.Web: [$sel:stats:WebConfig] :: WebConfig -> !Maybe Stats
+ Haskoin.Store.Web: [$sel:txTimeout:WebLimits] :: WebLimits -> !Word64
+ Haskoin.Store.Web: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Haskoin.Store.Common.StoreReadBase (Control.Monad.Trans.Reader.ReaderT Haskoin.Store.Web.WebState m)
+ Haskoin.Store.Web: instance Control.Monad.IO.Unlift.MonadUnliftIO m => Haskoin.Store.Common.StoreReadBase (Haskoin.Store.Web.WebT m)
- Haskoin.Store: StoreConfig :: !Int -> ![String] -> !Bool -> !FilePath -> !Network -> !Maybe String -> !Ctx -> !Word32 -> !Word32 -> !Int -> !Integer -> !Bool -> !Bool -> !Bool -> !NominalDiffTime -> !NominalDiffTime -> !SockAddr -> WithConnection -> !Maybe Store -> !Int -> StoreConfig
+ Haskoin.Store: StoreConfig :: !Int -> ![String] -> !Bool -> !FilePath -> !Network -> !Maybe String -> !Ctx -> !Word32 -> !Word32 -> !Int -> !Integer -> !Bool -> !Bool -> !Bool -> !NominalDiffTime -> !NominalDiffTime -> !SockAddr -> WithConnection -> !Maybe Stats -> !Int -> StoreConfig
- Haskoin.Store: getTransaction :: (Monad m, StoreReadBase m) => TxHash -> m (Maybe Transaction)
+ Haskoin.Store: getTransaction :: StoreReadBase m => TxHash -> m (Maybe Transaction)
- Haskoin.Store: withCache :: StoreReadBase m => Maybe CacheConfig -> CacheT m a -> m a
+ Haskoin.Store: withCache :: Maybe CacheConfig -> CacheT m a -> m a
- Haskoin.Store.BlockStore: BlockStoreConfig :: !Ctx -> !PeerMgr -> !Chain -> !Publisher StoreEvent -> !DatabaseReader -> !Network -> !Bool -> !Bool -> !Bool -> !NominalDiffTime -> !Maybe Store -> BlockStoreConfig
+ Haskoin.Store.BlockStore: BlockStoreConfig :: !Ctx -> !PeerMgr -> !Chain -> !Publisher StoreEvent -> !DatabaseReader -> !Network -> !Bool -> !Bool -> !Bool -> !NominalDiffTime -> !Maybe Stats -> BlockStoreConfig
- Haskoin.Store.Cache: cacheDelXPubs :: (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) => [XPubSpec] -> CacheT m Integer
+ Haskoin.Store.Cache: cacheDelXPubs :: (MonadLoggerIO m, StoreReadBase m) => [XPubSpec] -> CacheT m Integer
- Haskoin.Store.Cache: newCacheMetrics :: MonadIO m => Store -> m CacheMetrics
+ Haskoin.Store.Cache: newCacheMetrics :: MonadLoggerIO m => Stats -> Connection -> DatabaseReader -> m CacheMetrics
- Haskoin.Store.Cache: withCache :: StoreReadBase m => Maybe CacheConfig -> CacheT m a -> m a
+ Haskoin.Store.Cache: withCache :: Maybe CacheConfig -> CacheT m a -> m a
- Haskoin.Store.Common: getNumTransaction :: (Monad m, StoreReadExtra m) => Word64 -> m [Transaction]
+ Haskoin.Store.Common: getNumTransaction :: StoreReadExtra m => Word64 -> m [Transaction]
- Haskoin.Store.Common: getTransaction :: (Monad m, StoreReadBase m) => TxHash -> m (Maybe Transaction)
+ Haskoin.Store.Common: getTransaction :: StoreReadBase m => TxHash -> m (Maybe Transaction)
- Haskoin.Store.Common: nub' :: (Eq a, Hashable a) => [a] -> [a]
+ Haskoin.Store.Common: nub' :: Hashable a => [a] -> [a]
- Haskoin.Store.Manager: StoreConfig :: !Int -> ![String] -> !Bool -> !FilePath -> !Network -> !Maybe String -> !Ctx -> !Word32 -> !Word32 -> !Int -> !Integer -> !Bool -> !Bool -> !Bool -> !NominalDiffTime -> !NominalDiffTime -> !SockAddr -> WithConnection -> !Maybe Store -> !Int -> StoreConfig
+ Haskoin.Store.Manager: StoreConfig :: !Int -> ![String] -> !Bool -> !FilePath -> !Network -> !Maybe String -> !Ctx -> !Word32 -> !Word32 -> !Int -> !Integer -> !Bool -> !Bool -> !Bool -> !NominalDiffTime -> !NominalDiffTime -> !SockAddr -> WithConnection -> !Maybe Stats -> !Int -> StoreConfig
- Haskoin.Store.Web: WebConfig :: !String -> !Int -> !Store -> !Int -> !Int -> !Int -> !WebLimits -> !WebTimeouts -> !String -> !Bool -> !Maybe Store -> !Int -> !String -> !String -> !Bool -> !Bool -> !Int -> WebConfig
+ Haskoin.Store.Web: WebConfig :: !String -> !Int -> !Store -> !Int -> !Int -> !Int -> !WebLimits -> !String -> !Bool -> !Maybe Stats -> !Int -> !String -> !String -> !Bool -> !Bool -> !Int -> WebConfig
- Haskoin.Store.Web: WebLimits :: !Word32 -> !Word32 -> !Word32 -> !Word32 -> !Word32 -> !Word32 -> !Word32 -> !Word32 -> WebLimits
+ Haskoin.Store.Web: WebLimits :: !Word32 -> !Word32 -> !Word32 -> !Word32 -> !Word32 -> !Word32 -> !Word64 -> !Word64 -> WebLimits

Files

− CHANGELOG.md
@@ -1,1764 +0,0 @@-# Changelog-All notable changes to this project will be documented in this file.--The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)-and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html)--## [1.0.1] - 2023-08-03--### Changed--- Add compatibility with latest nightly LTS Haskell.--## [1.0.0] - 2023-07-28--### Changed--- Compatibility with latest haskoin-core and haskoin-node packages.-- Use DuplicateRecordFields and OverloadedRecordDot language extensions.-- Modernize package in a backwards-incompatible manner.-- Refactor package extensively.--## [0.65.10] - 2023-05-12--### Changed--- Revert to older LTS Haskell to attempt to resolve memory leak.--### Removed--- Remove unnecessary lock release.--## [0.65.9] - 2023-05-09--### Changed--- Be more aggressive caching individual transactions.-- Introduce a periodic cache mempool sync task.--## [0.65.8] - 2023-05-08--### Changed--- Perform health check in separate thread.--## [0.65.7] - 2023-05-05--### Changed--- Do not sync mempool to Redis if node is not up-to-date.--## [0.65.6] - 2023-05-04--### Changed--- Improve caching ingestion performance.--## [0.65.5] - 2022-09-19--### Added--- Fee field added to Blockchain.info-style export-history endpoint.--### Changed--- Bump version due to Git mistake.--## [0.65.3] - 2022-04-13--### Added--- IPv6 peer support.--## [0.65.2] - 2022-04-13--### Fixed--- Correct lazy bytestring deserialization algorithm for web client.--## [0.65.1] - 2022-04-13--### Changed--- Simplify function in Cache module.-- Bump depenedencies.--## [0.65.0] - 2022-02-03--### Changed--- Spenders now embedded into tx objects in database for performance.-- Use faster IORef instead of slow TVar when ingesting.-- Use mutable hash tables when ingesting.-- Allow to disable unscalable API endpoints.--## [0.64.19] - 2022-02-01--### Added--- Allow to set query timeouts.--## [0.64.18] - 2022-01-21--### Fixed--- Off-by-one error when importing change addresses to cache.--## [0.64.17] - 2022-01-21--### Fixed--- Add change addresses to cache when necessary.--## [0.64.16] - 2022-01-10--### Fixed--- Correct fee calculation for blocks that do not claim all rewards.--## [0.64.15] - 2022-01-07--### Fixed--- Correct scale of cache pruning code.--## [0.64.14] - 2022-01-07--### Fixed--- Always prune to 80% of max cache size.--## [0.64.13] - 2022-01-07--### Fixed--- Avoid doing a hissy fit on transient cache issue.--## [0.64.12] - 2022-01-07--### Fixed--- Prune a little more cache to allow for new keys.--## [0.64.11] - 2022-01-06--### Fixed--- Prune cache more often.--## [0.64.10] - 2021-12-30--### Changed--- Return empty addresses in xpub balances call.--## [0.64.9] - 2021-12-29--### Fixed--- Avoid notifying same transactions in mempool.--## [0.64.8] - 2021-12-26--### Changed--- Use better Fourmolu format.-- Be more precise when counting retrieved items.--### Fixed--- Avoid transactions duplicated in xpub data.-- Refactor some functions to make them more robust.--## [0.64.7] - 2021-12-26--### Fixed--- Optimisations for various Blockchain endpoints.-- More accurate item counts for various web endpoints.--## [0.64.6] - 2021-12-25--### Fixed--- Record cache index time just once per xpub.--## [0.64.5] - 2021-12-25--### Changed--- Add suffix cached to cache entries to avoid conflicts with data entries.--## [0.64.4] - 2021-12-25--### Fixed-- Bring dots back and avoid stats hacks.--## [0.64.3] - 2021-12-25--### Fixed--- Fix typo.--## [0.64.2] - 2021-12-25--### Changed--- Replace dots for underscores in metrics.--## [0.64.1] - 2021-12-25--### Changed--- Automatic formatting.-- Improve cache debug logging.--### Fixed--- Add item count to 'all' query metrics.--## [0.64.0] - 2021-12-25--### Changed--- Improve metrics.--### Fixed--- Get lock before importing transactions to cache.--## 0.63.0-### Fixed-- Make cache updates lightweight.--## 0.62.1-### Fixed-- Fix idempotent transaction elimination bug.--## 0.62.0-### Added-- Configurable Blockchain price URLs.-- Reuse HTTP connections with wreq sessions.--## 0.61.1-### Changed-- Upstream improvements on Base58 performance.--## 0.61.0-### Fixed-- Fixed recursion bug in Blockchain format conversion function.--## 0.60.0-### Fixed-- Correct notifications subsystem.--## 0.59.0-### Fixed-- Correct internal dependency.--## 0.58.0-### Fixed-- Correct output freeing algorithm.--## 0.57.0-### Changed-- Report errors freeing outputs.--## 0.56.0-### Changed-- Report all deleted mempool transactions.-- Crash on certain data corruption.--## 0.55.0-### Changed-- Always have a previous output object in Blockchain inputs.--## 0.54.0-### Added-- WebSocket support.-- Testnet4 support.--## 0.53.11-### Changed-- Dummy version increase to signal upstream update of Haskoin Core.--## 0.53.10-### Fixed-- Correct test that was reversed in previous version.--## 0.53.9-### Removed-- Mempool is no longer synced by default to fix public Bitcoin Core regression.--## 0.53.8-### Changed-- Put derivations stat inside database.--## 0.53.7-### Added-- Added counters for database retrievals.-- Added counter for xpub derivations.--### Changed-- Removed some buggy or unnecessary stats.--## 0.53.6-### Changed-- Improve web server statistics.--## 0.53.5-### Addded-- Endpoint to delete an xpub from cache.--## 0.53.4-### Added-- Debugging when reverting block.--### Fixed-- Negative (headers - blocks) diff shown incorrectly.-- Infinite loop when removing a block's coinbase transaction.--## 0.53.3-### Added-- Blockchain.info endpoint: `q/pubkeyhash/:addr`.-- Blockchain.info endpoint: `q/getsentbyaddress/:addr`.-- Blockchain.info endpoint: `q/pubkeyaddr/:addr`.-- Configurable request body limit size.--## 0.53.2-### Fixed-- Return output in satoshi for some endpoints.--## 0.53.1-### Fixed-- Return 404 in case of bad address.--## 0.53.0-### Added-- Blockchain.info endpoint: `q/addresstohash/:addr`.-- Blockchain.info endpoint: `q/addrpubkey/:pubkey`.-- Blockchain.info endpoint: `q/hashpubkey/:pubkey`.-- Blockchain.info endpoint: `q/getblockcount`.-- Blockchain.info endpoint: `q/latesthash`.-- Blockchain.info endpoint: `q/bcperblock`.-- Blockchain.info endpoint: `q/txtotalbtcoutput/:txid`.-- Blockchain.info endpoint: `q/txtotalbtcinput/:txid`.-- Blockchain.info endpoint: `q/txfee/:txid`.-- Blockchain.info endpoint: `q/txresult/:txhash/:addr`.-- Blockchain.info endpoint: `q/getreceivedbyaddress/:addr`.-- Blockchain.info endpoint: `q/addressbalance/:addr`.-- Blockchain.info endpoint: `q/addressfirstseen/:addr`.-- Support xpubs in `blockchain/rawaddr` endpoint.--### Fixed-- Various latest blocks endpoint fixes.-- Allow empty parameters in Blockchain.info POST requests.--## 0.52.13-### Fixed-- Upstream fixes for unknown inv types.--## 0.52.12-### Fixed-- Fix limits for legacy endpoints.--## 0.52.11-### Added-- Alias for Blockchain.info raw address endpoint.--### Fixed-- Fix unknown message types without payload upstream.-- Randomise peer timeouts upstream.--## 0.52.10-### Fixed-- Fix binary search algorithm for blocks upstream.--## 0.52.9-### Added-- Add Blockchain.info latest block endpoint.-- Add Blockchain.info unconfirmed-transactions endpoint.-- Add Blockchain.info blocks for one day endpoint.--## 0.52.8-### Fixed-- Fix special case for zero limit.--## 0.52.7-### Fixed-- Fix cache bug when too many transactions in mempool.--## 0.52.6-### Changed-- Use rationals for Blockchain.info export-history endpoint arithmetics.--## 0.52.5-### Added-- Support for Blockchain.info export-history endpoint.--### Fixed-- Use strict bytestring decoder for HTTP API client.--## 0.52.4-### Fixed-- Use Base58 addresses by default in Blockchain.info balance querios.--## 0.52.3-### Fixed-- Errors should also contain CORS headers.--## 0.52.2-### Fixed-- Blockchain.info raw address limit should use `limit` parametre and not `n`.--## 0.52.1-### Changed-- Stream multiple transactions when requested.--### Fixed-- Fix missing unspent outputs.--## 0.52.0-### Changed-- Remove unnecessary performance optimisations.--### Fixed-- Reduce balance retrievals for xpubs.-- Hack UTXO ordering to avoid unwanted behaviours.-- Do not merge conduits when there are too many.--## 0.51.0-### Fixed-- Data type for RequestTooLarge was missing.--## 0.50.3-### Added-- Log HTTP request bodies.--### Removed-- Remove web request timeout.--### Fixed-- Do not ignore unknown start tx anchors in conduits.-- Use fast transaction counting in cache.--### Changed-- Use WAI middlewares to manage stats.--## 0.50.2-### Fixed-- Do not use lazy parsers for incoming data that may throw exceptions.--## 0.50.1-### Fixed-- Do not allow incoming POST requests of unlimited body size.--## 0.50.0-### Added-- Limit number of simultaneous xpubs being cached.--## 0.49.0-### Changed-- Improve conduit merging algorithm.--## 0.48.0-### Added-- Timeouts and token bucket for web requests.-- Configurable price fetch interval.--## 0.47.6-### Fixed-- Fix web.all stat.--## 0.47.5-### Changed-- Simplify stats.--## 0.47.4-### Added-- Implement Blockchain.info block height.-- Implement Blockchain.info raw address.-- Implement Blockchain.info balances.--### Fixed-- Make Blockchain.info unspent endpoint more efficient.-- Make Blockchain.info xpubs endpoint more efficient.--### Changed-- Use compact JSON serialization by default everywhere.--## 0.47.3-### Fixed-- Fix serialization bug with unspents.--## 0.47.2-### Fixed-- Fix serialization bug for health check.--## 0.47.1-### Fixed-- Fix serialization bugs for web data types.--## 0.47.0-### Added-- Support for legacy block endpoints.--### Changed-- Serialization now uses bytes library and does interleaved IO.--## 0.46.6-### Added-- Allow comma-separated values in multiaddr.--## 0.46.5-### Changed-- Throw error when offset exceeded instead of truncating it.--## 0.46.4-### Fixed-- Do not compute balances separately for loose addresses when part of an xpub.--### Changed-- Do not fetch entire transaction list for loose addresses.-- Transaction count in wallet object is just a synonym for filtered tx count.-- Use conduits to prevent over-fetching transactions from database.--## 0.46.3-### Fixed-- Make hex value encoding compatible with Java BigInteger decoder.--## 0.46.2-### Changed-- Allow negative confirmations for compatibility with old API.--## 0.46.1-### Added-- Filters for transactions in multiaddr endpoint.--## 0.46.0-### Fixed-- Release to fix unintentional upload.--## 0.45.0-### Added-- Using a tx_index that results in a txid conflict returns a 409 - Conflict.--## 0.44.0-### Changed-- Numeric txid now 53 bits long and doesn't return transactions when hashes collide.--## 0.43.1-### Fixed-- Do not overcount statistics.--## 0.43.0-### Changed-- Transaction index now simply encodes txid as a long number (sorry JS users).--## 0.42.4-### Changed-- Optimise encoding of JSON data for Blockchain.info API.--## 0.42.3-### Changed-- Narrow down the balance for transactions to onlyShow set.--## 0.42.2-### Changed-- Make unspent algorithm more efficient.--## 0.42.1-### Added-- Support for Blockchain.info API unspent endpoint.--### Fixed-- Fix RBF transactions on Blockchain.info API.--## 0.42.0-### Removed-- No more pruning of old mempool transactions.--## 0.41.4-### Fixed-- Correct error computing wallet balances.--## 0.41.3-### Changed-- Use whole active set when computing results for multiaddr.-- Compute correct count of transactions for wallet.--## 0.41.2-### Changed-- Caching mempool is not much slower when pruning, so reversing that change.--## 0.41.1-### Changed-- Only prune mempool every ten minutes or so.--## 0.41.0-### Removed-- No more cache eviction endpoint.--## 0.40.22-### Added-- More metrics for cache.--## 0.40.21-### Added-- Support HTTP GET for multiaddr endpoint.--## 0.40.20-### Added-- Prefix 'app.' in statsd when using Nomad variables.--## 0.40.19-### Added-- Statsd support and pleny of statistics.--### Removed-- Removed use of sync lock for importing xpubs in cache due to volume.--## 0.40.18-### Added-- New options to control cache lock retry count and delay.--### Changed-- Cache xpubs asyncrhonously.-- Mempool syncs fully every refresh cycle.--## 0.40.17-### Changed-- Delete more than 1000 xpubs from Redis if needed.-- Attempt to get lock ten times in order to index a new xpub.--## 0.40.16-### Fixed-- Fix best block not set.--## 0.40.15-### Fixed-- Do not update cache best block if it is higher.--## 0.40.14-### Fixed-- Always respond to cache pings to avoid getting stuck.--## 0.40.13-### Added-- Be more careful about importing blocks to the cache.--### Removed-- Reduce excessive cache logging.--## 0.40.12-### Fixed-- Correct bad arithmetics in cache cooldown.--## 0.40.11-### Added-- Debug log cache cooldown.--### Fixed-- Unlimited retry lock acquisition now works.-- Make locking tighter--## 0.40.10-### Changed-- Reduce the number of things that cache do when pinged.--## 0.40.9-### Fixed-- Avoid filling the cache actor mailbox with pings.--## 0.40.8-### Fixed-- Correct a typo in cache code.--## 0.40.7-### Changed-- Improve cache mempool sync algorithm.--## 0.40.6-### Changed-- Make cache cooldown a number.--## 0.40.5-### Added-- Log every non-successful query.--## 0.40.4-### Fixed-- Use Redis TTL for cache cooldown.--## 0.40.3-### Fixed-- Mempool no longer gets saved in cache when node is not in sync.--## 0.40.2-### Changed-- Change source of price data to https://blockchain.info/ticker.--## 0.40.1-### Removed-- Do not expose Paths_haskoin_store module explicitly.--## 0.40.0-### Added-- Blockchain.info API compatibility for multiaddr and rawtx endpoints.--### Changed-- Segwit fields are no longer omitted on incompatible networks.--## 0.39.0-### Changed-- Upgrade Haskoin Core to support higher version on witness addresses.--## 0.38.4-### Fixed-- Fix block search by timestamp.--## 0.38.3-### Changed-- Disable low latency garbage collector by default.--## 0.38.2-### Changed-- Use GHC 8.10 and its low latency garbage collector.--## 0.38.1-### Changed-- Optimise algorithm for fork safety check.--## 0.38.0-### Added-- Support for Bitcoin Cash hard fork.--## 0.37.5-### Added-- Configurable cache refresh time.--## 0.37.4-### Fixed-- Now request mempool when connecting even if already in sync.--### Changed-- Add a delay to mempool sync to allow for block headers to catch up.-- Attempt to sync mempool whenever syncing is done or a new peer connects.--## 0.37.3-### Changed-- Correct logic error when deleting superseded transactions.--## 0.37.2-### Changed-- Simplify multiple algorithms to hopefully solve corruption bug.-- Allow to disable mempool transaction indexing via command line.--## 0.37.1-### Changed-- Better locking around caching operations.-- Have a global cooldown on cache mempool updates.--### Fixed-- Fix address unspent retrieval using correct column family.--## 0.37.0-### Added-- Add support for column families and prefixes breaking database backwards compatibility.--## 0.36.2-### Fixed-- Fix another place where mempool sorting was wrong.--## 0.36.1-### Fixed-- Fix wrong sorting of mempool transactions in database.--## 0.36.0-### Changed-- Do not use STM transactions because it is impossible to reason about preloading.-- Make importing blocks after mempool is populated faster.--## 0.35.2-### Changed-- Reduce frequency of cache mempool updates.--## 0.35.1-### Changed-- Remove cache lock retries.-- Do not use lock when caching a new xpub.--## 0.35.0-### Changed-- Use new RocksDB Haskell bindings.-- Make headers - blocks diff configurable in health check.-- Simplify locking in cache.--## 0.34.8-### Changed-- Minor refactoring.-- Only send one mempool message ever.--## 0.34.7-### Changed-- Fix block heights endpoint URL.--## 0.34.6-### Removed-- Remove asynchronous database retrieval permanently due to instability.--## 0.34.5-### Added-- Preload memory before freeing outputs.--## 0.34.4-### Added-- Asynchronous preloading only for blocks.--## 0.34.3-### Removed-- Disable asynchronous preloading to prevent segmentation fault error.--## 0.34.2-### Removed-- No more upper limit for mempool call.--### Fixed-- Health check uses minimum between last block and mempool tx.--## 0.34.1-### Added-- Pending mempool transactions health check.--### Removed-- No more upper limit for mempool call.--## 0.34.0-### Changed-- New format for health check.--## 0.33.1-### Changed-- Upload with updated Cabal files.--## 0.33.0-### Fixed-- Fix outputs showing up as both spent and unspent.--### Changed-- Simplify data structures used to hold uncommitted changes.-- Split StoreRead class in two.-- Improve pre-loading algorithms.--## 0.32.3-### Fixed-- Fix command line argument mappings to env vars.--## 0.32.2-### Fixed-- Do not complain when deleting non-existent unconfirmed txs.--## 0.32.1-### Fixed-- Fix corruption bug when deleting transactions during some reorgs.-- Fix tests on data package.-- Rename upstream address string functions.--## 0.32.0-### Changed-- Concurrently load dependencies when importing transaction data.-- Import each transaction using STM.-- Rework logging.-- Refactor all block chain importing algorithms.-- Use peer locking when importing data.-- Change license to MIT.--### Added-- Make max peer count configurable.--### Fixed-- Prevent premature peer timeouts when importing blocks.--## 0.31.0-### Added-- Add Haskell web client code to data library.-- Use type-safe definitions for web paths and parameters.-- Support JSON pretty encoding and enable by default for some endpoints.-- Allow environment variables for configuration.--### Changed-- Change command-line arguments to better fit GNU style.-- Refactor code to manipulate address balances.-- Improve logging.-- Rate-limit mempool importing.-- Do not add xpubs to cache if node not in sync.-- Do not import transactions to cache if no xpubs in cache.-- Refactor transaction importing logic extremely.-- Refactor memory database and writer.--### Fixed-- Fix BTC double-count transaction bug.-- Add missing xpub derive parameter to xpub evict documentation.--## 0.30.1-### Changed-- Limit mempool API endpoint list.--## 0.30.0-### Changed-- Raw JSON values are now wrapped in an object with a result property.-- Better logging when importing transactions into mempool.--### Fixed-- Transaction limit by block height works now.--## 0.29.3-### Changed-- Revert concurrent retrieval changes from 0.29.1 and 0.29.0 to solve stability-  issues.-- Bitcoin Cash (BCH) replaces BTC as default network in CLI.-- Peer discovery enabled automatically if no peers specified in CLI.-- Use separate Stack configuration file for data package.--## 0.29.2-### Changed-- Increase timeouts for manager and chain health checks.--## 0.29.1-### Added-- Perform xpub operations concurrently.--## 0.29.0-### Changed-- BlockTx renamed to TxRef.-- Remove unnecessary monad transformers to simplify import code.--### Added-- Retrieve balances and unspent outputs concurrently when importing.-- Coin instance for Unspent in data package.--## 0.28.0-### Changed-- Change the way that limits, start, and offset values are handled.--### Fixed-- Fix incorrect limit responses.--## 0.27.0-### Added-- Complete support for offset.--### Changed-- Improve monad stack for cache.--## 0.26.6-### Added-- Support for offset in all endpoints where it makes sense.--## 0.26.5-### Fixed-- Sort after using the nub function or use Data.List.nub in exceptional cases.--## 0.26.4-### Changed-- Don't do certain validity checks on confirmed transactions.--### Fixed-- Do not crash when syncing the blockchain from scratch on a populated cache.--## 0.26.3-### Changed-- Replace nub function everywhere with faster version written using hash sets.--### Fixed-- Fix shared cache locking.--## 0.26.2-### Changed-- Simplify cache block import code.-- Improve cache import performance.--## 0.26.1-### Fixed-- Do not call zadd or zrem in Redis with incorrect argument count.--### Changed-- Do not use slow Transaction type in Logic.-- Improve logging while importing blocks.-- Better transaction import algorithm.--## 0.26.0-### Changed-- Add information to some binary types for feature parity with JSON.--## 0.25.4-### Changed-- Hide segwit data on non-segwit networks.--### Fixed-- Fix work which now always serialises as a number literal.--## 0.25.3-### Changed-- Fix version display on health check.--## 0.25.2-### Changed-- Fix wrong version of Haskoin Store Data dependency.--## 0.25.1-### Changed-- Improve code organisation.-- Split data definitions and serialisation into its own package.--## 0.24.0-### Changed-- Use mocking to simulate peers instead of connecting to the network while testing.-- Depend on latest version of Haskoin Core for faster JSON serialisation.-- Clean up confusing JSON encoding/decoding codebase.--## 0.23.24-### Changed-- Implement faster JSON encoding using toEncoding from the Aeson library.--## 0.23.23-### Changed-- Do not read full mempool so often from cache code.--## 0.23.22-### Changed-- Depend on latest Haskoin Node that fixes debugging regression.--## 0.23.21-### Changed-- Reduce non-debug log verbosity.--## 0.23.20-### Changed-- Better Haskoin Node logging.--## 0.23.19-### Changed-- Use set peer timeout instead of constant in block store. --## 0.23.18-### Changed-- Allow setting peer timeouts in command line.--### Fixed-- Bump Haskoin Node dependency to fix another premature timeout condition.--## 0.23.17-### Fixed-- Touch syncing peer whenever we process one of its blocks to avoid premature timeout.--## 0.23.16-### Changed-- Better algorithm to avoid importing transaction multiple times in cache.-- Depend on latest Haskoin Node.--## 0.23.15-### Changed-- Do not storm the cache with mempool transactions.--## 0.23.13-### Fixed-- Do not ignore deleted incoming transactions.--## 0.23.12-### Fixed-- Do not do RBF checks when replacing a mempool transaction with a confirmed one.--## 0.23.11-### Fixed-- Streamline mempool transaction importing.--## 0.23.10-### Fixed-- Fix transactions not recorded in cache mempool set.-- Fix transactions being downloaded multiple times.--### Removed-- Do not store orphan transactions in database.--### Changed-- Use sets for incoming transactions instead of lists.-- Do not do anything to the cache if there are no xpubs in it.--## 0.23.9-### Fixed-- Wiping mempool fixed.--## 0.23.8-### Added-- Chunk transactions to be deleted from mempool.--## 0.23.7-### Added-- Mempool improvements.--## 0.23.6-### Added-- Ability to wipe mempool at start.-- Improvements to mempool processing code.--## 0.23.5-### Changed-- Tighten the locking loop to avoid slow cache building.--## 0.23.5-### Fixed-- Wrong error in cache when acquiring lock.--## 0.23.4-### Added-- Add extra debug logging for cache code.--### Fixed-- Fix a bug with xpub growing algorithm.--### Changed-- Use locks instead of transactions to update cache.--## 0.23.3-### Fixed-- Reduce contention when many instances of Haskoin Store share a cache.--## 0.23.2-### Added-- Allow retrieving xpub data without using cache.--## 0.23.1-### Added-- Allow xpub eviction from cache via API.-- Clarify cache address addition code.--### Fixed-- Balances were incorrectly computed in cache when new transactions arrived.--## 0.23.0-### Added-- Support for Redis transactions.-- Use a smaller initial gap for empty xpubs.--### Changed-- Remove custom JSON encoding class.-- Refactor and code simplification.--## 0.22.5-### Fixed-- Cache was being completely pruned.--## 0.22.4-### Fixed-- Cache now prunes correctly.--## 0.22.3-### Fixed-- Bug was making cache get stuck when pruning.--## 0.22.2-### Changed-- More efficient algorithms for caching and cache misses.-- Better debug logging of cache hits and misses.--## 0.22.1-### Added-- More debug logging for cache hits.--### Fixed-- Bug using maximum against empty list.--## 0.22.0-### Changed-- Extreme code refactoring.-- Move all code to Haskoin and drop Network from modules.--### Added-- Use Redis pipelining when importing multiple transactions into cache.-- Implement configurable LRU for Redis cache.-- Import xpubs directly into cache from web worker thread when a key is requested.--### Removed-- Only expose a few modules to external API.--## 0.21.7-### Changed-- Improve build configuration.--### Fixed-- Use minimum used addresses instead of minimum index for xpub cache decision.--## 0.21.6-### Fixed-- Fix missing xpub unspent outputs when using cache.--## 0.21.5-### Added-- Only store xpubs in cache if they have more than a threshold addresses used.--## 0.21.4-### Fixed-- Fix shared cache case where head is set beyond header chain by another node.--## 0.21.3-### Fixed-- Fix bug where best head was not being registered in cache.-- Fix best head in cache being decoded incorrectly.--## 0.21.2-### Added-- Complete support for Redis xpub cache.--## 0.21.1-### Fixed-- Latest version of secp256k1-haskell works with Debian 9.--## 0.21.0-### Fixed-- Fix output of web API calls when issuing limits with offsets.--### Changed-- Massive refactoring of entire codebase.--### Added-- Work in progress Redis caching for extended public keys.--## 0.20.2-### Changed-- Filter xpub address balances on web API to show only addresses that have been used.--### Removed-- Remove paths and addresses from xpub summary.--## 0.20.1-### Changed-- Refactor code greatly.-- Depend on new Haskoin Store package to avoid missing tx broadcasts.-- Merge StoreStream class into StoreRead.-- Move former streaming functions to use lists instead of conduits.-- Remove excessively verbose debugging.--## 0.20.0-### Added-- Set minimum version bounds for some dependencies.-- Now compatible with GHC 8.8.-- Extended key caching system using Redis.--### Changed-- Massively refactored codebase.-- Less verbose debug logging.--### Removed-- Removed conduits for faster queries.--## 0.19.6-### Added-- Use a parallel strategy to compute key derivations.--### Fixed-- Do not fail health check upon transaction timeout while syncing.--### Changed-- Do not use conduits for xpub balance streams.-- Multiple minor refactorings.--## 0.19.5-### Changed-- Minor refactor to block import code.--### Fixed-- Minor fix to transaction timeout check.--## 0.19.4-### Fixed-- Clarify and correct health check algorithm.--## 0.19.3-### Changed-- Add address transactions to cache.-- Improve multi-address transaction retrieval algorithms.--## 0.19.2-### Removed-- Cache-Control header turned out to be unnecessary.--### Fixed-- Fix some minor errors in web module.--## 0.19.1-### Added-- Set Cache-Control header to no-cache.--## 0.19.0-### Changed-- Store mempool in single key/value pair.--## 0.18.11-### Changed-- Do not stream mempool.--## 0.18.10-### Removed-- Disable timeout checks for testnets.--## 0.18.9-### Added-- Endpoint to locate a block by unix timestamp.--### Removed-- No more persistence for peers due to dependency on newest haskoin-node.--## 0.18.8-### Added-- Transaction and block timeouts for health check.-- Raw blocks.--## 0.18.7-### Fixed-- Missing tranasctions on xpub listings.--## 0.18.6-### Removed-- Follow Stack advise removing `-O2` GHC option.--## 0.18.5-### Added-- Compatibility with SegWit on extended public key endpoints.--### Changed-- Fix syncing peer not reset after timeout.-- Use simpler monad for streaming data.--## 0.18.4-### Changed-- Bump Haskoin Node to fix peers not stored and excessively verbose logging.--## 0.18.3-### Changed-- Configurable HTTP request logging. Disabled by default.--## 0.18.2-### Changed-- Fix for memory leak.--## 0.18.1-### Removed-- Remove transaction count from xpub summary object.--## 0.18.0-### Changed-- Simplified limits and start point handling on web API.-- Made transaction streaming algorithm faster for xpub transactions.-- Extended public key summary output contains all addresses that have received transactions.--### Added-- Fine-grained control for maximum limits via command line options.-- Transaction hash as starting point.-- Block hash as starting point.-- Timestamp as starting point.-- Configurable xpub gap limit.-- Transaction count added to xpub summary.-- UTXO count added to xpub summary.--### Removed-- Mempool endpoint now has no limits or offsets and always returns full list.-- Extended public key summary output no longer includes any transactions.-- Offsets not allowed for transaction lists involving multiple addresses or xpubs.-- Confusing block position parameter no longer part of web API.--## 0.17.2-### Changed-- Stream address balances and unspent outputs.-- Add configurable max value for limit and offset which defaults at 10000.--## 0.17.1-### Changed-- When posting a transaction to the network, timeout is now five seconds.-- Improve error message when transaction post timeout reached.-- Remove obsolete `not found` error for transaction post.-- Endpoints for retrieving blocks now do streaming for better performance.-- Improve Swagger API documentation.--## 0.17.0-### Added-- Endpoints for retrieving block transactions.-- Endpoint for retrieving set of latest blocks.--### Changed-- Use standardized JSON and binary serialization schemes for raw transaction endpoints.--## 0.16.6-### Changed-- Now logging info messages too by default.-- Consolidated web logging in middleware.--### Removed-- UUIDs for web requests.--## 0.16.5-### Removed-- Remove concurrency from xpub balance requests to prevent RocksDB segfaults.--## 0.16.4-### Removed-- Remove concurrency from requests using iterators to prevent RocksDB from segfaulting.--## 0.16.3-### Added-- Debugging information for web API.--## 0.16.2-### Changed-- Debugging disabled by default (use `--debug` to enable).--## 0.16.1-### Added-- Cache mempool transactions.-- Improve initial syncing performance.--## 0.16.0-### Added-- Orphan transaction support.-- Full address balance cache in RocksDB.-- Full unspent output cache in RocksDB.--### Changed-- Significantly refactor code.-- Move web stuff to its own module.-- Change types related to databases.-- Make xpub balance, transaction and unspent queries fetch data in parallel.--## 0.15.2-### Added-- Internal data types to support orphan transactions.--### Changed-- Do not spam block actor with pings.-- Fix balance/unspent cache not reverting when importing fails.-- Fix transaction sorting algorithm not including transaction position information.-- Fix conflicting mempool transaction preventing block from importing.--## 0.15.1-### Changed-- Fix duplicate coinbase transaction id bug.--## 0.15.0-### Removed-- Removed `PreciseUnixTime` data type.--### Changed-- Use 64 bits for Unix time representation.-- Data model now uses simplified Unix time representation.--## 0.14.9-### Added-- Last external/change index information to xpub summary object.--## 0.14.8-### Added-- Endpoint for xpub summaries.-- Endpoints for full transactions.-- Ability to query by offset/limit.-- More API documentation.--## 0.14.7-### Added-- Debug information for `publishTx`.--### Changed-- Transaction publishing no longer requests mempool.-- Fixed serialization for `TxId` freezing the entire program by infinitely recursing.--## 0.14.6-### Changed-- Enable full threading again as it was deemed not responsible for the freezing behaviour.--## 0.14.5-### Changed-- Enable threading but leave it at a single thread to be able to open more than 1024 files.--## 0.14.4-### Changed-- Target LTS Haskell 13.20 and disable threading in new attempt to fix freezing bug.--## 0.14.3-### Changed-- Remove `-O2`.--## 0.14.2-### Changed-- Target LTS Haskell 12.26 to attempt to fix freezing bug.--## 0.14.1-### Added-- Extra debugging around code that freezes.--### Changed-- Bump dependency on `haskoin-node`.--## 0.14.0-### Removed-- Dump slow protobuf serialization.--### Added-- Add custom serialization.-- Extra debug logging.--### Changed-- Bump `haskoin-core` and `haskoin-node`.--## 0.13.1-### Changed-- Bump `haskoin-node` in `stack.yaml`.-- Do not send empty `getdata` messages.--## 0.13.0-### Added-- Primitive content negotiation for web exceptions.-- Protobuf support for errors.-- Protobuf support for tranasction ids.--### Changed-- Protobuf format changed in non-backwards-compatible manner for transaction ids.--## 0.12.0-### Added-- Support for binary serialization using Protocol Buffers.-- New endpoints for binary raw transactions (not hex-encoded).--### Changed-- Services field now a hex string instead of a number to avoid overflowing signed 64-bit integer.-- Flatten list of block data objects when responding to request for multiple block heights.-- Errors now reported in plain text without container JSON object.-- Transaction broadcasts are responded to with transaction id in plaintext hex (no JSON).-- Remove database snapshots to improve performance.--## 0.11.2-### Changed-- Fix duplicate mempool transaction announcements in event stream.--## 0.11.1-### Removed-- Removed latest block time check.--## 0.11.0-### Changed-- Improve post transactions endpoint.--## 0.10.1-### Changed-- Fix bug where transaction lists from multiple addresses would sort incorrectly.-- Address gap reduced to 20.--## 0.10.0-### Removed-- Remove addresses from transaction lists.-- No longer use container objects for xpub transactions.--## 0.9.3-### Added-- Permissive CORS headers to allow queries from any domain.-- Improved documentation using real-world examples from the BCH testnet.--## 0.9.2-### Added-- HTTP JSON API switch to turn off transaction list when retrieving blocks.--## 0.9.1-### Added-- Total block fees.-- Total block outputs.-- Block subsidy.--### Changed-- Do not consider the blocks less one block away from headers as out of sync in health check.-- Health check now returns HTTP 503 when not OK or out of sync.--## 0.9.0-### Added-- Version to health check output.-- Block weight for segwit.-- Transaction weight for segwit.--## 0.8.1-### Added-- Health check endpoint.--## 0.8.0-### Added-- Limits and skips.-- Add timestamps to transactions.-- Add transaction count to address balance object.-- Add Merkle root to block data.-- Total funds received by an address now shows up in balance.-- Balances for any address that ever had funds show up in xpub endpoints.--### Changed-- Data model update.-- Performance improvement for xpub calls.-- Transactions are returned in reverse mempool/block order (highest or most recent first).-- Balance objects do not get deleted from database ever.--## 0.6.9-### Changed-- Reduce number of coinbase checks to 10,000 ancestors.--## 0.6.8-### Changed-- Further optimize coinbase after height checks.--## 0.6.7-### Changed-- Impose restrictions on recursion for coinbase after height checks.--## 0.6.6-### Added-- Check whether a transaction can be traced back to a coinbase after certain height.--## 0.6.5-### Changed-- Delete transactions in reverse topological order when reverting a block.--## 0.6.4-### Changed-- Do not fail silently when importing orphan transactions into the mempool.--## 0.6.3-### Changed-- Dummy release to bump haskoin-node in stack.yaml.--## 0.6.2-### Changed-- Correct bug where coinbase transactions weren't properly flagged.--## 0.6.1-### Changed-- Compatibility with Bitcoin Cash hard fork.-- Various bug fixes.--## 0.6.0-### Added-- Address balance cache in memory.--### Changed-- Simplify data model further.-- Fix bug importing outputs with UTXO cache.-- Unspent balances cannot be negative.--## 0.5.0-### Added-- Add UTXO cache in memory.-- Get transactions with witness data in segwit networks.--### Changed-- Paths for derivations in xpubs is a list and no longer a string.-- Various bug fixes.--## 0.4.2-### Removed-- Remove extended public key itself from output of relevant endpoints to save bandwidth.--## 0.4.1-### Changed-- Fix bug when deleting coinbase transactions.-- Extended public key API support.--## 0.4.0-### Changed-- Generate events for mempool transactions.-- Respond with entire block data when querying blocks by height.--## 0.3.1-### Changed-- Do not import transactions to mempool while not synced.-- Only sync mempool against a single peer.-- Allow duplicate transactions to fix re-introduced sync bug.--## 0.3.0-### Added-- Update dependencies.-- Keep orphan blocks and deleted transactions in database.-- Add a `mainchain` field for block data and a `deleted` field for transactions.-- Stream records for performance.-- Show witness data for transaction inputs in SegWit network.-- Support RBF in SegWit network.--### Changed-- Refactor all data access code away from actor.-- Refactor import logic away from actor.-- Abstract data access using typeclasses.-- Implement data access using clean layered architecture.-- Make most of import logic code pure.-- Database now in `db` as opposed to `blocks` directory.-- Use latest `haskoin-node`.--### Removed-- Remove some data from peer information output.-- Remove full transaction from address transaction data.-- Remove limits from address transaction data.-- Remove block data from previous output.-- Remove spender from JSON response when output not spent.-- Remove block hash from block reference.--## 0.2.3-### Removed-- Do not send transaction notifications if a block is being imported.--## 0.2.2-### Added-- Peer information endpoint.--### Changed-- Update `haskoin-node`.--## 0.2.1-### Changed-- Fix tests--## 0.2.0-### Added-- Documentation everywhere.-- Ability to retrieve address transactions.--### Changed-- New versions of NQE and Haskoin Node upstream.-- Improve and simplify API.-- Multi-element endpoints return arrays of arrays.-- Database snapshots for all queries are now mandatory.--### Removed-- Retrieving unspent and spent outputs for an address.-- Redundant API endpoints for multiple elements.--## 0.1.3-### Changed-- Fix a bug with transaction notifications.-- Improve handling orphan transactions.--## 0.1.2-### Changed-- Specify dependencies better.--## 0.1.1-### Changed-- Dependency `secp256k1` is now `secp256k1-haskell`.--## 0.1.0-### Added-- New `CHANGELOG.md` file.-- Bitcoin (BTC) and Bitcoin Cash (BCH) compatibility.-- RocksDB database.-- Mempool support.-- HTTP streaming for events.-- CashAddr support.-- Bech32 support.-- Rate limits.--### Changed-- Split out of former `haskoin` repository.-- Use hpack and `package.yaml`.--### Removed-- Removed Stylish Haskell configuration file.-- Remvoed `haskoin-core` and `haskoin-wallet` packages from this repository.
− README.md
@@ -1,57 +0,0 @@-# Haskoin Store--Block chain store and index featuring:--- Persistent storage using [RocksDB](https://rocksdb.org/).-- [Bitcoin Cash (BCH)](https://www.bitcoincash.org/) support.-- [Bitcoin Core (BTC)](https://bitcoin.org/) support.-- Indices for address balances, transactions, and unspent outputs (UTXO).-- Persistent mempool.-- Replace Bitcoin Core (BTC) RBF transactions by default.-- Query transactions, balances and UTXO on extended keys (xpub).-- Optional accelerated xpub cache using Redis.-- RESTful API with JSON and binary serialization.-- High performance concurrent architecture.-- Compatibility with Blockchain.info-style endpoints.--## Install with Nix on any distribution--* Get [Nix](https://nixos.org/nix/)--```sh-nix-env --install stack-git clone https://github.com/haskoin/haskoin-store.git-cd haskoin-store-stack --nix build --copy-bins-~/.local/bin/haskoin-store --help-```--## Install on Ubuntu or Debian--```sh-apt install git zlib1g-dev libsecp256k1-dev librocksdb-dev pkg-config haskell-stack-git clone https://github.com/haskoin/haskoin-store.git-cd haskoin-store-stack build --copy-bins-~/.local/bin/haskoin-store --help-```--## Install on Fedora--```sh-dnf install git zlib-devel libsecp256k1-devel rocksdb-devel stack-git clone https://github.com/haskoin/haskoin-store.git-cd haskoin-store-stack build --copy-bins-~/.local/bin/haskoin-store --help-```--## Non-Haskell Dependencies--* [zlib](https://zlib.net/)-* [libsecp256k1](https://github.com/Bitcoin-ABC/secp256k1)-* [RocksDB](https://github.com/facebook/rocksdb)--## API Documentation--* [Swagger API Documentation](https://api.haskoin.com/)
app/Main.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE CPP #-} {-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedRecordDot #-}@@ -10,10 +11,9 @@ {-# LANGUAGE TupleSections #-} {-# LANGUAGE NoFieldSelectors #-} -module Main where+module Main (main) where  import Control.Applicative ((<|>))-import Control.Arrow (second) import Control.Monad (when) import Control.Monad.Cont (ContT (ContT), runContT) import Control.Monad.Logger@@ -30,18 +30,9 @@ import Data.Maybe (fromMaybe) import Data.String.Conversions (cs) import Data.Text qualified as T-import Data.Word (Word32) import Haskoin   ( Network (..),     allNets,-    bch,-    bchRegTest,-    bchTest,-    bchTest4,-    btc,-    btcRegTest,-    btcTest,-    eitherToMaybe,     netByName,     withContext,   )@@ -50,15 +41,12 @@   ( StoreConfig (..),     WebConfig (..),     WebLimits (..),-    WebTimeouts (..),     runWeb,     withStore,   )-import Haskoin.Store.Stats (withStats) import Options.Applicative   ( Parser,     auto,-    eitherReader,     execParser,     flag,     fullDesc,@@ -79,7 +67,11 @@   ) import System.Exit (exitSuccess) import System.FilePath ((</>))-import System.IO.Unsafe (unsafePerformIO)+import System.Metrics.StatsD+    ( defStatConfig,+      withStats,+      StatConfig(statsdPort, statsdServer, namespace, reportSamples,+                 reportStats) ) import Text.Read (readMaybe) import UnliftIO (MonadIO) import UnliftIO.Directory@@ -88,12 +80,12 @@   ) import UnliftIO.Environment (lookupEnv) -version :: String+haskoinStoreVersion :: String  #ifdef CURRENT_PACKAGE_VERSION-version = CURRENT_PACKAGE_VERSION+haskoinStoreVersion = CURRENT_PACKAGE_VERSION #else-version = "Unavailable"+haskoinStoreVersion = "Unavailable" #endif  data Config = Config@@ -109,7 +101,6 @@     maxLaggingBlocks :: !Int,     minPeers :: !Int,     webLimits :: !WebLimits,-    webTimeouts :: !WebTimeouts,     redis :: !Bool,     redisURL :: !String,     redisMinAddrs :: !Int,@@ -125,6 +116,8 @@     statsdHost :: !String,     statsdPort :: !Int,     statsdPrefix :: !String,+    statsdNoEvents :: Bool,+    statsdAggregates :: Bool,     tickerRefresh :: !Int,     tickerURL :: !String,     priceHistoryURL :: !String,@@ -162,8 +155,6 @@     env "MIN_PEERS" 1 readMaybe   webLimits <-     getWebLimits-  webTimeouts <--    getWebTimeouts   redis <-     env "REDIS" False parseBool   redisURL <-@@ -192,6 +183,10 @@     env "STATSD_HOST" "localhost" pure   statsdPort <-     env "STATSD_PORT" 8125 readMaybe+  statsdNoEvents <-+    env "STATSD_NO_EVENTS" False parseBool+  statsdAggregates <-+    env "STATSD_AGGREGATES" False parseBool   statsdPrefix <-     getStatsdPrefix   tickerRefresh <-@@ -216,31 +211,24 @@       getAppUserDataDirectory "haskoin-store" >>= \d ->         env "DIR" d pure     getWebLimits = do-      let WebLimits {..} = def+      let d = def :: WebLimits       maxItemCount <--        env "MAX_ITEM_COUNT" maxItemCount readMaybe+        env "MAX_ITEM_COUNT" d.maxItemCount readMaybe       maxFullItemCount <--        env "MAX_FULL_ITEM_COUNT" maxFullItemCount readMaybe+        env "MAX_FULL_ITEM_COUNT" d.maxFullItemCount readMaybe       maxOffset <--        env "MAX_OFFSET" maxOffset readMaybe+        env "MAX_OFFSET" d.maxOffset readMaybe       defItemCount <--        env "DEF_ITEM_COUNT" defItemCount readMaybe+        env "DEF_ITEM_COUNT" d.defItemCount readMaybe       xpubGap <--        env "XPUB_GAP" xpubGap readMaybe+        env "XPUB_GAP" d.xpubGap readMaybe       xpubGapInit <--        env "XPUB_GAP_INIT" xpubGapInit readMaybe-      maxBodySize <--        env "MAX_BODY_SIZE" maxBodySize readMaybe-      requestTimeout <--        env "REQUEST_TIMEOUT" requestTimeout readMaybe+        env "XPUB_GAP_INIT" d.xpubGapInit readMaybe+      blockTimeout <-+        env "BLOCK_TIMEOUT" d.blockTimeout readMaybe+      txTimeout <-+        env "TX_TIMEOUT" d.txTimeout readMaybe       return WebLimits {..}-    getWebTimeouts = do-      let WebTimeouts {..} = def-      block <--        env "BLOCK_TIMEOUT" block readMaybe-      tx <--        env "TX_TIMEOUT" tx readMaybe-      return WebTimeouts {..}     getStatsdPrefix = do       let go = prefix <|> nomad           prefix =@@ -385,37 +373,21 @@           <> help "Max gap for empty xpubs"           <> showDefault           <> value c.webLimits.xpubGapInit-    maxBodySize <--      option auto $-        metavar "BYTES"-          <> long "max-body-size"-          <> help "Maximum request body size"-          <> showDefault-          <> value c.webLimits.maxBodySize-    requestTimeout <--      option auto $-        metavar "SECONDS"-          <> long "request-timeout"-          <> help "Web request timeout"-          <> showDefault-          <> value c.webLimits.requestTimeout-    return WebLimits {..}-  webTimeouts <- do-    block <-+    blockTimeout <-       option auto $         metavar "SECONDS"           <> long "block-timeout"           <> help "Block lag health timeout"           <> showDefault-          <> value c.webTimeouts.block-    tx <-+          <> value c.webLimits.blockTimeout+    txTimeout <-       option auto $         metavar "SECONDS"           <> long "tx-timeout"           <> help "Last tx received health timeout"           <> showDefault-          <> value c.webTimeouts.tx-    return WebTimeouts {..}+          <> value c.webLimits.txTimeout+    return WebLimits {..}   redis <-     flag c.redis True $       long "redis"@@ -505,6 +477,14 @@         <> help "Prefix for statsd metrics"         <> showDefault         <> value c.statsdPrefix+  statsdNoEvents <-+    flag c.statsdNoEvents True $+      long "statsd-no-events"+        <> help "Do not report individual events to statsd"+  statsdAggregates <-+    flag c.statsdAggregates True $+      long "statsd-aggregates"+        <> help "Send statistical aggregate reports to statsd every second"   tickerRefresh <-     option auto $       metavar "MICROSECONDS"@@ -549,15 +529,11 @@     Just net -> Right net     Nothing -> Left "Network name invalid" -peerReader :: String -> Either String String-peerReader "" = Left "Peer cannot be blank"-peerReader s = Right s- main :: IO () main = do   c <- execParser . opts =<< defConfig   when c.version $ do-    putStrLn version+    putStrLn haskoinStoreVersion     exitSuccess   if null c.peers && not c.discover     then run $ let Config {..} = c in Config {discover = True, ..}@@ -568,14 +544,14 @@         fullDesc           <> progDesc "Bitcoin (BCH & BTC) block chain index with HTTP API"           <> Options.Applicative.header-            ("haskoin-store version " <> version)+            ("haskoin-store version " <> haskoinStoreVersion)  run :: Config -> IO () run cfg =   withContext $ \ctx ->     runStderrLoggingT $ filterLogger l $ flip runContT return $ do-      stats <- ContT $ with_stats       net <- either error return $ networkReader cfg.net+      stats <- ContT $ with_stats net       let dir = cfg.dir </> net.name       $(logInfoS) "haskoin-store" $         "Creating working directory (if not found): " <> cs dir@@ -601,7 +577,7 @@                 peerTimeout = fromIntegral cfg.peerTimeout,                 maxPeerLife = fromIntegral cfg.maxPeerLife,                 connect = withConnection,-                statsStore = stats,+                stats = stats,                 redisSyncInterval = cfg.redisSyncInterval               }       lift $@@ -611,13 +587,12 @@               port = cfg.port,               store = store,               limits = cfg.webLimits,-              timeouts = cfg.webTimeouts,               maxPendingTxs = cfg.maxPendingTxs,               maxLaggingBlocks = cfg.maxLaggingBlocks,               minPeers = cfg.minPeers,-              version = version,+              version = haskoinStoreVersion,               noMempool = cfg.noMempool,-              statsStore = stats,+              stats = stats,               tickerRefresh = cfg.tickerRefresh,               tickerURL = cfg.tickerURL,               priceHistoryURL = cfg.priceHistoryURL,@@ -626,7 +601,7 @@               healthCheckInterval = cfg.healthCheckInterval             }   where-    with_stats go+    with_stats net go       | cfg.statsd = do           $(logInfoS) "Main" $             "Sending stats to "@@ -636,9 +611,13 @@               <> " with prefix: "               <> T.pack cfg.statsdPrefix           withStats-            (T.pack cfg.statsdHost)-            cfg.statsdPort-            (T.pack cfg.statsdPrefix)+            defStatConfig+              { statsdServer = cfg.statsdHost,+                statsdPort = cfg.statsdPort,+                namespace = cfg.statsdPrefix <> "." <> net.name,+                reportSamples = not cfg.statsdNoEvents,+                reportStats = cfg.statsdAggregates+              }             (go . Just)       | otherwise = go Nothing     l _ lvl
haskoin-store.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           haskoin-store-version:        1.0.1+version:        1.1.0 synopsis:       Storage and index for Bitcoin and Bitcoin Cash description:    Please see the README on GitHub at <https://github.com/haskoin/haskoin-store#readme> category:       Bitcoin, Finance, Network@@ -17,9 +17,6 @@ license:        MIT license-file:   LICENSE build-type:     Simple-extra-source-files:-    README.md-    CHANGELOG.md  source-repository head   type: git@@ -36,12 +33,12 @@       Haskoin.Store.Database.Writer       Haskoin.Store.Logic       Haskoin.Store.Manager-      Haskoin.Store.Stats       Haskoin.Store.Web   other-modules:       Paths_haskoin_store   hs-source-dirs:       src+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints   build-depends:       aeson >=1.4.7.1     , aeson-pretty >=0.8.8@@ -54,14 +51,11 @@     , containers >=0.6.2.1     , data-default >=0.7.1.1     , deepseq >=1.4.4.0-    , ekg-core >=0.1.1.7-    , ekg-statsd >=0.2.5-    , foldl >=1.4.10     , hashable >=1.3.0.0     , hashtables >=1.2.4.2     , haskoin-core >=1.0.0     , haskoin-node >=1.0.0-    , haskoin-store-data ==1.0.1+    , haskoin-store-data ==1.1.0     , hedis >=0.12.13     , http-types >=0.12.3     , lens >=4.18.1@@ -74,6 +68,7 @@     , rocksdb-haskell-jprupp >=2.1.3     , rocksdb-query >=0.4.0     , scotty >=0.11.5+    , statsd-rupp >=0.4     , stm >=2.5.0.0     , string-conversions >=0.4.0.1     , text >=1.2.3.0@@ -83,7 +78,6 @@     , unordered-containers >=0.2.10.0     , vault >=0.3.1.5     , wai >=3.2.2.1-    , wai-extra >=3.1.6     , wai-websockets >=3.0.1.2     , warp >=3.3.10     , websockets >=0.12.4@@ -96,7 +90,7 @@       Paths_haskoin_store   hs-source-dirs:       app-  ghc-options: -threaded -rtsopts -with-rtsopts=-N+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N   build-depends:       aeson >=1.4.7.1     , aeson-pretty >=0.8.8@@ -109,16 +103,13 @@     , containers >=0.6.2.1     , data-default >=0.7.1.1     , deepseq >=1.4.4.0-    , ekg-core >=0.1.1.7-    , ekg-statsd >=0.2.5     , filepath-    , foldl >=1.4.10     , hashable >=1.3.0.0     , hashtables >=1.2.4.2     , haskoin-core >=1.0.0     , haskoin-node >=1.0.0     , haskoin-store-    , haskoin-store-data ==1.0.1+    , haskoin-store-data ==1.1.0     , hedis >=0.12.13     , http-types >=0.12.3     , lens >=4.18.1@@ -132,6 +123,7 @@     , rocksdb-haskell-jprupp >=2.1.3     , rocksdb-query >=0.4.0     , scotty >=0.11.5+    , statsd-rupp >=0.4     , stm >=2.5.0.0     , string-conversions >=0.4.0.1     , text >=1.2.3.0@@ -141,7 +133,6 @@     , unordered-containers >=0.2.10.0     , vault >=0.3.1.5     , wai >=3.2.2.1-    , wai-extra >=3.1.6     , wai-websockets >=3.0.1.2     , warp >=3.3.10     , websockets >=0.12.4@@ -157,7 +148,7 @@       Paths_haskoin_store   hs-source-dirs:       test-  ghc-options: -threaded -rtsopts -with-rtsopts=-N+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N   build-depends:       QuickCheck >=2.13.2     , aeson >=1.4.7.1@@ -172,15 +163,12 @@     , containers >=0.6.2.1     , data-default >=0.7.1.1     , deepseq >=1.4.4.0-    , ekg-core >=0.1.1.7-    , ekg-statsd >=0.2.5-    , foldl >=1.4.10     , hashable >=1.3.0.0     , hashtables >=1.2.4.2     , haskoin-core >=1.0.0     , haskoin-node >=1.0.0     , haskoin-store-    , haskoin-store-data ==1.0.1+    , haskoin-store-data ==1.1.0     , hedis >=0.12.13     , hspec >=2.7.1     , http-types >=0.12.3@@ -194,6 +182,7 @@     , rocksdb-haskell-jprupp >=2.1.3     , rocksdb-query >=0.4.0     , scotty >=0.11.5+    , statsd-rupp >=0.4     , stm >=2.5.0.0     , string-conversions >=0.4.0.1     , text >=1.2.3.0@@ -203,7 +192,6 @@     , unordered-containers >=0.2.10.0     , vault >=0.3.1.5     , wai >=3.2.2.1-    , wai-extra >=3.1.6     , wai-websockets >=3.0.1.2     , warp >=3.3.10     , websockets >=0.12.4
src/Haskoin/Store/BlockStore.hs view
@@ -150,9 +150,9 @@     newMempoolTx,     revertBlock,   )-import Haskoin.Store.Stats import NQE-  ( Listen,+  ( InChan,+    Listen,     Mailbox,     Publisher,     inboxToMailbox,@@ -163,9 +163,7 @@     send,     sendSTM,   )-import System.Metrics qualified as Metrics-import System.Metrics.Gauge qualified as Metrics (Gauge)-import System.Metrics.Gauge qualified as Metrics.Gauge+import System.Metrics.StatsD import System.Random (randomRIO) import UnliftIO   ( Exception,@@ -229,69 +227,60 @@   }  data StoreMetrics = StoreMetrics-  { storeHeight :: !Metrics.Gauge,-    headersHeight :: !Metrics.Gauge,-    pendingTxs :: !Metrics.Gauge,-    connectedPeers :: !Metrics.Gauge,-    mempoolSize :: !Metrics.Gauge+  { blocks :: !StatGauge,+    headers :: !StatGauge,+    queuedTxs :: !StatGauge,+    peers :: !StatGauge,+    mempool :: !StatGauge   } -newStoreMetrics :: (MonadIO m) => Metrics.Store -> m StoreMetrics-newStoreMetrics s = liftIO $ do-  storeHeight <- g "blockchain.height"-  headersHeight <- g "blockchain.headers"-  pendingTxs <- g "mempool.pending_txs"-  connectedPeers <- g "network.peers_connected"-  mempoolSize <- g "mempool.size"-  return StoreMetrics {..}+newStoreMetrics :: (MonadIO m) => BlockStoreConfig -> m (Maybe StoreMetrics)+newStoreMetrics cfg =+  forM cfg.stats $ \s -> liftIO $ do+    m <- withDB cfg.db getMempool+    b <- fmap (maybe 0 (.height)) $ withDB cfg.db $ runMaybeT $+        MaybeT getBestBlock >>= MaybeT . getBlock+    h <- chainGetBest cfg.chain+    p <- getPeers cfg.peerMgr+    blocks <- g s "blocks" (fromIntegral b)+    headers <- g s "headers" (fromIntegral h.height)+    queuedTxs <- g s "queued_txs" 0+    peers <- g s "peers" (length p)+    mempool <- g s "mempool" (length m)+    return StoreMetrics {..}   where-    g x = Metrics.createGauge ("store." <> x) s+    g s x = newStatGauge s ("store." <> x)  setStoreHeight :: (MonadIO m) => BlockT m ()-setStoreHeight =-  asks (.metrics) >>= \case-    Nothing -> return ()-    Just m ->-      getBestBlock >>= \case-        Nothing -> setit m 0-        Just bb ->-          getBlock bb >>= \case-            Nothing -> setit m 0-            Just b -> setit m b.height-  where-    setit m i = liftIO $ m.storeHeight `Metrics.Gauge.set` fromIntegral i+setStoreHeight = void $ runMaybeT $ do+  m <- MaybeT (asks (.metrics))+  h <- MaybeT getBestBlock+  b <- MaybeT (getBlock h)+  setGauge m.blocks (fromIntegral b.height)  setHeadersHeight :: (MonadIO m) => BlockT m ()-setHeadersHeight =-  asks (.metrics) >>= \case-    Nothing -> return ()-    Just m -> do-      h <- fmap (.height) $ chainGetBest =<< asks (.config.chain)-      liftIO $ m.headersHeight `Metrics.Gauge.set` fromIntegral h+setHeadersHeight = void $ runMaybeT $ do+  m <- MaybeT (asks (.metrics))+  n <- chainGetBest =<< asks (.config.chain)+  setGauge m.headers (fromIntegral n.height)  setPendingTxs :: (MonadIO m) => BlockT m ()-setPendingTxs =-  asks (.metrics) >>= \case-    Nothing -> return ()-    Just m -> do-      s <- asks (.txs) >>= \t -> atomically (HashMap.size <$> readTVar t)-      liftIO $ m.pendingTxs `Metrics.Gauge.set` fromIntegral s+setPendingTxs = void $ runMaybeT $ do+  m <- MaybeT (asks (.metrics))+  p <- readTVarIO =<< asks (.txs)+  setGauge m.queuedTxs (HashMap.size p)  setPeersConnected :: (MonadIO m) => BlockT m ()-setPeersConnected =-  asks (.metrics) >>= \case-    Nothing -> return ()-    Just m -> do-      ps <- fmap length $ getPeers =<< asks (.config.peerMgr)-      liftIO $ m.connectedPeers `Metrics.Gauge.set` fromIntegral ps+setPeersConnected = void $ runMaybeT $ do+  m <- MaybeT (asks (.metrics))+  p <- getPeers =<< asks (.config.peerMgr)+  setGauge m.peers (length p)  setMempoolSize :: (MonadIO m) => BlockT m ()-setMempoolSize =-  asks (.metrics) >>= \case-    Nothing -> return ()-    Just m -> do-      s <- length <$> getMempool-      liftIO $ m.mempoolSize `Metrics.Gauge.set` fromIntegral s+setMempoolSize = void $ runMaybeT $ do+  m <- MaybeT (asks (.metrics))+  p <- lift getMempool+  setGauge m.mempool (length p)  -- | Configuration for a block store. data BlockStoreConfig = BlockStoreConfig@@ -314,7 +303,7 @@     syncMempool :: !Bool,     -- | disconnect syncing peer if inactive for this long     peerTimeout :: !NominalDiffTime,-    statsStore :: !(Maybe Metrics.Store)+    stats :: !(Maybe Stats)   }  type BlockT m = ReaderT BlockStore m@@ -391,7 +380,7 @@   ts <- newTVarIO HashMap.empty   rq <- newTVarIO HashSet.empty   inbox <- newInbox-  metrics <- mapM newStoreMetrics cfg.statsStore+  metrics <- newStoreMetrics cfg   let r =         BlockStore           { mailbox = inboxToMailbox inbox,@@ -442,6 +431,10 @@       withAsync (pingMe (inboxToMailbox inbox)) $ \a ->         link a >> runBlockStoreLoop inbox +runBlockStoreLoop ::+  (InChan mbox, MonadUnliftIO m, MonadLoggerIO m) =>+  mbox BlockStoreMessage ->+  ReaderT BlockStore m b runBlockStoreLoop inbox =   forever $ do     $(logDebugS) "BlockStore" "Waiting for new event..."@@ -470,8 +463,8 @@   s <- asks (.config.syncMempool)   when s f -mempool :: (MonadUnliftIO m, MonadLoggerIO m) => Peer -> BlockT m ()-mempool p =+requestMempool :: (MonadUnliftIO m, MonadLoggerIO m) => Peer -> BlockT m ()+requestMempool p =   guardMempool . syncMempool $     isInSync >>= \s -> when s $ do       $(logDebugS) "BlockStore" $@@ -529,7 +522,7 @@         False -> syncMe         True -> do           updateOrphans-          mempool peer+          requestMempool peer     failure e = do       $(logErrorS) "BlockStore" $         "Error importing block "@@ -633,7 +626,6 @@   atomically $ do     modifyTVar ts $ HashMap.insert th p     modifyTVar rq $ HashSet.delete th-    HashMap.size <$> readTVar ts   setPendingTxs   where     th = txHash p.tx@@ -765,9 +757,9 @@     new_mempool_tx = do       t <- newMempoolTx tx seconds       $(logInfoS) "BlockStore" $-        "Tx "+        bool "Already have" "Imported" t+          <> " tx "           <> txHashToHex (txHash tx)-          <> bool " was already imported" " imported" t       when t $ fulfillOrphans block_read tx_hash       return t @@ -797,9 +789,7 @@   unless (null txs) (import_txs block_read txs)   where     run_import block_read p =-      let t = p.tx-          h = txHash t-       in importMempoolTx block_read p.time p.tx+      importMempoolTx block_read p.time p.tx     import_txs block_read txs =       let r = mapM (run_import block_read) txs        in do@@ -1097,7 +1087,7 @@ trySyncingPeer :: (MonadUnliftIO m, MonadLoggerIO m) => Peer -> BlockT m () trySyncingPeer p =   isInSync >>= \case-    True -> mempool p+    True -> requestMempool p     False ->       trySetPeer p >>= \case         False -> return ()
src/Haskoin/Store/Cache.hs view
@@ -37,46 +37,34 @@ where  import Control.DeepSeq (NFData)-import Control.Monad (forM, forM_, forever, guard, unless, void, when, (>=>))+import Control.Monad (forM, forM_, forever, unless, void, when) import Control.Monad.Logger   ( MonadLoggerIO,     logDebugS,     logErrorS,     logInfoS,-    logWarnS,   )-import Control.Monad.Reader (ReaderT (..), ask, asks)+import Control.Monad.Reader (ReaderT (..), asks) import Control.Monad.Trans (lift)-import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)-import Data.Bits (complement, shift, (.&.), (.|.))+import Data.Bits (shift, (.&.), (.|.)) import Data.ByteString (ByteString)-import Data.ByteString qualified as B import Data.Default (def) import Data.Either (fromRight, isRight, rights) import Data.Functor ((<&>)) import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict qualified as HashMap-import Data.HashSet (HashSet) import Data.HashSet qualified as HashSet import Data.IntMap.Strict qualified as I import Data.List (sort) import Data.Map.Strict qualified as Map import Data.Maybe   ( catMaybes,-    fromMaybe,-    isJust,-    isNothing,     mapMaybe,   ) import Data.Serialize (Serialize, decode, encode) import Data.String.Conversions (cs) import Data.Text (Text)-import Data.Time.Clock (NominalDiffTime, diffUTCTime)-import Data.Time.Clock.System-  ( getSystemTime,-    systemSeconds,-    systemToUTCTime,-  )+import Data.Time.Clock.POSIX (getPOSIXTime) import Data.Word (Word32, Word64) import Database.Redis   ( Connection,@@ -93,12 +81,10 @@     zrem,   ) import Database.Redis qualified as Redis-import Database.Redis qualified as Reids import GHC.Generics (Generic) import Haskoin   ( Address,     BlockHash,-    BlockHeader (..),     BlockNode (..),     Ctx,     DerivPathI (..),@@ -124,8 +110,6 @@   ) import Haskoin.Node   ( Chain,-    chainBlockMain,-    chainGetAncestor,     chainGetBest,     chainGetBlock,     chainGetParents,@@ -133,49 +117,35 @@   ) import Haskoin.Store.Common import Haskoin.Store.Data-import Haskoin.Store.Stats+import Haskoin.Store.Database.Reader (DatabaseReader, withDB) import NQE   ( Inbox,     Listen,     Mailbox,-    inboxToMailbox,     query,     receive,     send,   )-import System.Metrics qualified as Metrics-import System.Metrics.Counter qualified as Metrics (Counter)-import System.Metrics.Counter qualified as Metrics.Counter-import System.Metrics.Distribution qualified as Metrics (Distribution)-import System.Metrics.Distribution qualified as Metrics.Distribution-import System.Metrics.Gauge qualified as Metrics (Gauge)-import System.Metrics.Gauge qualified as Metrics.Gauge-import System.Random (randomIO, randomRIO)+import System.Metrics.StatsD import UnliftIO   ( Exception,     MonadIO,     MonadUnliftIO,-    TQueue,-    TVar,     atomically,     bracket,     liftIO,-    link,-    modifyTVar,-    newTVarIO,-    readTQueue,-    readTVar,     throwIO,-    wait,     withAsync,-    writeTQueue,-    writeTVar,   ) import UnliftIO.Concurrent (threadDelay)  runRedis :: (MonadLoggerIO m) => Redis (Either Reply a) -> CacheX m a runRedis action = do   conn <- asks (.redis)+  withRedis conn action++withRedis :: (MonadLoggerIO m) => Connection -> Redis (Either Reply a) -> m a+withRedis conn action =   liftIO (Redis.runRedis conn action) >>= \case     Right x -> return x     Left e -> do@@ -191,64 +161,69 @@   }  data CacheMetrics = CacheMetrics-  { cacheHits :: !Metrics.Counter,-    cacheMisses :: !Metrics.Counter,-    lockAcquired :: !Metrics.Counter,-    lockReleased :: !Metrics.Counter,-    lockFailed :: !Metrics.Counter,-    xPubBals :: !Metrics.Counter,-    xPubUnspents :: !Metrics.Counter,-    xPubTx :: !Metrics.Counter,-    xPubTxCount :: !Metrics.Counter,-    indexTime :: !StatDist+  { hits :: !StatCounter,+    misses :: !StatCounter,+    lockAcquired :: !StatCounter,+    lockReleased :: !StatCounter,+    lockFailed :: !StatCounter,+    xBalances :: !StatCounter,+    xUnspents :: !StatCounter,+    xTxs :: !StatCounter,+    xTxCount :: !StatCounter,+    indexTime :: !StatTiming,+    height :: !StatGauge   } -newCacheMetrics :: (MonadIO m) => Metrics.Store -> m CacheMetrics-newCacheMetrics s = liftIO $ do-  cacheHits <- c "cache.hits"-  cacheMisses <- c "cache.misses"+newCacheMetrics ::+  (MonadLoggerIO m) =>+  Stats ->+  Connection ->+  DatabaseReader ->+  m CacheMetrics+newCacheMetrics s r db = do+  h <- redisBest+  hits <- c "cache.hits"+  misses <- c "cache.misses"   lockAcquired <- c "cache.lock_acquired"   lockReleased <- c "cache.lock_released"   lockFailed <- c "cache.lock_failed"   indexTime <- d "cache.index"-  xPubBals <- c "cache.xpub_balances_cached"-  xPubUnspents <- c "cache.xpub_unspents_cached"-  xPubTx <- c "cache.xpub_txs_cached"-  xPubTxCount <- c "cache.xpub_tx_count_cached"+  xBalances <- c "cache.xpub_balances"+  xUnspents <- c "cache.xpub_unspents"+  xTxs <- c "cache.xpub_txs"+  xTxCount <- c "cache.xpub_tx_count"+  height <- newStatGauge s "cache.height" h   return CacheMetrics {..}   where-    c x = Metrics.createCounter x s-    d x = createStatDist x s+    redisBest =+      maybe dbBest dbBlock =<< withRedis r redisGetHead+    dbBlock h =+      maybe 0 (fromIntegral . (.height)) <$> withDB db (getBlock h)+    dbBest =+      maybe (return 0) dbBlock =<< withDB db getBestBlock+    c x = newStatCounter s x 10+    d x = newStatTiming s x 10 -withMetrics ::+withTiming ::   (MonadUnliftIO m) =>-  (CacheMetrics -> StatDist) ->+  (CacheMetrics -> StatTiming) ->   CacheX m a ->   CacheX m a-withMetrics df go =+withTiming df go =   asks (.metrics) >>= \case     Nothing -> go     Just m ->       bracket-        (systemToUTCTime <$> liftIO getSystemTime)+        time         (end m)         (const go)   where-    end metrics t1 = do-      t2 <- systemToUTCTime <$> liftIO getSystemTime-      let diff = round $ diffUTCTime t2 t1 * 1000-      df metrics `addStatTime` diff-      addStatQuery (df metrics)+    time = round . (* 1000) <$> liftIO getPOSIXTime+    end m t1 = time >>= addTiming (df m) . subtract t1 -incrementCounter ::-  (MonadIO m) =>-  (CacheMetrics -> Metrics.Counter) ->-  Int ->-  CacheX m ()-incrementCounter f i =-  asks (.metrics) >>= \case-    Just s -> liftIO $ Metrics.Counter.add (f s) (fromIntegral i)-    Nothing -> return ()+withMetrics ::+  (MonadIO m) => (CacheMetrics -> CacheX m a) -> CacheX m ()+withMetrics go = asks (.metrics) >>= mapM_ go  type CacheT = ReaderT (Maybe CacheConfig) @@ -271,7 +246,22 @@   liftIO (checkedConnect conninfo)  instance-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>+  (MonadUnliftIO m, StoreReadBase m) =>+  StoreReadBase (CacheX m)+  where+  getCtx = lift getCtx+  getNetwork = lift getNetwork+  getBestBlock = lift getBestBlock+  getBlocksAtHeight = lift . getBlocksAtHeight+  getBlock = lift . getBlock+  getTxData = lift . getTxData+  getSpender = lift . getSpender+  getBalance = lift . getBalance+  getUnspent = lift . getUnspent+  getMempool = lift getMempool++instance+  (MonadUnliftIO m, StoreReadBase m) =>   StoreReadBase (CacheT m)   where   getCtx = lift getCtx@@ -287,6 +277,23 @@  instance   (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m) =>+  StoreReadExtra (CacheX m)+  where+  getBalances = lift . getBalances+  getAddressesTxs addrs = lift . getAddressesTxs addrs+  getAddressTxs addr = lift . getAddressTxs addr+  getAddressUnspents addr = lift . getAddressUnspents addr+  getAddressesUnspents addrs = lift . getAddressesUnspents addrs+  getMaxGap = lift getMaxGap+  getInitialGap = lift getInitialGap+  getNumTxData = lift . getNumTxData+  xPubTxs = getXPubTxs+  xPubTxCount = getXPubTxCount+  xPubBals = getXPubBalances+  xPubUnspents = getXPubUnspents++instance+  (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m) =>   StoreReadExtra (CacheT m)   where   getBalances = lift . getBalances@@ -298,39 +305,23 @@   getInitialGap = lift getInitialGap   getNumTxData = lift . getNumTxData   xPubBals xpub =-    ask >>= \case-      Nothing ->-        lift $-          xPubBals xpub-      Just cfg ->-        lift $-          runReaderT (getXPubBalances xpub) cfg+    ReaderT $ \case+      Nothing -> xPubBals xpub+      Just cfg -> runReaderT (xPubBals xpub) cfg   xPubUnspents xpub xbals limits =-    ask >>= \case-      Nothing ->-        lift $-          xPubUnspents xpub xbals limits-      Just cfg ->-        lift $-          runReaderT (getXPubUnspents xpub xbals limits) cfg+    ReaderT $ \case+      Nothing -> xPubUnspents xpub xbals limits+      Just cfg -> runReaderT (xPubUnspents xpub xbals limits) cfg   xPubTxs xpub xbals limits =-    ask >>= \case-      Nothing ->-        lift $-          xPubTxs xpub xbals limits-      Just cfg ->-        lift $-          runReaderT (getXPubTxs xpub xbals limits) cfg+    ReaderT $ \case+      Nothing -> xPubTxs xpub xbals limits+      Just cfg -> runReaderT (xPubTxs xpub xbals limits) cfg   xPubTxCount xpub xbals =-    ask >>= \case-      Nothing ->-        lift $-          xPubTxCount xpub xbals-      Just cfg ->-        lift $-          runReaderT (getXPubTxCount xpub xbals) cfg+    ReaderT $ \case+      Nothing -> xPubTxCount xpub xbals+      Just cfg -> runReaderT (xPubTxCount xpub xbals) cfg -withCache :: (StoreReadBase m) => Maybe CacheConfig -> CacheT m a -> m a+withCache :: Maybe CacheConfig -> CacheT m a -> m a withCache s f = runReaderT f s  balancesPfx :: ByteString@@ -353,40 +344,41 @@   CacheX m [TxRef] getXPubTxs xpub xbals limits = go False   where-    go m =-      isXPubCached xpub >>= \c ->-        if c-          then do-            txs <- cacheGetXPubTxs xpub limits-            incrementCounter (.xPubTx) (length txs)-            return txs-          else do-            if m-              then lift $ xPubTxs xpub xbals limits-              else do-                newXPubC xpub xbals-                go True+    go recursed = do+      cached <- isXPubCached recursed xpub+      if cached+        then do+          txs <- cacheGetXPubTxs xpub limits+          withMetrics $ \s ->+            incrementCounter s.xTxs (length txs)+          return txs+        else do+          if recursed+            then lift $ xPubTxs xpub xbals limits+            else do+              newXPubC xpub xbals+              go True  getXPubTxCount ::   (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m) =>   XPubSpec ->   [XPubBal] ->   CacheX m Word32-getXPubTxCount xpub xbals =-  go False+getXPubTxCount xpub xbals = go False   where-    go t =-      isXPubCached xpub >>= \c ->-        if c-          then do-            incrementCounter (.xPubTxCount) 1-            cacheGetXPubTxCount xpub-          else do-            if t-              then lift $ xPubTxCount xpub xbals-              else do-                newXPubC xpub xbals-                go True+    go recursed = do+      cached <- isXPubCached recursed xpub+      if cached+        then do+          withMetrics $ \s ->+            incrementCounter s.xTxCount 1+          cacheGetXPubTxCount xpub+        else do+          if recursed+            then lift $ xPubTxCount xpub xbals+            else do+              newXPubC xpub xbals+              go True  getXPubUnspents ::   (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m) =>@@ -397,70 +389,69 @@ getXPubUnspents xpub xbals limits =   go False   where-    xm =-      let f x = (x.balance.address, x)-          g = (> 0) . (.balance.utxo)-       in HashMap.fromList $ map f $ filter g xbals-    go m =-      isXPubCached xpub >>= \c ->-        if c-          then do-            process-          else do-            if m-              then lift $ xPubUnspents xpub xbals limits-              else do-                newXPubC xpub xbals-                go True-    process = do-      ops <- map snd <$> cacheGetXPubUnspents xpub limits-      uns <- catMaybes <$> lift (mapM getUnspent ops)-      ctx <- lift getCtx-      let f u =-            either-              (const Nothing)-              (\a -> Just (a, u))-              (scriptToAddressBS ctx u.script)-          g a = HashMap.lookup a xm-          h u x =-            XPubUnspent-              { unspent = u,-                path = x.path-              }-          us = mapMaybe f uns-          i a u = h u <$> g a-      incrementCounter (.xPubUnspents) (length us)-      return $ mapMaybe (uncurry i) us+    go recursed = do+      cached <- isXPubCached recursed xpub+      if cached+        then do+          ops <- map snd <$> cacheGetXPubUnspents xpub limits+          uns <- catMaybes <$> lift (mapM getUnspent ops)+          ctx <- lift getCtx+          let f u =+                either+                  (const Nothing)+                  (\a -> Just (a, u))+                  (scriptToAddressBS ctx u.script)+              xm =+                let j x = (x.balance.address, x)+                    k = (> 0) . (.balance.utxo)+                 in HashMap.fromList $ map j $ filter k xbals+              g a = HashMap.lookup a xm+              h u x =+                XPubUnspent+                  { unspent = u,+                    path = x.path+                  }+              us = mapMaybe f uns+              i a u = h u <$> g a+          withMetrics $ \s -> incrementCounter s.xUnspents (length us)+          return $ mapMaybe (uncurry i) us+        else do+          if recursed+            then lift $ xPubUnspents xpub xbals limits+            else do+              newXPubC xpub xbals+              go True  getXPubBalances ::   (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m) =>   XPubSpec ->   CacheX m [XPubBal]-getXPubBalances xpub =-  isXPubCached xpub >>= \c ->-    if c-      then do-        xbals <- cacheGetXPubBalances xpub-        incrementCounter (.xPubBals) (length xbals)-        return xbals-      else do-        bals <- lift $ xPubBals xpub-        newXPubC xpub bals-        return bals+getXPubBalances xpub = do+  cached <- isXPubCached False xpub+  if cached+    then do+      xbals <- cacheGetXPubBalances xpub+      withMetrics $ \s -> incrementCounter s.xBalances (length xbals)+      return xbals+    else do+      bals <- lift $ xPubBals xpub+      newXPubC xpub bals+      return bals  isInCache :: (MonadLoggerIO m) => XPubSpec -> CacheT m Bool isInCache xpub =-  ask >>= \case+  ReaderT $ \case     Nothing -> return False-    Just cfg -> runReaderT (isXPubCached xpub) cfg+    Just cfg -> runReaderT (isXPubCached True xpub) cfg -isXPubCached :: (MonadLoggerIO m) => XPubSpec -> CacheX m Bool-isXPubCached xpub =-  runRedis (redisIsXPubCached xpub) >>= \c -> do-    if c-      then incrementCounter (.cacheHits) 1-      else incrementCounter (.cacheMisses) 1-    return c+isXPubCached :: (MonadLoggerIO m) => Bool -> XPubSpec -> CacheX m Bool+isXPubCached silent xpub = do+  cached <- runRedis (redisIsXPubCached xpub)+  unless silent $ withMetrics $ \s ->+    if cached+      then incrementCounter s.hits 1+      else incrementCounter s.misses 1+  return cached  redisIsXPubCached :: (RedisCtx m f) => XPubSpec -> m (f Bool) redisIsXPubCached xpub = Redis.exists (balancesPfx <> encode xpub)@@ -472,7 +463,7 @@   return bals  cacheGetXPubTxCount ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m) =>+  (MonadUnliftIO m, MonadLoggerIO m) =>   XPubSpec ->   CacheX m Word32 cacheGetXPubTxCount xpub = do@@ -666,8 +657,8 @@     return       [ (k, v)         | (k', v') <- xs,-          let Right k = decode k',-          let Right v = decode v'+          let k = fromRight undefined $ decode k',+          let v = fromRight undefined $ decode v'       ]  data CacheWriterMessage@@ -723,28 +714,28 @@   go >>= \case     Right Redis.Ok -> do       $(logDebugS) "Cache" "Acquired lock"-      incrementCounter (.lockAcquired) 1+      withMetrics $ \s -> incrementCounter s.lockAcquired 1       return True     Right Redis.Pong -> do       $(logErrorS)         "Cache"         "Unexpected pong when acquiring lock"-      incrementCounter (.lockFailed) 1+      withMetrics $ \s -> incrementCounter s.lockFailed 1       return False     Right (Redis.Status s) -> do       $(logErrorS) "Cache" $         "Unexpected status acquiring lock: " <> cs s-      incrementCounter (.lockFailed) 1+      withMetrics $ \m -> incrementCounter m.lockFailed 1       return False     Left (Redis.Bulk Nothing) -> do       $(logDebugS) "Cache" "Lock already taken"-      incrementCounter (.lockFailed) 1+      withMetrics $ \s -> incrementCounter s.lockFailed 1       return False     Left e -> do       $(logErrorS)         "Cache"         "Error when trying to acquire lock"-      incrementCounter (.lockFailed) 1+      withMetrics $ \s -> incrementCounter s.lockFailed 1       throwIO (RedisError e)   where     go = do@@ -786,37 +777,19 @@       threadDelay (150 * 1000 * 1000)       refreshLock -isFull ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>-  CacheX m Bool-isFull = do-  x <- asks (.maxKeys)-  s <- runRedis Redis.dbsize-  return $ s > x- pruneDB ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>-  CacheX m Integer+  (MonadLoggerIO m, StoreReadBase m) => CacheX m () pruneDB = do-  x <- asks (((`div` 10) . (* 8)) . (.maxKeys))-  -- Prune to 80% of max+  x <- asks (.maxKeys)   s <- runRedis Redis.dbsize-  if s > x then flush (s - x) else return 0-  where-    flush n =-      case n `div` 64 of-        0 -> return 0-        x -> do-          ks <--            fmap (map fst) . runRedis $-              getFromSortedSet maxKey Nothing 0 (fromIntegral x)-          $(logDebugS) "Cache" $-            "Pruning " <> cs (show (length ks)) <> " old xpubs"-          delXPubKeys ks+  $(logDebugS) "Cache" "Pruning old xpubs"+  when (s > x) $+    void . delXPubKeys . map fst+      =<< runRedis (getFromSortedSet maxKey Nothing 0 32)  touchKeys :: (MonadLoggerIO m) => [XPubSpec] -> CacheX m () touchKeys xpubs = do-  now <- systemSeconds <$> liftIO getSystemTime+  now <- round <$> liftIO getPOSIXTime :: (MonadIO m) => m Int   runRedis $ redisTouchKeys now xpubs  redisTouchKeys :: (Monad f, RedisCtx m f, Real a) => a -> [XPubSpec] -> m (f ())@@ -853,7 +826,7 @@ newXPubC xpub xbals =   should_index >>= \i -> when i $     bracket set_index unset_index $ \j -> when j $-      withMetrics (.indexTime) $ do+      withTiming (.indexTime) $ do         xpubtxt <- xpubText xpub         $(logDebugS) "Cache" $           "Caching "@@ -877,7 +850,7 @@             <> ": "             <> cs (show (length xtxs))             <> " txs"-        now <- systemSeconds <$> liftIO getSystemTime+        now <- round <$> liftIO getPOSIXTime :: (MonadIO m) => m Int         runRedis $ do           b <- redisTouchKeys now [xpub]           c <- redisAddXPubBalances xpub xbals@@ -893,7 +866,7 @@           then             inSync >>= \s ->               if s-                then not <$> isFull+                then pruneDB >> return True                 else return False           else return False     key = idxPfx <> encode xpub@@ -910,7 +883,7 @@       liftIO (Redis.runRedis conn red) <&> isRight  inSync ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m) =>+  (MonadUnliftIO m, StoreReadExtra m) =>   CacheX m Bool inSync =   lift getBestBlock >>= \case@@ -983,7 +956,7 @@           <> cs (show (length tds))           <> " transactions from block "           <> blockHashToHex bh-      cacheSetHead bh+      cacheSetHead bd     Nothing -> do       $(logErrorS) "Cache" $         "Could not get block: "@@ -1016,7 +989,7 @@       <> " outputs"   unspentmap <- getunspents ctx   gap <- lift getMaxGap-  now <- systemSeconds <$> liftIO getSystemTime+  now <- round <$> liftIO getPOSIXTime :: (MonadIO m) => m Int   let xpubs = allxpubsls addrmap   forM_ (zip [(1 :: Int) ..] xpubs) $ \(i, xpub) -> do     xpubtxt <- xpubText xpub@@ -1266,16 +1239,16 @@ cacheGetMempool :: (MonadLoggerIO m) => CacheX m [(UnixTime, TxHash)] cacheGetMempool = runRedis redisGetMempool -cacheIsInMempool :: (MonadLoggerIO m) => TxHash -> CacheX m Bool-cacheIsInMempool = runRedis . redisIsInMempool- cacheGetHead :: (MonadLoggerIO m) => CacheX m (Maybe BlockHash) cacheGetHead = runRedis redisGetHead -cacheSetHead :: (MonadLoggerIO m, StoreReadBase m) => BlockHash -> CacheX m ()-cacheSetHead bh = do+cacheSetHead :: (MonadLoggerIO m, StoreReadBase m) => BlockData -> CacheX m ()+cacheSetHead bd = do   $(logDebugS) "Cache" $ "Cache head set to: " <> blockHashToHex bh   void $ runRedis (redisSetHead bh)+  withMetrics $ \s -> setGauge s.height (fromIntegral bd.height)+  where+    bh = headerHash bd.header  cacheGetAddrsInfo ::   (MonadLoggerIO m) => [Address] -> CacheX m [Maybe AddressXPub]@@ -1289,10 +1262,6 @@       (\btx -> (blockRefScore btx.block, encode btx.txid))       btxs -redisIsInMempool :: (Applicative f, RedisCtx m f) => TxHash -> m (f Bool)-redisIsInMempool txid =-  fmap isJust <$> Redis.zrank mempoolSetKey (encode txid)- redisRemFromMempool ::   (Applicative f, RedisCtx m f) => [TxHash] -> m (f Integer) redisRemFromMempool [] = return (pure 0)@@ -1303,7 +1272,7 @@ redisSetAddrInfo a i = void <$> Redis.set (addrPfx <> encode a) (encode i)  cacheDelXPubs ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>+  (MonadLoggerIO m, StoreReadBase m) =>   [XPubSpec] ->   CacheT m Integer cacheDelXPubs xpubs = ReaderT $ \case@@ -1311,7 +1280,7 @@   Nothing -> return 0  delXPubKeys ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>+  (MonadLoggerIO m, StoreReadBase m) =>   [XPubSpec] ->   CacheX m Integer delXPubKeys [] = return 0@@ -1414,10 +1383,10 @@   | null fbals =       []   | not haschange =-      zipWith f (addrs ctx) list-        <> zipWith f (changeaddrs ctx) changelist+      zipWith f addrs list+        <> zipWith f changeaddrs changelist   | otherwise =-      zipWith f (addrs ctx) list+      zipWith f addrs list   where     haschange = any ((== 1) . head . (.path)) xbals     f a p = (a, AddressXPub {spec = xpub, path = p})@@ -1431,12 +1400,12 @@         then [maxidx + 1 .. aidx + gap]         else []     paths = map (Deriv :/ dchain :/) ixs-    keys ctx = map (\p -> derivePubPath ctx p xpub.key)+    keys = map (\p -> derivePubPath ctx p xpub.key)     list = map pathToList paths-    xpubf ctx = xPubAddrFunction ctx xpub.deriv-    addrs ctx = map (xpubf ctx) (keys ctx paths)+    xpubf = xPubAddrFunction ctx xpub.deriv+    addrs = map xpubf (keys paths)     changepaths = map (Deriv :/ 1 :/) [0 .. gap - 1]-    changeaddrs ctx = map (xpubf ctx) (keys ctx changepaths)+    changeaddrs = map xpubf (keys changepaths)     changelist = map pathToList changepaths  sortTxData :: [TxData] -> [TxData]@@ -1484,9 +1453,7 @@     f t s = ((scoreBlockRef s).timestamp, t)  xpubText ::-  ( MonadUnliftIO m,-    MonadLoggerIO m,-    StoreReadBase m+  ( StoreReadBase m   ) =>   XPubSpec ->   CacheX m Text
src/Haskoin/Store/Common.hs view
@@ -19,7 +19,6 @@     StoreWrite (..),     StoreEvent (..),     PubExcept (..),-    DataMetrics (..),     getActiveBlock,     getActiveTxData,     getDefaultBalance,@@ -41,7 +40,6 @@     microseconds,     streamThings,     joinDescStreams,-    createDataMetrics,   ) where @@ -57,7 +55,6 @@   ) import Control.DeepSeq (NFData) import Control.Exception (Exception)-import Control.Monad (forM) import Control.Monad.Trans (lift) import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT) import Control.Monad.Trans.Reader (runReaderT)@@ -65,12 +62,8 @@ import Data.Default (Default (..)) import Data.HashSet qualified as H import Data.Hashable (Hashable)-import Data.IntMap.Strict (IntMap)-import Data.IntMap.Strict qualified as I-import Data.List (sortOn) import Data.Map.Strict qualified as Map-import Data.Maybe (catMaybes, mapMaybe)-import Data.Ord (Down (..))+import Data.Maybe (mapMaybe) import Data.Serialize (Serialize (..)) import Data.Time.Clock.System   ( getSystemTime,@@ -99,9 +92,6 @@     toTransaction,     zeroBalance,   )-import System.Metrics qualified as Metrics-import System.Metrics.Counter (Counter)-import System.Metrics.Counter qualified as Counter import UnliftIO (MonadIO, liftIO)  type DeriveAddr = XPubKey -> KeyIndex -> Address@@ -111,8 +101,8 @@ type Limit = Word32  data Start-  = AtTx {atTxHash :: !TxHash}-  | AtBlock {atBlockHeight :: !BlockHeight}+  = AtTx !TxHash+  | AtBlock !BlockHeight   deriving (Eq, Show)  data Limits = Limits@@ -214,13 +204,13 @@     rx = sum [b.balance.received | b <- xt]  getTransaction ::-  (Monad m, StoreReadBase m) => TxHash -> m (Maybe Transaction)+  ( StoreReadBase m) =>TxHash -> m (Maybe Transaction) getTransaction h = do   ctx <- getCtx   fmap (toTransaction ctx) <$> getTxData h  getNumTransaction ::-  (Monad m, StoreReadExtra m) => Word64 -> m [Transaction]+  ( StoreReadExtra m) =>Word64 -> m [Transaction] getNumTransaction i =   getCtx >>= \ctx ->     map (toTransaction ctx) <$> getNumTxData i@@ -341,7 +331,7 @@             then go ((i, tx) : orphans) ths xs             else (i, tx) : go orphans (txHash tx `H.delete` ths) xs -nub' :: (Eq a, Hashable a) => [a] -> [a]+nub' :: (Hashable a) => [a] -> [a] nub' = H.toList . H.fromList  microseconds :: (MonadIO m) => m Integer@@ -398,32 +388,3 @@         let mp2 = Map.deleteMax mp             mp' = Map.unionWith (++) mp1 mp2         go (Just x) mp'--data DataMetrics = DataMetrics-  { dataBestCount :: !Counter,-    dataBlockCount :: !Counter,-    dataTxCount :: !Counter,-    dataMempoolCount :: !Counter,-    dataBalanceCount :: !Counter,-    dataUnspentCount :: !Counter,-    dataAddrTxCount :: !Counter,-    dataXPubBals :: !Counter,-    dataXPubUnspents :: !Counter,-    dataXPubTxs :: !Counter,-    dataXPubTxCount :: !Counter-  }--createDataMetrics :: (MonadIO m) => Metrics.Store -> m DataMetrics-createDataMetrics s = liftIO $ do-  dataBestCount <- Metrics.createCounter "data.best_block" s-  dataBlockCount <- Metrics.createCounter "data.blocks" s-  dataTxCount <- Metrics.createCounter "data.txs" s-  dataMempoolCount <- Metrics.createCounter "data.mempool" s-  dataBalanceCount <- Metrics.createCounter "data.balances" s-  dataUnspentCount <- Metrics.createCounter "data.unspents" s-  dataAddrTxCount <- Metrics.createCounter "data.address_txs" s-  dataXPubBals <- Metrics.createCounter "data.xpub_balances" s-  dataXPubUnspents <- Metrics.createCounter "data.xpub_unspents" s-  dataXPubTxs <- Metrics.createCounter "data.xpub_txs" s-  dataXPubTxCount <- Metrics.createCounter "data.xpub_tx_count" s-  return DataMetrics {..}
src/Haskoin/Store/Database/Reader.hs view
@@ -5,12 +5,16 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE NoFieldSelectors #-}  module Haskoin.Store.Database.Reader   ( -- * RocksDB Database Access     DatabaseReader (..),     DatabaseReaderT,+    DataMetrics,+    withDB,+    createDataMetrics,     withDatabaseReader,     addrTxCF,     addrOutCF,@@ -24,28 +28,25 @@  import Conduit   ( ConduitT,-    dropC,     dropWhileC,     lift,     mapC,     runConduit,     sinkList,-    takeC,     (.|),   ) import Control.Monad.Except (runExceptT, throwError)-import Control.Monad.Reader (ReaderT, ask, asks, runReaderT)+import Control.Monad.Reader (MonadReader, ReaderT, asks, runReaderT) import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT) import Data.Bits ((.&.)) import Data.ByteString qualified as BS import Data.Default (def)-import Data.Function (on) import Data.IntMap.Strict qualified as IntMap import Data.List (sortOn) import Data.Maybe (fromMaybe) import Data.Ord (Down (..)) import Data.Serialize (encode)-import Data.Word (Word32, Word64)+import Data.Word (Word32) import Database.RocksDB   ( ColumnFamily,     Config (..),@@ -63,23 +64,11 @@     retrieveCF,   ) import Haskoin-  ( Address,-    BlockHash,-    BlockHeight,-    Ctx,-    Network,-    OutPoint (..),-    TxHash,-    pubSubKey,-    txHash,-  ) import Haskoin.Store.Common import Haskoin.Store.Data import Haskoin.Store.Database.Types-import System.Metrics qualified as Metrics-import System.Metrics.Counter (Counter)-import System.Metrics.Counter qualified as Counter-import UnliftIO (MonadIO, MonadUnliftIO, liftIO)+import System.Metrics.StatsD+import UnliftIO (MonadIO, MonadUnliftIO, bracket_, liftIO)  type DatabaseReaderT = ReaderT DatabaseReader @@ -92,19 +81,48 @@     ctx :: !Ctx   } -incrementCounter ::-  (MonadIO m) =>-  (DataMetrics -> Counter) ->-  Int ->-  ReaderT DatabaseReader m ()-incrementCounter f i =-  asks (.metrics) >>= \case-    Just s -> liftIO $ Counter.add (f s) (fromIntegral i)-    Nothing -> return ()+data DataMetrics = DataMetrics+  { dataBestCount :: !StatCounter,+    dataBlockCount :: !StatCounter,+    dataTxCount :: !StatCounter,+    dataMempoolCount :: !StatCounter,+    dataBalanceCount :: !StatCounter,+    dataUnspentCount :: !StatCounter,+    dataAddrTxCount :: !StatCounter,+    dataXPubBals :: !StatCounter,+    dataXPubUnspents :: !StatCounter,+    dataXPubTxs :: !StatCounter,+    dataXPubTxCount :: !StatCounter,+    dataIters :: !StatGauge+  } +createDataMetrics :: (MonadIO m) => Stats -> m DataMetrics+createDataMetrics s = do+  dataBestCount <- newStatCounter s "db.best_block" n+  dataBlockCount <- newStatCounter s "db.blocks" n+  dataTxCount <- newStatCounter s "db.txs" n+  dataMempoolCount <- newStatCounter s "db.mempool" n+  dataBalanceCount <- newStatCounter s "db.balances" n+  dataUnspentCount <- newStatCounter s "db.unspents" n+  dataAddrTxCount <- newStatCounter s "db.address_txs" n+  dataXPubBals <- newStatCounter s "db.xpub_balances" n+  dataXPubUnspents <- newStatCounter s "db.xpub_unspents" n+  dataXPubTxs <- newStatCounter s "db.xpub_txs" n+  dataXPubTxCount <- newStatCounter s "db.xpub_tx_count" n+  dataIters <- newStatGauge s "db.iterators" 0+  return DataMetrics {..}+  where+    n = 10++withMetrics :: (MonadReader DatabaseReader m) => (DataMetrics -> m a) -> m ()+withMetrics go = asks (.metrics) >>= mapM_ go+ dataVersion :: Word32 dataVersion = 18 +withDB :: DatabaseReader -> DatabaseReaderT m a -> m a+withDB = flip runReaderT+ withDatabaseReader ::   (MonadUnliftIO m) =>   Network ->@@ -129,7 +147,7 @@     initRocksDB bdb     runReaderT f bdb   where-    cfg = def {createIfMissing = True, maxFiles = Just (-1)}+    cfg = def {createIfMissing = True}  columnFamilyConfig :: [(String, Config)] columnFamilyConfig =@@ -204,10 +222,9 @@           Just TxData {block = b@BlockRef {}} ->             matchingSkip it (AddrTxKeyA a) (AddrTxKeyB a b)           Just TxData {block = MemRef {}} ->-            let cond (AddrTxKey _a (TxRef MemRef {} th)) =-                  th /= txh-                cond (AddrTxKey _a (TxRef BlockRef {} _th)) =-                  False+            let cond (AddrTxKey _ (TxRef MemRef {} th)) = th /= txh+                cond (AddrTxKey _ (TxRef BlockRef {} _)) = False+                cond _ = undefined              in matching it (AddrTxKeyA a)                   .| (dropWhileC (cond . fst) >> mapC id)           Nothing -> return ()@@ -235,25 +252,35 @@           Just TxData {block = b@BlockRef {}} ->             matchingSkip it (AddrOutKeyA a) (AddrOutKeyB a b)           Just TxData {block = MemRef {}} ->-            let cond (AddrOutKey _a MemRef {} p) =-                  p.hash /= txh-                cond (AddrOutKey _a BlockRef {} _p) =-                  False+            let cond (AddrOutKey _ MemRef {} p) = p.hash /= txh+                cond (AddrOutKey _ BlockRef {} _) = False+                cond _ = undefined              in matching it (AddrOutKeyA a)                   .| (dropWhileC (cond . fst) >> mapC id)           Nothing -> return () +withIterCFStat ::+  (MonadUnliftIO m) =>+  DB ->+  ColumnFamily ->+  (Iterator -> DatabaseReaderT m a) ->+  DatabaseReaderT m a+withIterCFStat db cf = bracket_ open close . withIterCF db cf+  where+    open = asks (.metrics) >>= mapM_ (\m -> incrementGauge m.dataIters 1)+    close = asks (.metrics) >>= mapM_ (\m -> decrementGauge m.dataIters 1)+ withManyIters ::   (MonadUnliftIO m) =>   DB ->   ColumnFamily ->   Int ->-  ([Iterator] -> m a) ->-  m a+  ([Iterator] -> DatabaseReaderT m a) ->+  DatabaseReaderT m a withManyIters db cf i f = go [] i   where     go acc 0 = f acc-    go acc n = withIterCF db cf $ \it -> go (it : acc) (n - 1)+    go acc n = withIterCFStat db cf $ \it -> go (it : acc) (n - 1)  joinConduits ::   (Monad m, Ord o) =>@@ -272,7 +299,7 @@     retrieveCF db (txCF db) (TxKey th) >>= \case       Nothing -> return Nothing       Just t -> do-        incrementCounter (.dataTxCount) 1+        withMetrics $ \s -> incrementCounter s.dataTxCount 1         return (Just t)    getSpender op = runMaybeT $ do@@ -287,21 +314,21 @@     case fmap (valToUnspent ctx p) val of       Nothing -> return Nothing       Just u -> do-        incrementCounter (.dataUnspentCount) 1+        withMetrics $ \s -> incrementCounter s.dataUnspentCount 1         return (Just u)    getBalance a = do     db <- asks (.db)-    incrementCounter (.dataBalanceCount) 1+    withMetrics $ \s -> incrementCounter s.dataBalanceCount 1     fmap (valToBalance a) <$> retrieveCF db (balanceCF db) (BalKey a)    getMempool = do     db <- asks (.db)-    incrementCounter (.dataMempoolCount) 1+    withMetrics $ \s -> incrementCounter s.dataMempoolCount 1     fromMaybe [] <$> retrieve db MemKey    getBestBlock = do-    incrementCounter (.dataBestCount) 1+    withMetrics $ \s -> incrementCounter s.dataBestCount 1     asks (.db) >>= (`retrieve` BestKey)    getBlocksAtHeight h = do@@ -309,7 +336,7 @@     retrieveCF db (heightCF db) (HeightKey h) >>= \case       Nothing -> return []       Just ls -> do-        incrementCounter (.dataBlockCount) (length ls)+        withMetrics $ \s -> incrementCounter s.dataBlockCount (length ls)         return ls    getBlock h = do@@ -317,7 +344,7 @@     retrieveCF db (blockCF db) (BlockKey h) >>= \case       Nothing -> return Nothing       Just b -> do-        incrementCounter (.dataBlockCount) 1+        withMetrics $ \s -> incrementCounter s.dataBlockCount 1         return (Just b)  instance (MonadUnliftIO m) => StoreReadExtra (DatabaseReaderT m) where@@ -325,7 +352,7 @@     db <- asks (.db)     withManyIters db (addrTxCF db) (length addrs) $ \its -> do       txs <- joinConduits (cs its) limits-      incrementCounter (.dataAddrTxCount) (length txs)+      withMetrics $ \s -> incrementCounter s.dataAddrTxCount (length txs)       return txs     where       cs = zipWith c addrs@@ -336,7 +363,7 @@     ctx <- asks (.ctx)     withManyIters db (addrOutCF db) (length addrs) $ \its -> do       uns <- joinConduits (cs ctx its) limits-      incrementCounter (.dataUnspentCount) (length uns)+      withMetrics $ \s -> incrementCounter s.dataUnspentCount (length uns)       return uns     where       cs ctx = zipWith (c ctx) addrs@@ -345,22 +372,22 @@   getAddressUnspents a limits = do     db <- asks (.db)     ctx <- asks (.ctx)-    us <- withIterCF db (addrOutCF db) $ \it ->+    us <- withIterCFStat db (addrOutCF db) $ \it ->       runConduit $         unspentConduit ctx a limits.start it           .| applyLimitsC limits           .| sinkList-    incrementCounter (.dataUnspentCount) (length us)+    withMetrics $ \s -> incrementCounter s.dataUnspentCount (length us)     return us    getAddressTxs a limits = do     db <- asks (.db)-    txs <- withIterCF db (addrTxCF db) $ \it ->+    txs <- withIterCFStat db (addrTxCF db) $ \it ->       runConduit $         addressConduit a limits.start it           .| applyLimitsC limits           .| sinkList-    incrementCounter (.dataAddrTxCount) (length txs)+    withMetrics $ \s -> incrementCounter s.dataAddrTxCount (length txs)     return txs    getMaxGap = asks (.maxGap)@@ -377,7 +404,7 @@               w' = b .&. 0xf8            in w == w'         txs = filter f $ map snd ls-    incrementCounter (.dataTxCount) (length txs)+    withMetrics $ \s -> incrementCounter s.dataTxCount (length txs)     return txs    getBalances as = do@@ -391,34 +418,44 @@     igap <- getInitialGap     gap <- getMaxGap     ext1 <- derive_until_gap gap 0 (take (fromIntegral igap) (aderiv ctx 0 0))-    if all (nullBalance . (.balance)) ext1+    if all nullxb ext1       then do-        incrementCounter (.dataXPubBals) (length ext1)+        withMetrics $ \s -> incrementCounter s.dataXPubBals (length ext1)         return ext1       else do         ext2 <- derive_until_gap gap 0 (aderiv ctx 0 igap)         chg <- derive_until_gap gap 1 (aderiv ctx 1 0)         let bals = ext1 <> ext2 <> chg-        incrementCounter (.dataXPubBals) (length bals)+        withMetrics $ \s -> incrementCounter s.dataXPubBals (length bals)         return bals     where+      nullxb b = nullBalance b.balance       aderiv ctx m =         deriveAddresses           (deriveFunction ctx xpub.deriv)           (pubSubKey ctx xpub.key m)-      xbalance m b n = XPubBal {path = [m, n], balance = b}-      derive_until_gap _ _ [] = return []-      derive_until_gap gap m as = do-        let (as1, as2) = splitAt (fromIntegral gap) as-        bs <- getBalances (map snd as1)-        let xbs = zipWith (xbalance m) bs (map fst as1)-        if all nullBalance bs-          then return xbs-          else (xbs <>) <$> derive_until_gap gap m as2+      derive_until_gap gap m adrs =+        let xb b n = XPubBal {path = [m, n], balance = b}+            ig = fromIntegral gap+            test acc =+              let ns = takeWhile nullxb acc+               in length ns > ig+            go acc [] = return $ reverse acc+            go acc as+              | test acc =+                  let (ns, ys) = span nullxb acc+                      xs = take ig $ reverse ns+                   in return $ reverse ys <> xs+              | otherwise = do+                  let (as', as'') = splitAt ig as+                  bs <- getBalances (map snd as')+                  let xs = reverse $ zipWith xb bs (map fst as')+                  go (xs <> acc) as''+         in go [] adrs    xPubUnspents _xspec xbals limits = do     us <- concat <$> mapM h cs-    incrementCounter (.dataXPubUnspents) (length us)+    withMetrics $ \s -> incrementCounter s.dataXPubUnspents (length us)     return . applyLimits limits $ sortOn Down us     where       l = deOffset limits@@ -433,9 +470,9 @@             filter (not . nullBalance) $               map (.balance) xbals     txs <- getAddressesTxs as limits-    incrementCounter (.dataXPubTxs) (length txs)+    withMetrics $ \s -> incrementCounter s.dataXPubTxs (length txs)     return txs    xPubTxCount xspec xbals = do-    incrementCounter (.dataXPubTxCount) 1+    withMetrics $ \s -> incrementCounter s.dataXPubTxCount 1     fromIntegral . length <$> xPubTxs xspec xbals def
src/Haskoin/Store/Database/Types.hs view
@@ -33,27 +33,19 @@ import Control.DeepSeq (NFData) import Control.Monad (guard) import Data.Bits-  ( Bits,-    shift,-    shiftL,-    shiftR,+  ( shift,     (.&.),-    (.|.),   ) import Data.ByteString (ByteString) import Data.ByteString qualified as BS import Data.Default (Default (..))-import Data.Either (fromRight) import Data.Hashable (Hashable) import Data.Serialize   ( Serialize (..),-    decode,-    encode,     getBytes,     getWord16be,     getWord32be,     getWord8,-    putWord32be,     putWord64be,     putWord8,     runGet,@@ -76,7 +68,6 @@   ( Balance (..),     BlockData,     BlockRef,-    Spender,     TxData,     TxRef (..),     UnixTime,
src/Haskoin/Store/Database/Writer.hs view
@@ -10,11 +10,8 @@  module Haskoin.Store.Database.Writer (WriterT, runWriter) where -import Control.Monad (join) import Control.Monad.Reader (ReaderT (..)) import Control.Monad.Reader qualified as R-import Data.HashMap.Strict (HashMap)-import Data.HashMap.Strict qualified as M import Data.HashTable.IO qualified as H import Data.IntMap.Strict qualified as IntMap import Data.List (sortOn)@@ -46,15 +43,9 @@ import UnliftIO   ( IORef,     MonadIO,-    TVar,-    atomically,     liftIO,-    modifyIORef,-    modifyTVar,     newIORef,-    newTVarIO,     readIORef,-    readTVarIO,     writeIORef,   ) @@ -77,8 +68,6 @@   getBalance = getBalanceI   getMempool = getMempoolI -type NetRef = IORef (Maybe Network)- type BestRef = IORef (Maybe (Maybe BlockHash))  type BlockTable = H.BasicHashTable BlockHash (Maybe BlockData)@@ -219,7 +208,7 @@ txOps :: (MonadIO m) => DB -> TxTable -> m [BatchOp] txOps db t = map (uncurry f) <$> liftIO (H.toList t)   where-    f k (Just t) = insertOpCF (txCF db) (TxKey k) t+    f k (Just t') = insertOpCF (txCF db) (TxKey k) t'     f k Nothing = deleteOpCF (txCF db) (TxKey k)  balOps :: (MonadIO m) => DB -> BalanceTable -> m [BatchOp]@@ -231,8 +220,8 @@ addrTxOps :: (MonadIO m) => DB -> AddrTxTable -> m [BatchOp] addrTxOps db t = map (uncurry f) <$> liftIO (H.toList t)   where-    f (a, t) (Just ()) = insertOpCF (addrTxCF db) (AddrTxKey a t) ()-    f (a, t) Nothing = deleteOpCF (addrTxCF db) (AddrTxKey a t)+    f (a, t') (Just ()) = insertOpCF (addrTxCF db) (AddrTxKey a t') ()+    f (a, t') Nothing = deleteOpCF (addrTxCF db) (AddrTxKey a t')  addrOutOps :: (MonadIO m) => DB -> AddrOutTable -> m [BatchOp] addrOutOps db t = map (uncurry f) <$> liftIO (H.toList t)@@ -313,12 +302,6 @@   mempoolTable <- liftIO $ H.fromList (map swap mempool)   return Memory {..} -getNetworkH :: (Monad m) => Memory -> m Network-getNetworkH Memory {net} = return net--getCtxH :: (Monad m) => Memory -> m Ctx-getCtxH Memory {ctx} = return ctx- getBestBlockH :: Memory -> IO (Maybe (Maybe BlockHash)) getBestBlockH = readIORef . (.best) @@ -339,9 +322,6 @@  getBalanceH :: Address -> Memory -> IO (Maybe (Maybe Balance)) getBalanceH a s = H.lookup s.balanceTable a--getMempoolH :: Memory -> IO [(UnixTime, TxHash)]-getMempoolH Memory {..} = sortOn Down . map swap <$> H.toList mempoolTable  getUnspentH :: OutPoint -> Memory -> IO (Maybe (Maybe Unspent)) getUnspentH p Memory {..} = H.lookup unspentTable p
src/Haskoin/Store/Logic.hs view
@@ -30,7 +30,6 @@     void,     when,     zipWithM_,-    (<=<),   ) import Control.Monad.Except (MonadError, throwError) import Control.Monad.Logger@@ -40,7 +39,6 @@   ) import qualified Data.ByteString as B import Data.Either (rights)-import Data.HashSet (HashSet) import qualified Data.HashSet as HashSet import qualified Data.IntMap.Strict as I import Data.List (nub)@@ -135,7 +133,7 @@       freeOutputs True True tx       rbf <- isRBF (MemRef w) tx       checkNewTx tx-      importTx (MemRef w) w rbf tx+      _ <- importTx (MemRef w) w rbf tx       return True  bestBlockData :: (MonadImport m) => m BlockData@@ -265,8 +263,6 @@       <> blockHashToHex (headerHash n.header)   return (bd, tds)   where-    cb_out_val =-      sum $ map (.value) $ (head b.txs).outputs     ts_out_val =       sum $ map (sum . map (.value) . (.outputs)) $ tail $ b.txs     w =@@ -481,7 +477,7 @@   mapM_ (\t -> let h = txHash t in deleteSingleTx h >> return h) chain  getChain ::-  (MonadImport m, MonadLoggerIO m) =>+  (MonadImport m) =>   -- | only delete transaction if unconfirmed   Bool ->   -- | only delete RBF
src/Haskoin/Store/Manager.hs view
@@ -1,6 +1,5 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE NoFieldSelectors #-}@@ -12,12 +11,9 @@   ) where -import Control.Monad (forever, unless, when)+import Control.Monad (forever, unless) import Control.Monad.Cont   ( ContT (..),-    MonadCont (callCC),-    cont,-    runCont,     runContT,   ) import Control.Monad.Logger (MonadLoggerIO)@@ -78,11 +74,11 @@   ) import Haskoin.Store.Common   ( StoreEvent (..),-    createDataMetrics,   ) import Haskoin.Store.Database.Reader   ( DatabaseReader (..),     DatabaseReaderT,+    createDataMetrics,     withDatabaseReader,   ) import NQE@@ -96,7 +92,7 @@     withSubscription,   ) import Network.Socket (SockAddr (..))-import System.Metrics qualified as Metrics (Store)+import System.Metrics.StatsD import UnliftIO   ( MonadIO,     MonadUnliftIO,@@ -155,8 +151,8 @@     maxPeerLife :: !NominalDiffTime,     -- | connect to peers using the function 'withConnection'     connect :: !(SockAddr -> WithConnection),-    -- | stats store-    statsStore :: !(Maybe Metrics.Store),+    -- | StatsD+    stats :: !(Maybe Stats),     -- | sync mempool against cache every this many seconds     redisSyncInterval :: !Int   }@@ -196,7 +192,7 @@   DatabaseReaderT m a ->   m a connectDB cfg f = do-  stats <- mapM createDataMetrics cfg.statsStore+  stats <- mapM createDataMetrics cfg.stats   withDatabaseReader     cfg.net     cfg.ctx@@ -224,7 +220,7 @@       wipeMempool = cfg.wipeMempool,       syncMempool = cfg.syncMempool,       peerTimeout = cfg.peerTimeout,-      statsStore = cfg.statsStore+      stats = cfg.stats     }  nodeCfg ::@@ -263,8 +259,8 @@     Nothing ->       action Nothing     Just redisurl -> do-      metrics <- mapM newCacheMetrics cfg.statsStore       conn <- connectRedis redisurl+      metrics <- mapM (\s -> newCacheMetrics s conn db) cfg.stats       withSubscription pub $ \evts ->         let conf = c conn metrics          in withProcess (f conf) $ \p ->
− src/Haskoin/Store/Stats.hs
@@ -1,209 +0,0 @@-{-# LANGUAGE DuplicateRecordFields #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# LANGUAGE OverloadedRecordDot #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE NoFieldSelectors #-}--module Haskoin.Store.Stats-  ( StatDist,-    withStats,-    createStatDist,-    addStatTime,-    addClientError,-    addServerError,-    addStatQuery,-    addStatItems,-  )-where--import Control.Concurrent.STM.TQueue-  ( TQueue,-    flushTQueue,-    writeTQueue,-  )-import Control.Foldl qualified as L-import Control.Monad (forever)-import Data.Function (on)-import Data.HashMap.Strict (HashMap)-import Data.HashMap.Strict qualified as HashMap-import Data.Int (Int64)-import Data.List (sort, sortBy, sortOn)-import Data.Maybe (fromMaybe)-import Data.Ord (Down (..), comparing)-import Data.String.Conversions (cs)-import Data.Text (Text)-import System.Metrics-  ( Store,-    Value (..),-    newStore,-    registerGcMetrics,-    registerGroup,-    sampleAll,-  )-import System.Remote.Monitoring.Statsd-  ( defaultStatsdOptions,-    flushInterval,-    forkStatsd,-    host,-    port,-    prefix,-  )-import UnliftIO-  ( MonadIO,-    TVar,-    atomically,-    liftIO,-    modifyTVar,-    newTQueueIO,-    newTVarIO,-    readTVar,-    withAsync,-  )-import UnliftIO.Concurrent (threadDelay)--withStats :: (MonadIO m) => Text -> Int -> Text -> (Store -> m a) -> m a-withStats h p pfx go = do-  store <- liftIO newStore-  _statsd <--    liftIO $-      forkStatsd-        defaultStatsdOptions-          { prefix = pfx,-            host = h,-            port = p-          }-        store-  liftIO $ registerGcMetrics store-  go store--data StatData = StatData-  { times :: ![Int64],-    queries :: !Int64,-    items :: !Int64,-    clientErrors :: !Int64,-    serverErrors :: !Int64-  }--data StatDist = StatDist-  { queue :: !(TQueue Int64),-    queries :: !(TVar Int64),-    items :: !(TVar Int64),-    clientErrors :: !(TVar Int64),-    serverErrors :: !(TVar Int64)-  }--createStatDist :: (MonadIO m) => Text -> Store -> m StatDist-createStatDist t store = liftIO $ do-  queue <- newTQueueIO-  queries <- newTVarIO 0-  items <- newTVarIO 0-  clientErrors <- newTVarIO 0-  serverErrors <- newTVarIO 0-  let metrics =-        HashMap.fromList-          [ ( t <> ".request_count",-              Counter . (.queries)-            ),-            ( t <> ".item_count",-              Counter . (.items)-            ),-            ( t <> ".client_errors",-              Counter . (.clientErrors)-            ),-            ( t <> ".server_errors",-              Counter . (.serverErrors)-            ),-            ( t <> ".mean_ms",-              Gauge . mean . (.times)-            ),-            ( t <> ".avg_ms",-              Gauge . avg . (.times)-            ),-            ( t <> ".max_ms",-              Gauge . maxi . (.times)-            ),-            ( t <> ".min_ms",-              Gauge . mini . (.times)-            ),-            ( t <> ".p90max_ms",-              Gauge . p90max . (.times)-            ),-            ( t <> ".p90avg_ms",-              Gauge . p90avg . (.times)-            ),-            ( t <> ".var_ms",-              Gauge . var . (.times)-            )-          ]-  let sd = StatDist {..}-  registerGroup metrics (flush sd) store-  return sd--toDouble :: Int64 -> Double-toDouble = fromIntegral--addStatTime :: (MonadIO m) => StatDist -> Int64 -> m ()-addStatTime q =-  liftIO . atomically . writeTQueue q.queue--addStatQuery :: (MonadIO m) => StatDist -> m ()-addStatQuery q =-  liftIO . atomically $ modifyTVar q.queries (+ 1)--addStatItems :: (MonadIO m) => StatDist -> Int64 -> m ()-addStatItems q =-  liftIO . atomically . modifyTVar q.items . (+)--addClientError :: (MonadIO m) => StatDist -> m ()-addClientError q =-  liftIO . atomically $ modifyTVar q.clientErrors (+ 1)--addServerError :: (MonadIO m) => StatDist -> m ()-addServerError q =-  liftIO . atomically $ modifyTVar q.serverErrors (+ 1)--flush :: (MonadIO m) => StatDist -> m StatData-flush StatDist {..} = atomically $ do-  times <- flushTQueue queue-  queries <- readTVar queries-  items <- readTVar items-  clientErrors <- readTVar clientErrors-  serverErrors <- readTVar serverErrors-  return $ StatData {..}--average :: (Fractional a) => L.Fold a a-average = (/) <$> L.sum <*> L.genericLength--avg :: [Int64] -> Int64-avg = round . L.fold average . map toDouble--mean :: [Int64] -> Int64-mean = round . L.fold L.mean . map toDouble--maxi :: [Int64] -> Int64-maxi = fromMaybe 0 . L.fold L.maximum--mini :: [Int64] -> Int64-mini = fromMaybe 0 . L.fold L.minimum--var :: [Int64] -> Int64-var = round . L.fold L.variance . map toDouble--p90max :: [Int64] -> Int64-p90max ls =-  case chopped of-    [] -> 0-    h : _ -> h-  where-    sorted = sortOn Down ls-    len = length sorted-    chopped = drop (length sorted `div` 10) sorted--p90avg :: [Int64] -> Int64-p90avg ls =-  avg chopped-  where-    sorted = sortOn Down ls-    len = length sorted-    chopped = drop (length sorted `div` 10) sorted
src/Haskoin/Store/Web.hs view
@@ -20,3165 +20,2766 @@     WebConfig (..),     Except (..),     WebLimits (..),-    WebTimeouts (..),-    runWeb,-  )-where--import Conduit-  ( ConduitT,-    await,-    concatMapC,-    concatMapMC,-    dropC,-    dropWhileC,-    headC,-    iterMC,-    mapC,-    runConduit,-    sinkList,-    takeC,-    takeWhileC,-    yield,-    (.|),-  )-import Control.Applicative ((<|>))-import Control.Arrow (second)-import Control.Lens ((.~), (^.))-import Control.Monad-  ( forM_,-    forever,-    join,-    unless,-    when,-    (<=<),-    (>=>),-  )-import Control.Monad.Logger-  ( MonadLoggerIO,-    logDebugS,-    logErrorS,-    logWarnS,-  )-import Control.Monad.Reader-  ( ReaderT,-    asks,-    local,-    runReaderT,-  )-import Control.Monad.Trans (lift)-import Control.Monad.Trans.Control (liftWith, restoreT)-import Control.Monad.Trans.Maybe-  ( MaybeT (..),-    runMaybeT,-  )-import Data.Aeson-  ( Encoding,-    ToJSON (..),-    Value,-  )-import Data.Aeson qualified as A-import Data.Aeson.Encode.Pretty-  ( Config (..),-    defConfig,-    encodePretty',-  )-import Data.Aeson.Encoding-  ( encodingToLazyByteString,-    list,-  )-import Data.Aeson.Text (encodeToLazyText)-import Data.Base16.Types (assertBase16)-import Data.ByteString (ByteString)-import Data.ByteString qualified as B-import Data.ByteString.Base16 (decodeBase16, isBase16)-import Data.ByteString.Builder (byteString, lazyByteString)-import Data.ByteString.Char8 qualified as C-import Data.ByteString.Lazy qualified as L-import Data.Bytes.Get-import Data.Bytes.Put-import Data.Bytes.Serial-import Data.Char (isSpace)-import Data.Default (Default (..))-import Data.Function ((&))-import Data.HashMap.Strict (HashMap)-import Data.HashMap.Strict qualified as HashMap-import Data.HashSet (HashSet)-import Data.HashSet qualified as HashSet-import Data.Int (Int64)-import Data.List (nub)-import Data.Maybe-  ( catMaybes,-    fromJust,-    fromMaybe,-    isJust,-    mapMaybe,-    maybeToList,-  )-import Data.Proxy (Proxy (..))-import Data.Serialize (decode)-import Data.String (fromString)-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 (toStrict)-import Data.Text.Lazy qualified as TL-import Data.Time.Clock (diffUTCTime, getCurrentTime)-import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)-import Data.Time.Clock.System-  ( getSystemTime,-    systemSeconds,-    systemToUTCTime,-  )-import Data.Vault.Lazy qualified as V-import Data.Word (Word32, Word64)-import Database.RocksDB-  ( Property (..),-    getProperty,-  )-import GHC.RTS.Flags (ConcFlags (ConcFlags))-import Haskoin.Address-import Haskoin.Block qualified as H-import Haskoin.Crypto.Hash (Hash160 (..))-import Haskoin.Crypto.Keys-import Haskoin.Network-import Haskoin.Node-  ( Chain,-    OnlinePeer (..),-    PeerMgr,-    chainGetAncestor,-    chainGetBest,-    getPeers,-    sendMessage,-  )-import Haskoin.Script-import Haskoin.Store.BlockStore-import Haskoin.Store.Cache-import Haskoin.Store.Common-import Haskoin.Store.Data-import Haskoin.Store.Database.Reader-import Haskoin.Store.Manager-import Haskoin.Store.Stats-import Haskoin.Store.WebCommon-import Haskoin.Transaction-import Haskoin.Util-import NQE-  ( Inbox,-    Publisher,-    receive,-    withSubscription,-  )-import Network.HTTP.Types-  ( Status (..),-    hContentType,-    requestEntityTooLarge413,-    status400,-    status404,-    status409,-    status413,-    status500,-    status503,-    statusIsClientError,-    statusIsServerError,-    statusIsSuccessful,-  )-import Network.Wai-  ( Middleware,-    Request (..),-    Response,-    getRequestBodyChunk,-    responseLBS,-    responseStatus,-  )-import Network.Wai qualified as S-import Network.Wai.Handler.Warp-  ( defaultSettings,-    setHost,-    setPort,-  )-import Network.Wai.Handler.WebSockets (websocketsOr)-import Network.Wai.Middleware.RequestSizeLimit-import Network.Wai.Middleware.Timeout-import Network.WebSockets-  ( ServerApp,-    acceptRequest,-    defaultConnectionOptions,-    pendingRequest,-    rejectRequestWith,-    requestPath,-    sendTextData,-  )-import Network.WebSockets qualified as WebSockets-import Network.Wreq qualified as Wreq-import Network.Wreq.Session as Wreq (Session)-import Network.Wreq.Session qualified as Wreq.Session-import System.IO.Unsafe (unsafeInterleaveIO)-import System.Metrics qualified as Metrics-import System.Metrics.Gauge qualified as Metrics (Gauge)-import System.Metrics.Gauge qualified as Metrics.Gauge-import UnliftIO-  ( MonadIO,-    MonadUnliftIO,-    TVar,-    askRunInIO,-    atomically,-    bracket,-    bracket_,-    handleAny,-    liftIO,-    modifyTVar,-    newTVarIO,-    readTVarIO,-    timeout,-    withAsync,-    withRunInIO,-    writeTVar,-  )-import UnliftIO.Concurrent (threadDelay)-import Web.Scotty.Internal.Types (ActionT)-import Web.Scotty.Trans qualified as S--type WebT m = ActionT Except (ReaderT WebState m)--data WebLimits = WebLimits-  { maxItemCount :: !Word32,-    maxFullItemCount :: !Word32,-    maxOffset :: !Word32,-    defItemCount :: !Word32,-    xpubGap :: !Word32,-    xpubGapInit :: !Word32,-    maxBodySize :: !Word32,-    requestTimeout :: !Word32-  }-  deriving (Eq, Show)--instance Default WebLimits where-  def =-    WebLimits-      { maxItemCount = 200000,-        maxFullItemCount = 5000,-        maxOffset = 50000,-        defItemCount = 100,-        xpubGap = 32,-        xpubGapInit = 20,-        maxBodySize = 1024 * 1024,-        requestTimeout = 0-      }--data WebConfig = WebConfig-  { host :: !String,-    port :: !Int,-    store :: !Store,-    maxLaggingBlocks :: !Int,-    maxPendingTxs :: !Int,-    minPeers :: !Int,-    limits :: !WebLimits,-    timeouts :: !WebTimeouts,-    version :: !String,-    noMempool :: !Bool,-    statsStore :: !(Maybe Metrics.Store),-    tickerRefresh :: !Int,-    tickerURL :: !String,-    priceHistoryURL :: !String,-    noSlow :: !Bool,-    noBlockchainInfo :: !Bool,-    healthCheckInterval :: !Int-  }--data WebState = WebState-  { config :: !WebConfig,-    ticker :: !(TVar (HashMap Text BinfoTicker)),-    metrics :: !(Maybe WebMetrics),-    session :: !Wreq.Session,-    health :: !(TVar HealthCheck)-  }--data WebMetrics = WebMetrics-  { all :: !StatDist,-    -- Addresses-    addressTx :: !StatDist,-    addressTxFull :: !StatDist,-    addressBalance :: !StatDist,-    addressUnspent :: !StatDist,-    xpub :: !StatDist,-    xpubDelete :: !StatDist,-    xpubTxFull :: !StatDist,-    xpubTx :: !StatDist,-    xpubBalance :: !StatDist,-    xpubUnspent :: !StatDist,-    -- Transactions-    tx :: !StatDist,-    txRaw :: !StatDist,-    txAfter :: !StatDist,-    txBlock :: !StatDist,-    txBlockRaw :: !StatDist,-    txPost :: !StatDist,-    mempool :: !StatDist,-    -- Blocks-    block :: !StatDist,-    blockRaw :: !StatDist,-    -- Blockchain-    binfoMultiaddr :: !StatDist,-    binfoBalance :: !StatDist,-    binfoAddressRaw :: !StatDist,-    binfoUnspent :: !StatDist,-    binfoTxRaw :: !StatDist,-    binfoBlock :: !StatDist,-    binfoBlockHeight :: !StatDist,-    binfoBlockLatest :: !StatDist,-    binfoBlockRaw :: !StatDist,-    binfoMempool :: !StatDist,-    binfoExportHistory :: !StatDist,-    -- Blockchain /q endpoints-    binfoQaddresstohash :: !StatDist,-    binfoQhashtoaddress :: !StatDist,-    binfoQaddrpubkey :: !StatDist,-    binfoQpubkeyaddr :: !StatDist,-    binfoQhashpubkey :: !StatDist,-    binfoQgetblockcount :: !StatDist,-    binfoQlatesthash :: !StatDist,-    binfoQbcperblock :: !StatDist,-    binfoQtxtotalbtcoutput :: !StatDist,-    binfoQtxtotalbtcinput :: !StatDist,-    binfoQtxfee :: !StatDist,-    binfoQtxresult :: !StatDist,-    binfoQgetreceivedbyaddress :: !StatDist,-    binfoQgetsentbyaddress :: !StatDist,-    binfoQaddressbalance :: !StatDist,-    binfoQaddressfirstseen :: !StatDist,-    -- Others-    health :: !StatDist,-    peers :: !StatDist,-    db :: !StatDist,-    events :: !Metrics.Gauge.Gauge,-    -- Request-    key :: !(V.Key (TVar (Maybe (WebMetrics -> StatDist))))-  }--createMetrics :: (MonadIO m) => Metrics.Store -> m WebMetrics-createMetrics s = liftIO $ do-  all <- d "all"--  -- Addresses-  addressTx <- d "address_transactions"-  addressTxFull <- d "address_transactions_full"-  addressBalance <- d "address_balance"-  addressUnspent <- d "address_unspent"-  xpub <- d "xpub"-  xpubDelete <- d "xpub_delete"-  xpubTxFull <- d "xpub_transactions_full"-  xpubTx <- d "xpub_transactions"-  xpubBalance <- d "xpub_balances"-  xpubUnspent <- d "xpub_unspent"--  -- Transactions-  tx <- d "transaction"-  txRaw <- d "transaction_raw"-  txAfter <- d "transaction_after"-  txPost <- d "transaction_post"-  txBlock <- d "transactions_block"-  txBlockRaw <- d "transactions_block_raw"-  mempool <- d "mempool"--  -- Blocks-  block <- d "block"-  blockRaw <- d "block_raw"--  -- Blockchain-  binfoMultiaddr <- d "blockchain_multiaddr"-  binfoBalance <- d "blockchain_balance"-  binfoAddressRaw <- d "blockchain_rawaddr"-  binfoUnspent <- d "blockchain_unspent"-  binfoTxRaw <- d "blockchain_rawtx"-  binfoBlock <- d "blockchain_blocks"-  binfoBlockHeight <- d "blockchain_block_height"-  binfoBlockLatest <- d "blockchain_latestblock"-  binfoBlockRaw <- d "blockchain_rawblock"-  binfoMempool <- d "blockchain_mempool"-  binfoExportHistory <- d "blockchain_export_history"--  -- Blockchain /q endpoints-  binfoQaddresstohash <- d "blockchain_q_addresstohash"-  binfoQhashtoaddress <- d "blockchain_q_hashtoaddress"-  binfoQaddrpubkey <- d "blockckhain_q_addrpubkey"-  binfoQpubkeyaddr <- d "blockchain_q_pubkeyaddr"-  binfoQhashpubkey <- d "blockchain_q_hashpubkey"-  binfoQgetblockcount <- d "blockchain_q_getblockcount"-  binfoQlatesthash <- d "blockchain_q_latesthash"-  binfoQbcperblock <- d "blockchain_q_bcperblock"-  binfoQtxtotalbtcoutput <- d "blockchain_q_txtotalbtcoutput"-  binfoQtxtotalbtcinput <- d "blockchain_q_txtotalbtcinput"-  binfoQtxfee <- d "blockchain_q_txfee"-  binfoQtxresult <- d "blockchain_q_txresult"-  binfoQgetreceivedbyaddress <- d "blockchain_q_getreceivedbyaddress"-  binfoQgetsentbyaddress <- d "blockchain_q_getsentbyaddress"-  binfoQaddressbalance <- d "blockchain_q_addressbalance"-  binfoQaddressfirstseen <- d "blockchain_q_addressfirstseen"--  -- Others-  health <- d "health"-  peers <- d "peers"-  db <- d "dbstats"--  events <- g "events_connected"-  key <- V.newKey-  return WebMetrics {..}-  where-    d x = createStatDist ("web." <> x) s-    g x = Metrics.createGauge ("web." <> x) s--withGaugeIO :: (MonadUnliftIO m) => Metrics.Gauge -> m a -> m a-withGaugeIO g =-  bracket_-    (liftIO $ Metrics.Gauge.inc g)-    (liftIO $ Metrics.Gauge.dec g)--withGaugeIncrease ::-  (MonadUnliftIO m) =>-  (WebMetrics -> Metrics.Gauge) ->-  WebT m a ->-  WebT m a-withGaugeIncrease gf go =-  lift (asks (.metrics)) >>= \case-    Nothing -> go-    Just m -> do-      s <- liftWith $ \run -> withGaugeIO (gf m) (run go)-      restoreT $ return s--setMetrics :: (MonadUnliftIO m) => (WebMetrics -> StatDist) -> WebT m ()-setMetrics df =-  asks (.metrics) >>= mapM_ go-  where-    go m = do-      req <- S.request-      let t = fromMaybe e $ V.lookup m.key (vault req)-      atomically $ writeTVar t (Just df)-    e = error "the ways of the warrior are yet to be mastered"--addItemCount :: (MonadUnliftIO m) => Int -> WebT m ()-addItemCount i =-  asks (.metrics) >>= mapM_ \m -> do-    addStatItems m.all (fromIntegral i)-    req <- S.request-    forM_ (V.lookup m.key (vault req)) $-      readTVarIO >=> mapM_ \s -> addStatItems (s m) (fromIntegral i)--getItemCounter :: (MonadIO m, MonadIO n) => WebT m (Int -> n ())-getItemCounter =-  fromMaybe (\_ -> return ()) <$> runMaybeT do-    q <- lift S.request-    m <- MaybeT $ asks (.metrics)-    t <- MaybeT . return $ V.lookup m.key (vault q)-    s <- MaybeT $ readTVarIO t-    return $ addStatItems (s m) . fromIntegral--data WebTimeouts = WebTimeouts-  { tx :: !Word64,-    block :: !Word64-  }-  deriving (Eq, Show)--data SerialAs = SerialAsBinary | SerialAsJSON | SerialAsPrettyJSON-  deriving (Eq, Show)--instance Default WebTimeouts where-  def = WebTimeouts {tx = 3600 `div` 2, block = 4 * 3600}--instance-  (MonadUnliftIO m, MonadLoggerIO m) =>-  StoreReadBase (ReaderT WebState m)-  where-  getCtx = runInWebReader getCtx-  getNetwork = runInWebReader getNetwork-  getBestBlock = runInWebReader getBestBlock-  getBlocksAtHeight height = runInWebReader (getBlocksAtHeight height)-  getBlock bh = runInWebReader (getBlock bh)-  getTxData th = runInWebReader (getTxData th)-  getSpender op = runInWebReader (getSpender op)-  getUnspent op = runInWebReader (getUnspent op)-  getBalance a = runInWebReader (getBalance a)-  getMempool = runInWebReader getMempool--instance-  (MonadUnliftIO m, MonadLoggerIO m) =>-  StoreReadExtra (ReaderT WebState m)-  where-  getMaxGap = runInWebReader getMaxGap-  getInitialGap = runInWebReader getInitialGap-  getBalances as = runInWebReader (getBalances as)-  getAddressesTxs as = runInWebReader . getAddressesTxs as-  getAddressTxs a = runInWebReader . getAddressTxs a-  getAddressUnspents a = runInWebReader . getAddressUnspents a-  getAddressesUnspents as = runInWebReader . getAddressesUnspents as-  xPubBals = runInWebReader . xPubBals-  xPubUnspents xpub xbals = runInWebReader . xPubUnspents xpub xbals-  xPubTxs xpub xbals = runInWebReader . xPubTxs xpub xbals-  xPubTxCount xpub = runInWebReader . xPubTxCount xpub-  getNumTxData = runInWebReader . getNumTxData--instance (MonadUnliftIO m, MonadLoggerIO m) => StoreReadBase (WebT m) where-  getCtx = lift getCtx-  getNetwork = lift getNetwork-  getBestBlock = lift getBestBlock-  getBlocksAtHeight = lift . getBlocksAtHeight-  getBlock = lift . getBlock-  getTxData = lift . getTxData-  getSpender = lift . getSpender-  getUnspent = lift . getUnspent-  getBalance = lift . getBalance-  getMempool = lift getMempool--instance (MonadUnliftIO m, MonadLoggerIO m) => StoreReadExtra (WebT m) where-  getBalances = lift . getBalances-  getAddressesTxs as = lift . getAddressesTxs as-  getAddressTxs a = lift . getAddressTxs a-  getAddressUnspents a = lift . getAddressUnspents a-  getAddressesUnspents as = lift . getAddressesUnspents as-  xPubBals = lift . xPubBals-  xPubUnspents xpub xbals = lift . xPubUnspents xpub xbals-  xPubTxs xpub xbals = lift . xPubTxs xpub xbals-  xPubTxCount xpub = lift . xPubTxCount xpub-  getMaxGap = lift getMaxGap-  getInitialGap = lift getInitialGap-  getNumTxData = lift . getNumTxData------------------------ Path Handlers ------------------------runWeb :: (MonadUnliftIO m, MonadLoggerIO m) => WebConfig -> m ()-runWeb config = do-  ticker <- newTVarIO HashMap.empty-  metrics <- mapM createMetrics config.statsStore-  session <- liftIO Wreq.Session.newAPISession-  health <- runHealthCheck >>= newTVarIO-  let state = WebState {..}-  withAsync (priceUpdater session ticker) $ \_a1 ->-    withAsync (healthCheckLoop health) $ \_a2 -> do-      logger <- logIt metrics-      runner <- askRunInIO-      S.scottyOptsT opts (runner . flip runReaderT state) $ do-        S.middleware $ webSocketEvents state-        S.middleware logger-        S.middleware $ reqSizeLimit config.limits.maxBodySize-        timeoutMiddleware-        S.defaultHandler defHandler-        handlePaths config-        S.notFound $ raise ThingNotFound-  where-    priceUpdater session =-      unless (config.noSlow || config.noBlockchainInfo)-        . price-          config.store.net-          session-          config.tickerURL-          config.tickerRefresh-    timeoutMiddleware =-      when (config.limits.requestTimeout > 0) $-        S.middleware $-          reqTimeout config.limits.requestTimeout-    runHealthCheck = runReaderT (healthCheck config) config.store.db-    healthCheckLoop v = forever $ do-      threadDelay (config.healthCheckInterval * 1000 * 1000)-      runHealthCheck >>= atomically . writeTVar v-    opts = def {S.settings = settings defaultSettings}-    settings = setPort config.port . setHost (fromString config.host)--getRates ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  Network ->-  Wreq.Session ->-  String ->-  Text ->-  [Word64] ->-  m [BinfoRate]-getRates net session url currency times = do-  handleAny err $ do-    r <--      liftIO $-        Wreq.asJSON-          =<< Wreq.Session.postWith opts session url body-    return $ r ^. Wreq.responseBody-  where-    err _ = do-      $(logErrorS) "Web" "Could not get historic prices"-      return []-    body = toJSON times-    base =-      Wreq.defaults-        & Wreq.param "base" .~ [T.toUpper $ T.pack net.name]-    opts = base & Wreq.param "quote" .~ [currency]--price ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  Network ->-  Wreq.Session ->-  String ->-  Int ->-  TVar (HashMap Text BinfoTicker) ->-  m ()-price net session url pget v = forM_ purl $ \u -> forever $ do-  let err e = $(logErrorS) "Price" $ cs (show e)-  handleAny err $ do-    r <- liftIO $ Wreq.asJSON =<< Wreq.Session.get session u-    atomically . writeTVar v $ r ^. Wreq.responseBody-  threadDelay pget-  where-    purl = case code of-      Nothing -> Nothing-      Just x -> Just (url <> "?base=" <> x)-      where-        code-          | net == btc = Just "btc"-          | net == bch = Just "bch"-          | otherwise = Nothing--raise :: (MonadIO m) => Except -> WebT m a-raise err =-  lift (asks (.metrics)) >>= \case-    Nothing -> S.raise err-    Just m -> do-      req <- S.request-      mM <- case V.lookup m.key (vault req) of-        Nothing -> return Nothing-        Just t -> readTVarIO t-      let status = errStatus err-      if-          | statusIsClientError status ->-              liftIO $ do-                addClientError m.all-                forM_ mM $ \f -> addClientError (f m)-          | statusIsServerError status ->-              liftIO $ do-                addServerError m.all-                forM_ mM $ \f -> addServerError (f m)-          | otherwise ->-              return ()-      S.raise err--errStatus :: Except -> Status-errStatus ThingNotFound = status404-errStatus BadRequest = status400-errStatus UserError {} = status400-errStatus StringError {} = status400-errStatus ServerError = status500-errStatus TxIndexConflict {} = status409-errStatus ServerTimeout = status500-errStatus RequestTooLarge = status413--defHandler :: (Monad m) => Except -> WebT m ()-defHandler e = do-  setHeaders-  S.status $ errStatus e-  S.json e--handlePaths ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  WebConfig ->-  S.ScottyT Except (ReaderT WebState m) ()-handlePaths WebConfig {..} = do-  -- Block Paths-  pathCompact-    (GetBlock <$> paramLazy <*> paramDef)-    scottyBlock-    (marshalEncoding net)-    (marshalValue net)-  pathCompact-    (GetBlockBest <$> paramDef)-    scottyBlockBest-    (marshalEncoding net)-    (marshalValue net)-  pathCompact-    (GetBlockHeight <$> paramLazy <*> paramDef)-    (fmap SerialList . scottyBlockHeight)-    (list (marshalEncoding net) . (.get))-    (json_list (marshalValue net) . (.get))-  pathCompact-    (GetBlockTime <$> paramLazy <*> paramDef)-    scottyBlockTime-    (marshalEncoding net)-    (marshalValue net)-  pathCompact-    (GetBlockMTP <$> paramLazy <*> paramDef)-    scottyBlockMTP-    (marshalEncoding net)-    (marshalValue net)-  pathCompact-    (GetTx <$> paramLazy)-    scottyTx-    (marshalEncoding net)-    (marshalValue net)-  pathCompact-    (GetTxRaw <$> paramLazy)-    scottyTxRaw-    toEncoding-    toJSON-  pathCompact-    (PostTx <$> parseBody)-    scottyPostTx-    toEncoding-    toJSON-  pathCompact-    (GetMempool <$> paramOptional <*> parseOffset)-    (fmap SerialList . scottyMempool)-    toEncoding-    toJSON-  pathCompact-    (GetAddrTxs <$> paramLazy <*> parseLimits)-    (fmap SerialList . scottyAddrTxs)-    toEncoding-    toJSON-  pathCompact-    (GetAddrBalance <$> paramLazy)-    scottyAddrBalance-    (marshalEncoding net)-    (marshalValue net)-  pathCompact-    (GetAddrUnspent <$> paramLazy <*> parseLimits)-    (fmap SerialList . scottyAddrUnspent)-    (list (marshalEncoding net) . (.get))-    (json_list (marshalValue net) . (.get))-  pathCompact-    (GetPeers & return)-    (fmap SerialList . scottyPeers)-    toEncoding-    toJSON-  pathCompact-    (GetHealth & return)-    scottyHealth-    toEncoding-    toJSON-  S.get "/events" scottyEvents-  S.get "/dbstats" scottyDbStats-  unless noSlow $ do-    pathCompact-      (GetBlocks <$> param <*> paramDef)-      (fmap SerialList . scottyBlocks)-      (list (marshalEncoding net) . (.get))-      (json_list (marshalValue net) . (.get))-    pathCompact-      (GetBlockRaw <$> paramLazy)-      scottyBlockRaw-      toEncoding-      toJSON-    pathCompact-      (GetBlockBestRaw & return)-      scottyBlockBestRaw-      toEncoding-      toJSON-    pathCompact-      (GetBlockLatest <$> paramDef)-      (fmap SerialList . scottyBlockLatest)-      (list (marshalEncoding net) . (.get))-      (json_list (marshalValue net) . (.get))-    pathCompact-      (GetBlockHeights <$> param <*> paramDef)-      (fmap SerialList . scottyBlockHeights)-      (list (marshalEncoding net) . (.get))-      (json_list (marshalValue net) . (.get))-    pathCompact-      (GetBlockHeightRaw <$> paramLazy)-      scottyBlockHeightRaw-      toEncoding-      toJSON-    pathCompact-      (GetBlockTimeRaw <$> paramLazy)-      scottyBlockTimeRaw-      toEncoding-      toJSON-    pathCompact-      (GetBlockMTPRaw <$> paramLazy)-      scottyBlockMTPRaw-      toEncoding-      toJSON-    pathCompact-      (GetTxs <$> param)-      (fmap SerialList . scottyTxs)-      (list (marshalEncoding net) . (.get))-      (json_list (marshalValue net) . (.get))-    pathCompact-      (GetTxsRaw <$> param)-      scottyTxsRaw-      toEncoding-      toJSON-    pathCompact-      (GetTxsBlock <$> paramLazy)-      (fmap SerialList . scottyTxsBlock)-      (list (marshalEncoding net) . (.get))-      (json_list (marshalValue net) . (.get))-    pathCompact-      (GetTxsBlockRaw <$> paramLazy)-      scottyTxsBlockRaw-      toEncoding-      toJSON-    pathCompact-      (GetTxAfter <$> paramLazy <*> paramLazy)-      scottyTxAfter-      toEncoding-      toJSON-    pathCompact-      (GetAddrsTxs <$> param <*> parseLimits)-      (fmap SerialList . scottyAddrsTxs)-      toEncoding-      toJSON-    pathCompact-      (GetAddrTxsFull <$> paramLazy <*> parseLimits)-      (fmap SerialList . scottyAddrTxsFull)-      (list (marshalEncoding net) . (.get))-      (json_list (marshalValue net) . (.get))-    pathCompact-      (GetAddrsTxsFull <$> param <*> parseLimits)-      (fmap SerialList . scottyAddrsTxsFull)-      (list (marshalEncoding net) . (.get))-      (json_list (marshalValue net) . (.get))-    pathCompact-      (GetAddrsBalance <$> param)-      (fmap SerialList . scottyAddrsBalance)-      (list (marshalEncoding net) . (.get))-      (json_list (marshalValue net) . (.get))-    pathCompact-      (GetAddrsUnspent <$> param <*> parseLimits)-      (fmap SerialList . scottyAddrsUnspent)-      (list (marshalEncoding net) . (.get))-      (json_list (marshalValue net) . (.get))-    pathCompact-      (GetXPub <$> paramLazy <*> paramDef <*> paramDef)-      scottyXPub-      toEncoding-      toJSON-    pathCompact-      (GetXPubTxs <$> paramLazy <*> paramDef <*> parseLimits <*> paramDef)-      (fmap SerialList . scottyXPubTxs)-      toEncoding-      toJSON-    pathCompact-      (GetXPubTxsFull <$> paramLazy <*> paramDef <*> parseLimits <*> paramDef)-      (fmap SerialList . scottyXPubTxsFull)-      (list (marshalEncoding net) . (.get))-      (json_list (marshalValue net) . (.get))-    pathCompact-      (GetXPubBalances <$> paramLazy <*> paramDef <*> paramDef)-      (fmap SerialList . scottyXPubBalances)-      (list (marshalEncoding net) . (.get))-      (json_list (marshalValue net) . (.get))-    pathCompact-      (GetXPubUnspent <$> paramLazy <*> paramDef <*> parseLimits <*> paramDef)-      (fmap SerialList . scottyXPubUnspent)-      (list (marshalEncoding net) . (.get))-      (json_list (marshalValue net) . (.get))-    pathCompact-      (DelCachedXPub <$> paramLazy <*> paramDef)-      scottyDelXPub-      toEncoding-      toJSON-    unless noBlockchainInfo $ do-      S.post "/blockchain/multiaddr" scottyMultiAddr-      S.get "/blockchain/multiaddr" scottyMultiAddr-      S.get "/blockchain/balance" scottyShortBal-      S.post "/blockchain/balance" scottyShortBal-      S.get "/blockchain/rawaddr/:addr" scottyRawAddr-      S.get "/blockchain/address/:addr" scottyRawAddr-      S.get "/blockchain/xpub/:addr" scottyRawAddr-      S.post "/blockchain/unspent" scottyBinfoUnspent-      S.get "/blockchain/unspent" scottyBinfoUnspent-      S.get "/blockchain/rawtx/:txid" scottyBinfoTx-      S.get "/blockchain/rawblock/:block" scottyBinfoBlock-      S.get "/blockchain/latestblock" scottyBinfoLatest-      S.get "/blockchain/unconfirmed-transactions" scottyBinfoMempool-      S.get "/blockchain/block-height/:height" scottyBinfoBlockHeight-      S.get "/blockchain/blocks/:milliseconds" scottyBinfoBlocksDay-      S.get "/blockchain/export-history" scottyBinfoHistory-      S.post "/blockchain/export-history" scottyBinfoHistory-      S.get "/blockchain/q/addresstohash/:addr" scottyBinfoAddrToHash-      S.get "/blockchain/q/hashtoaddress/:hash" scottyBinfoHashToAddr-      S.get "/blockchain/q/addrpubkey/:pubkey" scottyBinfoAddrPubkey-      S.get "/blockchain/q/pubkeyaddr/:addr" scottyBinfoPubKeyAddr-      S.get "/blockchain/q/hashpubkey/:pubkey" scottyBinfoHashPubkey-      S.get "/blockchain/q/getblockcount" scottyBinfoGetBlockCount-      S.get "/blockchain/q/latesthash" scottyBinfoLatestHash-      S.get "/blockchain/q/bcperblock" scottyBinfoSubsidy-      S.get "/blockchain/q/txtotalbtcoutput/:txid" scottyBinfoTotalOut-      S.get "/blockchain/q/txtotalbtcinput/:txid" scottyBinfoTotalInput-      S.get "/blockchain/q/txfee/:txid" scottyBinfoTxFees-      S.get "/blockchain/q/txresult/:txid/:addr" scottyBinfoTxResult-      S.get "/blockchain/q/getreceivedbyaddress/:addr" scottyBinfoReceived-      S.get "/blockchain/q/getsentbyaddress/:addr" scottyBinfoSent-      S.get "/blockchain/q/addressbalance/:addr" scottyBinfoAddrBalance-      S.get "/blockchain/q/addressfirstseen/:addr" scottyFirstSeen-  where-    json_list f = toJSONList . map f-    net = store.net--pathCompact ::-  (ApiResource a b, MonadIO m) =>-  WebT m a ->-  (a -> WebT m b) ->-  (b -> Encoding) ->-  (b -> Value) ->-  S.ScottyT Except (ReaderT WebState m) ()-pathCompact parser action encJson encValue =-  pathCommon parser action encJson encValue False--pathCommon ::-  (ApiResource a b, MonadIO m) =>-  WebT m a ->-  (a -> WebT m b) ->-  (b -> Encoding) ->-  (b -> Value) ->-  Bool ->-  S.ScottyT Except (ReaderT WebState m) ()-pathCommon parser action encJson encValue pretty =-  S.addroute (resourceMethod proxy) (capturePath proxy) $ do-    setHeaders-    proto <- setupContentType pretty-    apiRes <- parser-    res <- action apiRes-    S.raw $ protoSerial proto encJson encValue res-  where-    toProxy :: WebT m a -> Proxy a-    toProxy = const Proxy-    proxy = toProxy parser--streamEncoding :: (Monad m) => Encoding -> WebT m ()-streamEncoding e = do-  S.setHeader "Content-Type" "application/json; charset=utf-8"-  S.raw (encodingToLazyByteString e)--protoSerial ::-  (Serial a) =>-  SerialAs ->-  (a -> Encoding) ->-  (a -> Value) ->-  a ->-  L.ByteString-protoSerial SerialAsBinary _ _ = runPutL . serialize-protoSerial SerialAsJSON f _ = encodingToLazyByteString . f-protoSerial SerialAsPrettyJSON _ g =-  encodePretty' defConfig {confTrailingNewline = True} . g--setHeaders :: (Monad m, S.ScottyError e) => ActionT e m ()-setHeaders = S.setHeader "Access-Control-Allow-Origin" "*"--waiExcept :: Status -> Except -> Response-waiExcept s e =-  responseLBS s hs e'-  where-    hs =-      [ ("Access-Control-Allow-Origin", "*"),-        ("Content-Type", "application/json")-      ]-    e' = A.encode e--setupJSON :: (Monad m) => Bool -> ActionT Except m SerialAs-setupJSON pretty = do-  S.setHeader "Content-Type" "application/json"-  p <- S.param "pretty" `S.rescue` const (return pretty)-  return $ if p then SerialAsPrettyJSON else SerialAsJSON--setupBinary :: (Monad m) => ActionT Except m SerialAs-setupBinary = do-  S.setHeader "Content-Type" "application/octet-stream"-  return SerialAsBinary--setupContentType :: (Monad m) => Bool -> ActionT Except m SerialAs-setupContentType pretty = do-  accept <- S.header "accept"-  maybe (setupJSON pretty) setType accept-  where-    setType "application/octet-stream" = setupBinary-    setType _ = setupJSON pretty---- GET Block / GET Blocks ----scottyBlock ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetBlock -> WebT m BlockData-scottyBlock (GetBlock h (NoTx noTx)) = do-  setMetrics (.block)-  getBlock h >>= \case-    Nothing ->-      raise ThingNotFound-    Just b -> do-      addItemCount 1-      return $ pruneTx noTx b--getBlocks ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  [H.BlockHash] ->-  Bool ->-  WebT m [BlockData]-getBlocks hs notx =-  (pruneTx notx <$>) . catMaybes <$> mapM getBlock (nub hs)--scottyBlocks ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetBlocks -> WebT m [BlockData]-scottyBlocks (GetBlocks hs (NoTx notx)) = do-  setMetrics (.block)-  bs <- getBlocks hs notx-  addItemCount (length bs)-  return bs--pruneTx :: Bool -> BlockData -> BlockData-pruneTx False b = b-pruneTx True BlockData {..} = BlockData {txs = take 1 txs, ..}---- GET BlockRaw ----scottyBlockRaw ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetBlockRaw ->-  WebT m (RawResult H.Block)-scottyBlockRaw (GetBlockRaw h) = do-  setMetrics (.blockRaw)-  b <- getRawBlock h-  addItemCount (1 + length b.txs)-  return $ RawResult b--getRawBlock ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  H.BlockHash ->-  WebT m H.Block-getRawBlock h = do-  b <- getBlock h >>= maybe (raise ThingNotFound) return-  lift (toRawBlock b)--toRawBlock :: (MonadUnliftIO m, StoreReadBase m) => BlockData -> m H.Block-toRawBlock b = do-  let ths = b.txs-  txs <- mapM f ths-  return H.Block {header = b.header, txs}-  where-    f x = withRunInIO $ \run ->-      unsafeInterleaveIO . run $-        getTransaction x >>= \case-          Nothing -> undefined-          Just t -> return $ transactionData t---- GET BlockBest / BlockBestRaw ----scottyBlockBest ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetBlockBest -> WebT m BlockData-scottyBlockBest (GetBlockBest (NoTx notx)) = do-  setMetrics (.block)-  getBestBlock >>= \case-    Nothing -> raise ThingNotFound-    Just bb ->-      getBlock bb >>= \case-        Nothing -> raise ThingNotFound-        Just b -> do-          addItemCount 1-          return $ pruneTx notx b--scottyBlockBestRaw ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetBlockBestRaw ->-  WebT m (RawResult H.Block)-scottyBlockBestRaw _ = do-  setMetrics (.blockRaw)-  getBestBlock >>= \case-    Nothing -> raise ThingNotFound-    Just bb -> do-      b <- getRawBlock bb-      addItemCount (1 + length b.txs)-      return $ RawResult b---- GET BlockLatest ----scottyBlockLatest ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetBlockLatest ->-  WebT m [BlockData]-scottyBlockLatest (GetBlockLatest (NoTx noTx)) = do-  setMetrics (.block)-  blocks <--    getBestBlock-      >>= maybe-        (raise ThingNotFound)-        (go [] <=< getBlock)-  addItemCount (length blocks)-  return blocks-  where-    go acc Nothing = return $ reverse acc-    go acc (Just b)-      | b.height <= 0 = return $ reverse acc-      | length acc == 99 = return . reverse $ pruneTx noTx b : acc-      | otherwise = go (pruneTx noTx b : acc) =<< getBlock b.header.prev---- GET BlockHeight / BlockHeights / BlockHeightRaw ----scottyBlockHeight ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetBlockHeight -> WebT m [BlockData]-scottyBlockHeight (GetBlockHeight h (NoTx notx)) = do-  setMetrics (.block)-  blocks <- (`getBlocks` notx) =<< getBlocksAtHeight (fromIntegral h)-  addItemCount (length blocks)-  return blocks--scottyBlockHeights ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetBlockHeights ->-  WebT m [BlockData]-scottyBlockHeights (GetBlockHeights (HeightsParam heights) (NoTx notx)) = do-  setMetrics (.block)-  bhs <- concat <$> mapM getBlocksAtHeight (fromIntegral <$> heights)-  blocks <- getBlocks bhs notx-  addItemCount (length blocks)-  return blocks--scottyBlockHeightRaw ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetBlockHeightRaw ->-  WebT m (RawResultList H.Block)-scottyBlockHeightRaw (GetBlockHeightRaw h) = do-  setMetrics (.blockRaw)-  blocks <- mapM getRawBlock =<< getBlocksAtHeight (fromIntegral h)-  addItemCount (length blocks + sum (map (length . (.txs)) blocks))-  return $ RawResultList blocks---- GET BlockTime / BlockTimeRaw ----scottyBlockTime ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetBlockTime ->-  WebT m BlockData-scottyBlockTime (GetBlockTime (TimeParam t) (NoTx notx)) = do-  setMetrics (.block)-  ch <- lift $ asks (.config.store.chain)-  blockAtOrBefore ch t >>= \case-    Nothing -> raise ThingNotFound-    Just b -> do-      addItemCount 1-      return $ pruneTx notx b--scottyBlockMTP ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetBlockMTP ->-  WebT m BlockData-scottyBlockMTP (GetBlockMTP (TimeParam t) (NoTx notx)) = do-  setMetrics (.block)-  ch <- lift $ asks (.config.store.chain)-  blockAtOrAfterMTP ch t >>= \case-    Nothing -> raise ThingNotFound-    Just b -> do-      addItemCount 1-      return $ pruneTx notx b--scottyBlockTimeRaw ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetBlockTimeRaw ->-  WebT m (RawResult H.Block)-scottyBlockTimeRaw (GetBlockTimeRaw (TimeParam t)) = do-  setMetrics (.blockRaw)-  ch <- lift $ asks (.config.store.chain)-  blockAtOrBefore ch t >>= \case-    Nothing -> raise ThingNotFound-    Just b -> do-      raw <- lift $ toRawBlock b-      addItemCount (1 + length raw.txs)-      return $ RawResult raw--scottyBlockMTPRaw ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetBlockMTPRaw ->-  WebT m (RawResult H.Block)-scottyBlockMTPRaw (GetBlockMTPRaw (TimeParam t)) = do-  setMetrics (.blockRaw)-  ch <- lift $ asks (.config.store.chain)-  blockAtOrAfterMTP ch t >>= \case-    Nothing -> raise ThingNotFound-    Just b -> do-      raw <- lift $ toRawBlock b-      addItemCount (1 + length raw.txs)-      return $ RawResult raw---- GET Transactions ----scottyTx :: (MonadUnliftIO m, MonadLoggerIO m) => GetTx -> WebT m Transaction-scottyTx (GetTx txid) = do-  setMetrics (.tx)-  getTransaction txid >>= \case-    Nothing -> raise ThingNotFound-    Just tx -> do-      addItemCount 1-      return tx--scottyTxs ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetTxs -> WebT m [Transaction]-scottyTxs (GetTxs txids) = do-  setMetrics (.tx)-  txs <- catMaybes <$> mapM f (nub txids)-  addItemCount (length txs)-  return txs-  where-    f x = lift $-      withRunInIO $ \run ->-        unsafeInterleaveIO . run $-          getTransaction x--scottyTxRaw ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetTxRaw -> WebT m (RawResult Tx)-scottyTxRaw (GetTxRaw txid) = do-  setMetrics (.txRaw)-  getTransaction txid >>= \case-    Nothing -> raise ThingNotFound-    Just tx -> do-      addItemCount 1-      return $ RawResult (transactionData tx)--scottyTxsRaw ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetTxsRaw ->-  WebT m (RawResultList Tx)-scottyTxsRaw (GetTxsRaw txids) = do-  setMetrics (.txRaw)-  txs <- catMaybes <$> mapM f (nub txids)-  addItemCount (length txs)-  return $ RawResultList $ transactionData <$> txs-  where-    f x = lift $-      withRunInIO $ \run ->-        unsafeInterleaveIO . run $-          getTransaction x--getTxsBlock ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  H.BlockHash ->-  WebT m [Transaction]-getTxsBlock h =-  getBlock h >>= \case-    Nothing -> raise ThingNotFound-    Just b -> do-      txs <- mapM f b.txs-      addItemCount (length txs)-      return txs-  where-    f x = lift $-      withRunInIO $ \run ->-        unsafeInterleaveIO . run $-          getTransaction x >>= \case-            Nothing -> undefined-            Just t -> return t--scottyTxsBlock ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetTxsBlock ->-  WebT m [Transaction]-scottyTxsBlock (GetTxsBlock h) = do-  setMetrics (.txBlock)-  txs <- getTxsBlock h-  addItemCount (length txs)-  return txs--scottyTxsBlockRaw ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetTxsBlockRaw ->-  WebT m (RawResultList Tx)-scottyTxsBlockRaw (GetTxsBlockRaw h) = do-  setMetrics (.txBlockRaw)-  txs <- fmap transactionData <$> getTxsBlock h-  addItemCount (length txs)-  return $ RawResultList txs---- GET TransactionAfterHeight ----scottyTxAfter ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetTxAfter ->-  WebT m (GenericResult (Maybe Bool))-scottyTxAfter (GetTxAfter txid height) = do-  setMetrics (.txAfter)-  (result, count) <- cbAfterHeight (fromIntegral height) txid-  addItemCount count-  return $ GenericResult result---- | Check if any of the ancestors of this transaction is a coinbase after the--- specified height. Returns 'Nothing' if answer cannot be computed before--- hitting limits.-cbAfterHeight ::-  (MonadIO m, StoreReadBase m) =>-  H.BlockHeight ->-  TxHash ->-  m (Maybe Bool, Int)-cbAfterHeight height txid =-  inputs n HashSet.empty HashSet.empty [txid]-  where-    n = 10000-    inputs 0 _ _ [] = return (Nothing, 10000)-    inputs i is ns [] =-      let is' = HashSet.union is ns-          ns' = HashSet.empty-          ts = HashSet.toList (HashSet.difference ns is)-       in case ts of-            [] -> return (Just False, n - i)-            _ -> inputs i is' ns' ts-    inputs i is ns (t : ts) =-      getTransaction t >>= \case-        Nothing -> return (Nothing, n - i)-        Just tx-          | height_check tx ->-              if cb_check tx-                then return (Just True, n - i + 1)-                else-                  let ns' = HashSet.union (ins tx) ns-                   in inputs (i - 1) is ns' ts-          | otherwise -> inputs (i - 1) is ns ts-    cb_check = any isCoinbase . (.inputs)-    ins = HashSet.fromList . map (.outpoint.hash) . (.inputs)-    height_check tx =-      case tx.block of-        BlockRef h _ -> h > height-        MemRef _ -> True---- POST Transaction ----scottyPostTx :: (MonadUnliftIO m, MonadLoggerIO m) => PostTx -> WebT m TxId-scottyPostTx (PostTx tx) = do-  setMetrics (.txPost)-  addItemCount 1-  lift (asks (.config)) >>= \cfg ->-    lift (publishTx cfg tx) >>= \case-      Right () -> return (TxId (txHash tx))-      Left e@(PubReject _) -> raise $ UserError (show e)-      _ -> raise ServerError---- | Publish a new transaction to the network.-publishTx ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>-  WebConfig ->-  Tx ->-  m (Either PubExcept ())-publishTx cfg tx =-  withSubscription pub $ \s ->-    getTransaction (txHash tx) >>= \case-      Just _ -> return $ Right ()-      Nothing -> go s-  where-    pub = cfg.store.pub-    mgr = cfg.store.peerMgr-    net = cfg.store.net-    go s =-      getPeers mgr >>= \case-        [] -> return $ Left PubNoPeers-        OnlinePeer {mailbox = p} : _ -> do-          MTx tx `sendMessage` p-          let v =-                if net.segWit-                  then InvWitnessTx-                  else InvTx-          sendMessage-            (MGetData (GetData [InvVector v (txHash tx).get]))-            p-          f p s-    t = 5 * 1000 * 1000-    f p s-      | cfg.noMempool = return $ Right ()-      | otherwise =-          liftIO (UnliftIO.timeout t (g p s)) >>= \case-            Nothing -> return $ Left PubTimeout-            Just (Left e) -> return $ Left e-            Just (Right ()) -> return $ Right ()-    g p s =-      receive s >>= \case-        StoreTxReject p' h' c _-          | p == p' && h' == txHash tx -> return . Left $ PubReject c-        StorePeerDisconnected p'-          | p == p' -> return $ Left PubPeerDisconnected-        StoreMempoolNew h'-          | h' == txHash tx -> return $ Right ()-        _ -> g p s---- GET Mempool / Events ----scottyMempool ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetMempool -> WebT m [TxHash]-scottyMempool (GetMempool limitM (OffsetParam o)) = do-  setMetrics (.mempool)-  WebLimits {..} <- lift $ asks (.config.limits)-  let wl' = WebLimits {maxItemCount = 0, ..}-      l = Limits (validateLimit wl' False limitM) (fromIntegral o) Nothing-  ths <- map snd . applyLimits l <$> getMempool-  addItemCount 1-  return ths--webSocketEvents :: WebState -> Middleware-webSocketEvents s =-  websocketsOr defaultConnectionOptions events-  where-    pub = s.config.store.pub-    gauge = (.events) <$> s.metrics-    events pending = withSubscription pub $ \sub -> do-      let path = requestPath $ pendingRequest pending-      if path == "/events"-        then do-          conn <- acceptRequest pending-          forever $-            receiveEvent sub >>= \case-              Nothing -> return ()-              Just event -> sendTextData conn (A.encode event)-        else-          rejectRequestWith-            pending-            WebSockets.defaultRejectRequest-              { WebSockets.rejectBody =-                  L.toStrict $ A.encode ThingNotFound,-                WebSockets.rejectCode =-                  404,-                WebSockets.rejectMessage =-                  "Not Found",-                WebSockets.rejectHeaders =-                  [("Content-Type", "application/json")]-              }--scottyEvents :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyEvents =-  withGaugeIncrease (.events) $ do-    setHeaders-    proto <- setupContentType False-    pub <- lift $ asks (.config.store.pub)-    S.stream $ \io flush' ->-      withSubscription pub $ \sub ->-        forever $ do-          flush'-          receiveEvent sub >>= \case-            Nothing -> return ()-            Just msg -> io (serial proto msg)-  where-    serial proto e =-      lazyByteString $-        protoSerial proto toEncoding toJSON e <> newLine proto-    newLine SerialAsBinary = mempty-    newLine SerialAsJSON = "\n"-    newLine SerialAsPrettyJSON = mempty--receiveEvent :: Inbox StoreEvent -> IO (Maybe Event)-receiveEvent sub =-  go <$> receive sub-  where-    go = \case-      StoreBestBlock b -> Just (EventBlock b)-      StoreMempoolNew t -> Just (EventTx t)-      StoreMempoolDelete t -> Just (EventTx t)-      _ -> Nothing---- GET Address Transactions ----scottyAddrTxs ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetAddrTxs -> WebT m [TxRef]-scottyAddrTxs (GetAddrTxs addr pLimits) = do-  setMetrics (.addressTx)-  txs <- getAddressTxs addr =<< paramToLimits False pLimits-  addItemCount (length txs)-  return txs--scottyAddrsTxs ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetAddrsTxs -> WebT m [TxRef]-scottyAddrsTxs (GetAddrsTxs addrs pLimits) = do-  setMetrics (.addressTx)-  txs <- getAddressesTxs addrs =<< paramToLimits False pLimits-  addItemCount (length txs)-  return txs--scottyAddrTxsFull ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetAddrTxsFull ->-  WebT m [Transaction]-scottyAddrTxsFull (GetAddrTxsFull addr pLimits) = do-  setMetrics (.addressTxFull)-  txs <- getAddressTxs addr =<< paramToLimits True pLimits-  ts <- catMaybes <$> mapM (getTransaction . (.txid)) txs-  addItemCount (length ts)-  return ts--scottyAddrsTxsFull ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetAddrsTxsFull ->-  WebT m [Transaction]-scottyAddrsTxsFull (GetAddrsTxsFull addrs pLimits) = do-  setMetrics (.addressTxFull)-  txs <- getAddressesTxs addrs =<< paramToLimits True pLimits-  ts <- catMaybes <$> mapM (getTransaction . (.txid)) txs-  addItemCount (length ts)-  return ts--scottyAddrBalance ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetAddrBalance ->-  WebT m Balance-scottyAddrBalance (GetAddrBalance addr) = do-  setMetrics (.addressBalance)-  addItemCount 1-  getDefaultBalance addr--scottyAddrsBalance ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetAddrsBalance -> WebT m [Balance]-scottyAddrsBalance (GetAddrsBalance addrs) = do-  setMetrics (.addressBalance)-  balances <- getBalances addrs-  addItemCount (length balances)-  return balances--scottyAddrUnspent ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetAddrUnspent -> WebT m [Unspent]-scottyAddrUnspent (GetAddrUnspent addr pLimits) = do-  setMetrics (.addressUnspent)-  unspents <- getAddressUnspents addr =<< paramToLimits False pLimits-  addItemCount (length unspents)-  return unspents--scottyAddrsUnspent ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetAddrsUnspent -> WebT m [Unspent]-scottyAddrsUnspent (GetAddrsUnspent addrs pLimits) = do-  setMetrics (.addressUnspent)-  unspents <- getAddressesUnspents addrs =<< paramToLimits False pLimits-  addItemCount (length unspents)-  return unspents---- GET XPubs ----scottyXPub ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetXPub -> WebT m XPubSummary-scottyXPub (GetXPub xpub deriv (NoCache noCache)) = do-  setMetrics (.xpub)-  let xspec = XPubSpec xpub deriv-  xbals <- lift . runNoCache noCache $ xPubBals xspec-  addItemCount (length xbals)-  return $ xPubSummary xspec xbals--scottyDelXPub ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  DelCachedXPub ->-  WebT m (GenericResult Bool)-scottyDelXPub (DelCachedXPub xpub deriv) = do-  setMetrics (.xpubDelete)-  let xspec = XPubSpec xpub deriv-  cacheM <- lift $ asks (.config.store.cache)-  n <- lift $ withCache cacheM (cacheDelXPubs [xspec])-  addItemCount (fromIntegral n)-  return (GenericResult (n > 0))--getXPubTxs ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  XPubKey ->-  DeriveType ->-  LimitsParam ->-  Bool ->-  WebT m [TxRef]-getXPubTxs xpub deriv plimits nocache = do-  limits <- paramToLimits False plimits-  let xspec = XPubSpec xpub deriv-  xbals <- xPubBals xspec-  addItemCount (length xbals)-  lift . runNoCache nocache $ xPubTxs xspec xbals limits--scottyXPubTxs ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetXPubTxs -> WebT m [TxRef]-scottyXPubTxs (GetXPubTxs xpub deriv plimits (NoCache nocache)) = do-  setMetrics (.xpubTx)-  txs <- getXPubTxs xpub deriv plimits nocache-  addItemCount (length txs)-  return txs--scottyXPubTxsFull ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetXPubTxsFull ->-  WebT m [Transaction]-scottyXPubTxsFull (GetXPubTxsFull xpub deriv plimits (NoCache nocache)) = do-  setMetrics (.xpubTxFull)-  refs <- getXPubTxs xpub deriv plimits nocache-  txs <--    fmap catMaybes $-      lift . runNoCache nocache $-        mapM (getTransaction . (.txid)) refs-  addItemCount (length txs)-  return txs--scottyXPubBalances ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetXPubBalances -> WebT m [XPubBal]-scottyXPubBalances (GetXPubBalances xpub deriv (NoCache noCache)) = do-  setMetrics (.xpubBalance)-  balances <- lift (runNoCache noCache (xPubBals spec))-  addItemCount (length balances)-  return balances-  where-    spec = XPubSpec xpub deriv--scottyXPubUnspent ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetXPubUnspent ->-  WebT m [XPubUnspent]-scottyXPubUnspent (GetXPubUnspent xpub deriv pLimits (NoCache noCache)) = do-  setMetrics (.xpubUnspent)-  limits <- paramToLimits False pLimits-  let xspec = XPubSpec xpub deriv-  xbals <- xPubBals xspec-  addItemCount (length xbals)-  unspents <- lift . runNoCache noCache $ xPubUnspents xspec xbals limits-  addItemCount (length unspents)-  return unspents-------------------------------------------- Blockchain.info API Compatibility --------------------------------------------netBinfoSymbol :: Network -> BinfoSymbol-netBinfoSymbol net-  | net == btc =-      BinfoSymbol-        { code = "BTC",-          symbol = "BTC",-          name = "Bitcoin",-          conversion = 100 * 1000 * 1000,-          after = True,-          local = False-        }-  | net == bch =-      BinfoSymbol-        { code = "BCH",-          symbol = "BCH",-          name = "Bitcoin Cash",-          conversion = 100 * 1000 * 1000,-          after = True,-          local = False-        }-  | otherwise =-      BinfoSymbol-        { code = "XTS",-          symbol = "¤",-          name = "Test",-          conversion = 100 * 1000 * 1000,-          after = False,-          local = False-        }--binfoTickerToSymbol :: Text -> BinfoTicker -> BinfoSymbol-binfoTickerToSymbol code BinfoTicker {..} =-  BinfoSymbol-    { code,-      symbol,-      name,-      conversion = 100 * 1000 * 1000 / fifteen, -- sat/usd-      after = False,-      local = True-    }-  where-    name = case code of-      "EUR" -> "Euro"-      "USD" -> "U.S. dollar"-      "GBP" -> "British pound"-      x -> x--getBinfoAddrsParam ::-  (MonadIO m) =>-  Text ->-  WebT m (HashSet BinfoAddr)-getBinfoAddrsParam name = do-  net <- lift $ asks (.config.store.net)-  ctx <- lift $ asks (.config.store.ctx)-  p <- S.param (cs name) `S.rescue` const (return "")-  if T.null p-    then return HashSet.empty-    else case parseBinfoAddr net ctx p of-      Nothing -> raise (UserError "invalid address")-      Just xs -> return $ HashSet.fromList xs--getBinfoActive ::-  (MonadIO m) =>-  WebT m (HashSet XPubSpec, HashSet Address)-getBinfoActive = do-  active <- getBinfoAddrsParam "active"-  p2sh <- getBinfoAddrsParam "activeP2SH"-  bech32 <- getBinfoAddrsParam "activeBech32"-  let xspec d b = (`XPubSpec` d) <$> xpub b-      xspecs =-        HashSet.fromList $-          concat-            [ mapMaybe (xspec DeriveNormal) (HashSet.toList active),-              mapMaybe (xspec DeriveP2SH) (HashSet.toList p2sh),-              mapMaybe (xspec DeriveP2WPKH) (HashSet.toList bech32)-            ]-      addrs = HashSet.fromList . mapMaybe addr $ HashSet.toList active-  return (xspecs, addrs)-  where-    addr (BinfoAddr a) = Just a-    addr (BinfoXpub _) = Nothing-    xpub (BinfoXpub x) = Just x-    xpub (BinfoAddr _) = Nothing--getNumTxId :: (MonadIO m) => WebT m Bool-getNumTxId = fmap not $ S.param "txidindex" `S.rescue` const (return False)--getChainHeight :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m H.BlockHeight-getChainHeight = do-  ch <- lift $ asks (.config.store.chain)-  (.height) <$> chainGetBest ch--scottyBinfoUnspent :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoUnspent = do-  setMetrics (.binfoUnspent)-  (xspecs, addrs) <- getBinfoActive-  numtxid <- getNumTxId-  limit <- get_limit-  min_conf <- get_min_conf-  net <- lift $ asks (.config.store.net)-  ctx <- lift $ asks (.config.store.ctx)-  height <- getChainHeight-  let mn BinfoUnspent {..} = min_conf > confirmations-  xbals <- lift $ getXBals xspecs-  addItemCount . sum . map length $ HashMap.elems xbals-  counter <- getItemCounter-  bus <--    lift . runConduit $-      getBinfoUnspents counter numtxid height xbals xspecs addrs-        .| (dropWhileC mn >> takeC limit .| sinkList)-  setHeaders-  streamEncoding (marshalEncoding (net, ctx) (BinfoUnspents bus))-  where-    get_limit = fmap (min 1000) $ S.param "limit" `S.rescue` const (return 250)-    get_min_conf = S.param "confirmations" `S.rescue` const (return 0)--getBinfoUnspents ::-  (StoreReadExtra m, MonadIO m) =>-  (Int -> m ()) ->-  Bool ->-  H.BlockHeight ->-  HashMap XPubSpec [XPubBal] ->-  HashSet XPubSpec ->-  HashSet Address ->-  ConduitT () BinfoUnspent m ()-getBinfoUnspents counter numtxid height xbals xspecs addrs = do-  cs' <- conduits-  joinDescStreams cs' .| mapC (uncurry binfo)-  where-    binfo Unspent {..} xp =-      let conf = case block of-            MemRef _ -> 0-            BlockRef h _ -> height - h + 1-          hash = outpoint.hash-          idx = outpoint.index-          val = value-          script = script-          txi = encodeBinfoTxId numtxid hash-       in BinfoUnspent-            { txid = hash,-              index = idx,-              script,-              value = val,-              confirmations = fromIntegral conf,-              txidx = txi,-              xpub = xp-            }-    conduits = (<>) <$> xconduits <*> pure acounduits-    xconduits = lift $ do-      let f x (XPubUnspent u p) =-            let path = toSoft (listToPath p)-                xp = BinfoXPubPath x.key <$> path-             in (u, xp)-          g x = do-            let h l = do-                  us <- xPubUnspents x (xBals x xbals) l-                  counter (length us)-                  return us-                l = let Limits {..} = def in Limits {limit = 16, ..}-            return $ streamThings h Nothing l .| mapC (f x)-      mapM g (HashSet.toList xspecs)-    acounduits =-      let f u = (u, Nothing)-          h a l = do-            us <- getAddressUnspents a l-            counter (length us)-            return us-          l = let Limits {..} = def in Limits {limit = 16, ..}-          g a = streamThings (h a) Nothing l .| mapC f-       in map g (HashSet.toList addrs)--getXBals ::-  (StoreReadExtra m) =>-  HashSet XPubSpec ->-  m (HashMap XPubSpec [XPubBal])-getXBals =-  fmap HashMap.fromList . mapM f . HashSet.toList-  where-    f x = (x,) . filter (not . nullBalance . (.balance)) <$> xPubBals x--xBals :: XPubSpec -> HashMap XPubSpec [XPubBal] -> [XPubBal]-xBals = HashMap.findWithDefault []--getBinfoTxs ::-  (StoreReadExtra m, MonadIO m) =>-  (Int -> m ()) -> -- counter-  HashMap XPubSpec [XPubBal] -> -- xpub balances-  HashMap Address (Maybe BinfoXPubPath) -> -- address book-  HashSet XPubSpec -> -- show xpubs-  HashSet Address -> -- show addrs-  HashSet Address -> -- balance addresses-  BinfoFilter ->-  Bool -> -- numtxid-  Bool -> -- prune outputs-  Int64 -> -- starting balance-  ConduitT () BinfoTx m ()-getBinfoTxs-  counter-  xbals-  abook-  sxspecs-  saddrs-  baddrs-  bfilter-  numtxid-  prune-  bal = do-    cs' <- conduits-    joinDescStreams cs' .| go bal-    where-      sxspecs_ls = HashSet.toList sxspecs-      saddrs_ls = HashSet.toList saddrs-      conduits =-        (<>)-          <$> mapM xpub_c sxspecs_ls-          <*> pure (map addr_c saddrs_ls)-      xpub_c x = do-        let f l = do-              ts <- xPubTxs x (xBals x xbals) l-              counter (length ts)-              return ts-            l = let Limits {..} = def in Limits {limit = 16, ..}-        lift . return $-          streamThings f (Just (.txid)) l-      addr_c a = do-        let f l = do-              as <- getAddressTxs a l-              counter (length as)-              return as-            l = let Limits {..} = def in Limits {limit = 16, ..}-        streamThings f (Just (.txid)) l-      binfo_tx = toBinfoTx numtxid abook prune-      compute_bal_change BinfoTx {..} =-        let ins = map (.output) inputs-            out = outputs-            f b BinfoTxOutput {..} =-              let val = fromIntegral value-               in case address of-                    Nothing -> 0-                    Just a-                      | a `HashSet.member` baddrs ->-                          if b then val else negate val-                      | otherwise -> 0-         in sum $ map (f False) ins <> map (f True) out-      go b =-        await >>= \case-          Nothing -> return ()-          Just (TxRef _ t) ->-            lift (getTransaction t) >>= \case-              Nothing -> go b-              Just x -> do-                lift $ counter 1-                let a = binfo_tx b x-                    b' = b - compute_bal_change a-                    c = isJust a.blockHeight-                    Just (d, _) = a.balance-                    r = d + fromIntegral a.fee-                case bfilter of-                  BinfoFilterAll ->-                    yield a >> go b'-                  BinfoFilterSent-                    | 0 > r -> yield a >> go b'-                    | otherwise -> go b'-                  BinfoFilterReceived-                    | r > 0 -> yield a >> go b'-                    | otherwise -> go b'-                  BinfoFilterMoved-                    | r == 0 -> yield a >> go b'-                    | otherwise -> go b'-                  BinfoFilterConfirmed-                    | c -> yield a >> go b'-                    | otherwise -> go b'-                  BinfoFilterMempool-                    | c -> return ()-                    | otherwise -> yield a >> go b'--getCashAddr :: (Monad m) => WebT m Bool-getCashAddr = S.param "cashaddr" `S.rescue` const (return False)--getAddress :: (Monad m, MonadUnliftIO m) => TL.Text -> WebT m Address-getAddress param' = do-  txt <- S.param param'-  net <- lift $ asks (.config.store.net)-  case textToAddr net txt of-    Nothing -> raise ThingNotFound-    Just a -> return a--getBinfoAddr :: (Monad m) => TL.Text -> WebT m BinfoAddr-getBinfoAddr param' = do-  txt <- S.param param'-  net <- lift $ asks (.config.store.net)-  ctx <- lift $ asks (.config.store.ctx)-  let x =-        BinfoAddr-          <$> textToAddr net txt-          <|> BinfoXpub <$> xPubImport net ctx txt-  maybe S.next return x--scottyBinfoHistory :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoHistory = do-  setMetrics (.binfoExportHistory)-  (xspecs, addrs) <- getBinfoActive-  (startM, endM) <- get_dates-  (code, price') <- getPrice-  xbals <- getXBals xspecs-  addItemCount . sum . map length $ HashMap.elems xbals-  counter <- getItemCounter-  let xaddrs = HashSet.fromList $ concatMap (map get_addr) (HashMap.elems xbals)-      aaddrs = xaddrs <> addrs-      cur = price'.fifteen-      cs' = conduits counter (HashMap.toList xbals) addrs endM-  txs <--    lift $-      runConduit $-        joinDescStreams cs'-          .| takeWhileC (is_newer startM)-          .| concatMapMC get_transaction-          .| sinkList-  addItemCount (length txs)-  let times = map (\Transaction {..} -> timestamp) txs-  net <- lift $ asks (.config.store.net)-  url <- lift $ asks (.config.priceHistoryURL)-  session <- lift $ asks (.session)-  rates <- map (.price) <$> lift (getRates net session url code times)-  addItemCount (length rates)-  let hs = zipWith (convert cur aaddrs) txs (rates <> repeat 0.0)-  setHeaders-  streamEncoding $ toEncoding hs-  where-    is_newer (Just b@BlockData {}) TxRef {block = t@BlockRef {}} =-      b.height <= t.height-    is_newer Nothing TxRef {} = True-    get_addr = (.balance.address)-    get_transaction TxRef {txid = h} =-      getTransaction h-    convert cur addrs tx rate =-      let ins = tx.inputs-          outs = tx.outputs-          fins = filter (input_addr addrs) ins-          fouts = filter (output_addr addrs) outs-          vin = fromIntegral . sum $ map (.value) fins-          vout = fromIntegral . sum $ map (.value) fouts-          fee = tx.fee-          v = vout - vin-          t = tx.timestamp-          h = txHash $ transactionData tx-       in toBinfoHistory v t rate cur fee h-    input_addr addrs' StoreInput {address = Just a} =-      a `HashSet.member` addrs'-    input_addr _ _ = False-    output_addr addrs' StoreOutput {address = Just a} =-      a `HashSet.member` addrs'-    output_addr _ _ = False-    get_dates = do-      BinfoDate start <- S.param "start"-      BinfoDate end' <- S.param "end"-      let end = end' + 24 * 60 * 60-      ch <- lift $ asks (.config.store.chain)-      startM <- blockAtOrAfter ch start-      endM <- blockAtOrBefore ch end-      return (startM, endM)-    conduits counter xpubs addrs endM =-      map (uncurry (xpub_c counter endM)) xpubs-        <> map (addr_c counter endM) (HashSet.toList addrs)-    addr_c counter endM a = do-      let f l = do-            ts <- getAddressTxs a l-            counter (length ts)-            return ts-          l =-            let Limits {..} = def-             in Limits-                  { limit = 16,-                    start = AtBlock . (.height) <$> endM,-                    ..-                  }-      streamThings f (Just (.txid)) l-    xpub_c counter endM x bs = do-      let f l = do-            ts <- xPubTxs x bs l-            counter (length ts)-            return ts-          l =-            let Limits {..} = def-             in Limits-                  { limit = 16,-                    start = AtBlock . (.height) <$> endM,-                    ..-                  }-      streamThings f (Just (.txid)) l--getPrice :: (MonadIO m) => WebT m (Text, BinfoTicker)-getPrice = do-  code <- T.toUpper <$> S.param "currency" `S.rescue` const (return "USD")-  ticker <- lift $ asks (.ticker)-  prices <- readTVarIO ticker-  case HashMap.lookup code prices of-    Nothing -> return (code, def)-    Just p -> return (code, p)--getSymbol :: (MonadIO m) => WebT m BinfoSymbol-getSymbol = uncurry binfoTickerToSymbol <$> getPrice--scottyBinfoBlocksDay :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoBlocksDay = do-  setMetrics (.binfoBlock)-  t <- min h . (`div` 1000) <$> S.param "milliseconds"-  ch <- lift $ asks (.config.store.chain)-  m <- blockAtOrBefore ch t-  bs <- go (d t) m-  addItemCount (length bs)-  streamEncoding $ toEncoding $ map toBinfoBlockInfo bs-  where-    h = fromIntegral (maxBound :: H.Timestamp)-    d = subtract (24 * 3600)-    go _ Nothing = return []-    go t (Just b)-      | b.header.timestamp <= fromIntegral t =-          return []-      | otherwise = do-          b' <- getBlock b.header.prev-          (b :) <$> go t b'--scottyMultiAddr :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyMultiAddr = do-  setMetrics (.binfoMultiaddr)-  (addrs', _, saddrs, sxpubs, xspecs) <- get_addrs-  numtxid <- getNumTxId-  cashaddr <- getCashAddr-  local' <- getSymbol-  offset <- getBinfoOffset-  n <- getBinfoCount "n"-  prune <- get_prune-  fltr <- get_filter-  xbals <- getXBals xspecs-  addItemCount . sum . map length $ HashMap.elems xbals-  xtxns <- get_xpub_tx_count xbals xspecs-  addItemCount (length xtxns)-  let sxbals = only_show_xbals sxpubs xbals-      xabals = compute_xabals xbals-      addrs = addrs' `HashSet.difference` HashMap.keysSet xabals-  abals <- get_abals addrs-  addItemCount (length abals)-  let sxspecs = only_show_xspecs sxpubs xspecs-      sxabals = compute_xabals sxbals-      sabals = only_show_abals saddrs abals-      sallbals = sabals <> sxabals-      sbal = compute_bal sallbals-      allbals = abals <> xabals-      abook = compute_abook addrs xbals-      sxaddrs = compute_xaddrs sxbals-      salladdrs = saddrs <> sxaddrs-      bal = compute_bal allbals-      ibal = fromIntegral sbal-  counter <- getItemCounter-  ftxs <--    lift . runConduit $-      getBinfoTxs-        counter-        xbals-        abook-        sxspecs-        saddrs-        salladdrs-        fltr-        numtxid-        prune-        ibal-        .| (dropC offset >> takeC n .| sinkList)-  net <- lift $ asks (.config.store.net)-  ctx <- lift $ asks (.config.store.ctx)-  best <- get_best_block-  addItemCount 1-  peers <- get_peers-  addItemCount (fromIntegral peers)-  let baddrs = toBinfoAddrs sabals sxbals xtxns-      abaddrs = toBinfoAddrs abals xbals xtxns-      recv = sum $ map (.received) abaddrs-      sent' = sum $ map (.sent) abaddrs-      txn = fromIntegral $ length ftxs-      wallet =-        BinfoWallet-          { balance = bal,-            txs = txn,-            filtered = txn,-            received = recv,-            sent = sent'-          }-      coin = netBinfoSymbol net-  let block =-        BinfoBlockInfo-          { hash = H.headerHash best.header,-            height = best.height,-            timestamp = best.header.timestamp,-            index = best.height-          }-  let info =-        BinfoInfo-          { connected = peers,-            conversion = 100 * 1000 * 1000,-            fiat = local',-            crypto = coin,-            head = block-          }-  let multiaddr =-        BinfoMultiAddr-          { addresses = baddrs,-            wallet = wallet,-            txs = ftxs,-            info = info,-            recommendFee = True,-            cashAddr = cashaddr-          }-  setHeaders-  streamEncoding $ marshalEncoding (net, ctx) multiaddr-  where-    get_xpub_tx_count xbals =-      fmap HashMap.fromList-        . mapM-          ( \x ->-              (x,)-                . fromIntegral-                <$> xPubTxCount x (xBals x xbals)-          )-        . HashSet.toList-    get_filter =-      S.param "filter" `S.rescue` const (return BinfoFilterAll)-    get_best_block =-      getBestBlock >>= \case-        Nothing -> raise ThingNotFound-        Just bh ->-          getBlock bh >>= \case-            Nothing -> raise ThingNotFound-            Just b -> return b-    get_prune =-      fmap not $-        S.param "no_compact"-          `S.rescue` const (return False)-    only_show_xbals sxpubs =-      HashMap.filterWithKey (\k _ -> k.key `HashSet.member` sxpubs)-    only_show_xspecs sxpubs =-      HashSet.filter (\k -> k.key `HashSet.member` sxpubs)-    only_show_abals saddrs =-      HashMap.filterWithKey (\k _ -> k `HashSet.member` saddrs)-    addr (BinfoAddr a) = Just a-    addr (BinfoXpub _) = Nothing-    xpub (BinfoXpub x) = Just x-    xpub (BinfoAddr _) = Nothing-    get_addrs = do-      (xspecs, addrs) <- getBinfoActive-      sh <- getBinfoAddrsParam "onlyShow"-      let xpubs = HashSet.map (.key) xspecs-          actives =-            HashSet.map BinfoAddr addrs-              <> HashSet.map BinfoXpub xpubs-          sh' = if HashSet.null sh then actives else sh-          saddrs = HashSet.fromList $ mapMaybe addr $ HashSet.toList sh'-          sxpubs = HashSet.fromList $ mapMaybe xpub $ HashSet.toList sh'-      return (addrs, xpubs, saddrs, sxpubs, xspecs)-    get_abals =-      let f b = (b.address, b)-          g = HashMap.fromList . map f-       in fmap g . getBalances . HashSet.toList-    get_peers = do-      ps <- lift $ getPeersInformation =<< asks (.config.store.peerMgr)-      return (fromIntegral (length ps))-    compute_xabals =-      let f b = (b.balance.address, b.balance)-       in HashMap.fromList . concatMap (map f) . HashMap.elems-    compute_bal =-      let f b = b.confirmed + b.unconfirmed-       in sum . map f . HashMap.elems-    compute_abook addrs xbals =-      let f XPubSpec {..} XPubBal {..} =-            let a = balance.address-                e = error "lions and tigers and bears"-                s = toSoft (listToPath path)-             in (a, Just (BinfoXPubPath key (fromMaybe e s)))-          amap =-            HashMap.map (const Nothing) $-              HashSet.toMap addrs-          xmap =-            HashMap.fromList-              . concatMap (uncurry (map . f))-              $ HashMap.toList xbals-       in amap <> xmap-    compute_xaddrs =-      let f = map (.balance.address)-       in HashSet.fromList . concatMap f . HashMap.elems--getBinfoCount :: (MonadUnliftIO m, MonadLoggerIO m) => TL.Text -> WebT m Int-getBinfoCount str = do-  d <- lift $ asks (.config.limits.defItemCount)-  x <- lift $ asks (.config.limits.maxFullItemCount)-  i <- min x <$> (S.param str `S.rescue` const (return d))-  return (fromIntegral i :: Int)--getBinfoOffset ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  WebT m Int-getBinfoOffset = do-  x <- lift $ asks (.config.limits.maxOffset)-  o <- S.param "offset" `S.rescue` const (return 0)-  when (o > x) $-    raise $-      UserError $-        "offset exceeded: " <> show o <> " > " <> show x-  return (fromIntegral o :: Int)--scottyRawAddr :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyRawAddr =-  setMetrics (.binfoAddressRaw)-    >> getBinfoAddr "addr"-    >>= \case-      BinfoAddr addr -> do_addr addr-      BinfoXpub xpub -> do_xpub xpub-  where-    do_xpub xpub = do-      numtxid <- getNumTxId-      derive <- S.param "derive" `S.rescue` const (return DeriveNormal)-      let xspec = XPubSpec xpub derive-      n <- getBinfoCount "limit"-      off <- getBinfoOffset-      xbals <- getXBals $ HashSet.singleton xspec-      addItemCount . sum . map length $ HashMap.elems xbals-      net <- lift $ asks (.config.store.net)-      let summary = xPubSummary xspec (xBals xspec xbals)-          abook = compute_abook xpub (xBals xspec xbals)-          xspecs = HashSet.singleton xspec-          saddrs = HashSet.empty-          baddrs = HashMap.keysSet abook-          bfilter = BinfoFilterAll-          amnt = summary.confirmed + summary.unconfirmed-      counter <- getItemCounter-      txs <--        lift $-          runConduit $-            getBinfoTxs-              counter-              xbals-              abook-              xspecs-              saddrs-              baddrs-              bfilter-              numtxid-              False-              (fromIntegral amnt)-              .| (dropC off >> takeC n .| sinkList)-      let ra =-            BinfoRawAddr-              { address = BinfoXpub xpub,-                balance = amnt,-                ntx = fromIntegral $ length txs,-                utxo = summary.utxo,-                received = summary.received,-                sent = fromIntegral summary.received - fromIntegral amnt,-                txs = txs-              }-      setHeaders-      ctx <- asks (.config.store.ctx)-      streamEncoding $ marshalEncoding (net, ctx) ra-    compute_abook xpub xbals =-      let f XPubBal {..} =-            let a = balance.address-                e = error "black hole swallows all your code"-                s = toSoft $ listToPath path-                m = fromMaybe e s-             in (a, Just (BinfoXPubPath xpub m))-       in HashMap.fromList $ map f xbals-    do_addr addr = do-      numtxid <- getNumTxId-      n <- getBinfoCount "limit"-      off <- getBinfoOffset-      bal <- fromMaybe (zeroBalance addr) <$> getBalance addr-      addItemCount 1-      net <- lift $ asks (.config.store.net)-      let abook = HashMap.singleton addr Nothing-          xspecs = HashSet.empty-          saddrs = HashSet.singleton addr-          bfilter = BinfoFilterAll-          amnt = bal.confirmed + bal.unconfirmed-      counter <- getItemCounter-      txs <--        lift $-          runConduit $-            getBinfoTxs-              counter-              HashMap.empty-              abook-              xspecs-              saddrs-              saddrs-              bfilter-              numtxid-              False-              (fromIntegral amnt)-              .| (dropC off >> takeC n .| sinkList)-      let ra =-            BinfoRawAddr-              { address = BinfoAddr addr,-                balance = amnt,-                ntx = bal.txs,-                utxo = bal.utxo,-                received = bal.received,-                sent = fromIntegral bal.received - fromIntegral amnt,-                txs = txs-              }-      setHeaders-      ctx <- asks (.config.store.ctx)-      streamEncoding $ marshalEncoding (net, ctx) ra--scottyBinfoReceived :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoReceived = do-  setMetrics (.binfoQgetreceivedbyaddress)-  a <- getAddress "addr"-  b <- fromMaybe (zeroBalance a) <$> getBalance a-  setHeaders-  addItemCount 1-  S.text $ cs $ show b.received--scottyBinfoSent :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoSent = do-  setMetrics (.binfoQgetsentbyaddress)-  a <- getAddress "addr"-  b <- fromMaybe (zeroBalance a) <$> getBalance a-  setHeaders-  addItemCount 1-  S.text $ cs $ show $ b.received - b.confirmed - b.unconfirmed--scottyBinfoAddrBalance :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoAddrBalance = do-  setMetrics (.binfoQaddressbalance)-  a <- getAddress "addr"-  b <- fromMaybe (zeroBalance a) <$> getBalance a-  setHeaders-  addItemCount 1-  S.text $ cs $ show $ b.confirmed + b.unconfirmed--scottyFirstSeen :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyFirstSeen = do-  setMetrics (.binfoQaddressfirstseen)-  a <- getAddress "addr"-  ch <- lift $ asks (.config.store.chain)-  bb <- chainGetBest ch-  let top = bb.height-      bot = 0-  i <- go ch bb a bot top-  setHeaders-  addItemCount 1-  S.text $ cs $ show i-  where-    go ch bb a bot top = do-      let mid = bot + (top - bot) `div` 2-          n = top - bot < 2-      x <- hasone a bot-      y <- hasone a mid-      z <- hasone a top-      if-          | x -> getblocktime ch bb bot-          | n -> getblocktime ch bb top-          | y -> go ch bb a bot mid-          | z -> go ch bb a mid top-          | otherwise -> return 0-    getblocktime ch bb h =-      (.header.timestamp) . fromJust <$> chainGetAncestor h bb ch-    hasone a h = do-      let l = Limits 1 0 (Just (AtBlock h))-      not . null <$> getAddressTxs a l--scottyShortBal :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyShortBal = do-  setMetrics (.binfoBalance)-  (xspecs, addrs) <- getBinfoActive-  cashaddr <- getCashAddr-  net <- lift $ asks (.config.store.net)-  abals <--    catMaybes-      <$> mapM-        (get_addr_balance net cashaddr)-        (HashSet.toList addrs)-  addItemCount (length abals)-  xbals <- mapM (get_xspec_balance net) (HashSet.toList xspecs)-  let res = HashMap.fromList (abals <> xbals)-  setHeaders-  streamEncoding $ toEncoding res-  where-    to_short_bal Balance {..} =-      BinfoShortBal-        { final = confirmed + unconfirmed,-          ntx = txs,-          received = received-        }-    get_addr_balance net cashaddr a =-      let net' =-            if-                | cashaddr -> net-                | net == bch -> btc-                | net == bchTest -> btcTest-                | net == bchTest4 -> btcTest-                | otherwise -> net-       in case addrToText net' a of-            Nothing -> return Nothing-            Just a' ->-              getBalance a >>= \case-                Nothing -> return $ Just (a', to_short_bal (zeroBalance a))-                Just b -> return $ Just (a', to_short_bal b)-    is_ext XPubBal {path = 0 : _} = True-    is_ext _ = False-    get_xspec_balance net xpub = do-      xbals <- xPubBals xpub-      xts <- xPubTxCount xpub xbals-      addItemCount (length xbals + 1)-      let val = sum $ map (.balance.confirmed) xbals-          zro = sum $ map (.balance.unconfirmed) xbals-          exs = filter is_ext xbals-          rcv = sum $ map (.balance.received) exs-          sbl =-            BinfoShortBal-              { final = val + zro,-                ntx = fromIntegral xts,-                received = rcv-              }-      ctx <- asks (.config.store.ctx)-      return (xPubExport net ctx xpub.key, sbl)--getBinfoHex :: (Monad m) => WebT m Bool-getBinfoHex =-  (== ("hex" :: Text))-    <$> S.param "format" `S.rescue` const (return "json")--scottyBinfoBlockHeight :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoBlockHeight = do-  numtxid <- getNumTxId-  height <- S.param "height"-  setMetrics (.binfoBlockHeight)-  block_hashes <- getBlocksAtHeight height-  block_headers <- catMaybes <$> mapM getBlock block_hashes-  addItemCount (length block_headers)-  next_block_hashes <- getBlocksAtHeight (height + 1)-  next_block_headers <- catMaybes <$> mapM getBlock next_block_hashes-  addItemCount (length next_block_headers)-  binfo_blocks <--    mapM (get_binfo_blocks numtxid next_block_headers) block_headers-  setHeaders-  net <- lift $ asks (.config.store.net)-  ctx <- lift $ asks (.config.store.ctx)-  streamEncoding $ marshalEncoding (net, ctx) binfo_blocks-  where-    get_tx th =-      withRunInIO $ \run ->-        unsafeInterleaveIO $-          run $-            fromJust <$> getTransaction th-    get_binfo_blocks numtxid next_block_headers block_header = do-      let my_hash = H.headerHash block_header.header-          get_prev = (.header.prev)-          get_hash = H.headerHash . (.header)-      txs <- lift $ mapM get_tx block_header.txs-      addItemCount (length txs)-      let next_blocks =-            map get_hash $-              filter-                ((== my_hash) . get_prev)-                next_block_headers-          binfo_txs = map (toBinfoTxSimple numtxid) txs-          binfo_block = toBinfoBlock block_header binfo_txs next_blocks-      return binfo_block--scottyBinfoLatest :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoLatest = do-  numtxid <- getNumTxId-  setMetrics (.binfoBlockLatest)-  best <- get_best_block-  addItemCount 1-  streamEncoding $-    toEncoding-      BinfoHeader-        { hash = H.headerHash best.header,-          timestamp = best.header.timestamp,-          index = best.height,-          height = best.height,-          txids = map (encodeBinfoTxId numtxid) best.txs-        }-  where-    get_best_block =-      getBestBlock >>= \case-        Nothing -> raise ThingNotFound-        Just bh ->-          getBlock bh >>= \case-            Nothing -> raise ThingNotFound-            Just b -> return b--scottyBinfoBlock :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoBlock = do-  numtxid <- getNumTxId-  hex <- getBinfoHex-  setMetrics (.binfoBlockRaw)-  S.param "block" >>= \case-    BinfoBlockHash bh -> go numtxid hex bh-    BinfoBlockIndex i ->-      getBlocksAtHeight i >>= \case-        [] -> raise ThingNotFound-        bh : _ -> go numtxid hex bh-  where-    get_tx th =-      withRunInIO $ \run ->-        unsafeInterleaveIO $-          run $-            fromJust <$> getTransaction th-    go numtxid hex bh =-      getBlock bh >>= \case-        Nothing -> raise ThingNotFound-        Just b -> do-          addItemCount 1-          txs <- lift $ mapM get_tx b.txs-          addItemCount (length txs)-          let my_hash = H.headerHash b.header-              get_prev = (.header.prev)-              get_hash = H.headerHash . (.header)-          nxt_headers <--            fmap catMaybes $-              mapM getBlock-                =<< getBlocksAtHeight (b.height + 1)-          addItemCount (length nxt_headers)-          let nxt =-                map get_hash $-                  filter-                    ((== my_hash) . get_prev)-                    nxt_headers-          if hex-            then do-              let x = H.Block b.header (map transactionData txs)-              setHeaders-              S.text . encodeHexLazy . runPutL $ serialize x-            else do-              let btxs = map (toBinfoTxSimple numtxid) txs-                  y = toBinfoBlock b btxs nxt-              setHeaders-              net <- lift $ asks (.config.store.net)-              ctx <- lift $ asks (.config.store.ctx)-              streamEncoding $ marshalEncoding (net, ctx) y--getBinfoTx ::-  (MonadLoggerIO m, MonadUnliftIO m) =>-  BinfoTxId ->-  WebT m (Either Except Transaction)-getBinfoTx txid = do-  tx <- case txid of-    BinfoTxIdHash h -> maybeToList <$> getTransaction h-    BinfoTxIdIndex i -> getNumTransaction i-  case tx of-    [t] -> return $ Right t-    [] -> return $ Left ThingNotFound-    ts ->-      let tids = map (txHash . transactionData) ts-       in return $ Left (TxIndexConflict tids)--scottyBinfoTx :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoTx = do-  numtxid <- getNumTxId-  hex <- getBinfoHex-  txid <- S.param "txid"-  setMetrics (.binfoTxRaw)-  tx <--    getBinfoTx txid >>= \case-      Right t -> return t-      Left e -> raise e-  addItemCount 1-  if hex then hx tx else js numtxid tx-  where-    js numtxid t = do-      net <- lift $ asks (.config.store.net)-      ctx <- lift $ asks (.config.store.ctx)-      setHeaders-      streamEncoding $ marshalEncoding (net, ctx) $ toBinfoTxSimple numtxid t-    hx t = do-      setHeaders-      S.text . encodeHexLazy . runPutL . serialize $ transactionData t--scottyBinfoTotalOut :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoTotalOut = do-  txid <- S.param "txid"-  setMetrics (.binfoQtxtotalbtcoutput)-  tx <--    getBinfoTx txid >>= \case-      Right t -> return t-      Left e -> raise e-  addItemCount 1-  S.text $ cs $ show $ sum $ map (.value) tx.outputs--scottyBinfoTxFees :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoTxFees = do-  txid <- S.param "txid"-  setMetrics (.binfoQtxfee)-  tx <--    getBinfoTx txid >>= \case-      Right t -> return t-      Left e -> raise e-  let i = sum $ map (.value) $ filter f $ tx.inputs-      o = sum $ map (.value) $ tx.outputs-  addItemCount 1-  S.text . cs . show $ i - o-  where-    f StoreInput {} = True-    f StoreCoinbase {} = False--scottyBinfoTxResult :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoTxResult = do-  txid <- S.param "txid"-  addr <- getAddress "addr"-  setMetrics (.binfoQtxresult)-  tx <--    getBinfoTx txid >>= \case-      Right t -> return t-      Left e -> raise e-  let i = toInteger $ sum $ map (.value) $ filter (f addr) $ tx.inputs-      o = toInteger $ sum $ map (.value) $ filter (g addr) $ tx.outputs-  addItemCount 1-  S.text $ cs $ show $ o - i-  where-    f addr StoreInput {address = Just a} = a == addr-    f _ _ = False-    g addr StoreOutput {address = Just a} = a == addr-    g _ _ = False--scottyBinfoTotalInput :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoTotalInput = do-  txid <- S.param "txid"-  setMetrics (.binfoQtxtotalbtcinput)-  tx <--    getBinfoTx txid >>= \case-      Right t -> return t-      Left e -> raise e-  addItemCount 1-  S.text $ cs $ show $ sum $ map (.value) $ filter f $ tx.inputs-  where-    f StoreInput {} = True-    f StoreCoinbase {} = False--scottyBinfoMempool :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoMempool = do-  setMetrics (.binfoMempool)-  numtxid <- getNumTxId-  offset <- getBinfoOffset-  n <- getBinfoCount "limit"-  mempool <- getMempool-  let txids = map snd $ take n $ drop offset mempool-  txs <- catMaybes <$> mapM getTransaction txids-  net <- lift $ asks (.config.store.net)-  setHeaders-  let mem = BinfoMempool $ map (toBinfoTxSimple numtxid) txs-  addItemCount (length txs)-  ctx <- lift $ asks (.config.store.ctx)-  streamEncoding $ marshalEncoding (net, ctx) mem--scottyBinfoGetBlockCount :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoGetBlockCount = do-  setMetrics (.binfoQgetblockcount)-  ch <- asks (.config.store.chain)-  bn <- chainGetBest ch-  setHeaders-  addItemCount 1-  S.text $ cs $ show $ bn.height--scottyBinfoLatestHash :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoLatestHash = do-  setMetrics (.binfoQlatesthash)-  ch <- asks (.config.store.chain)-  bn <- chainGetBest ch-  setHeaders-  addItemCount 1-  S.text $ TL.fromStrict $ H.blockHashToHex $ H.headerHash $ bn.header--scottyBinfoSubsidy :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoSubsidy = do-  setMetrics (.binfoQbcperblock)-  ch <- asks (.config.store.chain)-  net <- asks (.config.store.net)-  bn <- chainGetBest ch-  setHeaders-  addItemCount 1-  S.text $-    cs $-      show $-        (/ (100 * 1000 * 1000 :: Double)) $-          fromIntegral $-            H.computeSubsidy net (bn.height + 1)--scottyBinfoAddrToHash :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoAddrToHash = do-  setMetrics (.binfoQaddresstohash)-  addr <- getAddress "addr"-  setHeaders-  addItemCount 1-  S.text $ encodeHexLazy $ runPutL $ serialize $ addr.hash160--scottyBinfoHashToAddr :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoHashToAddr = do-  setMetrics (.binfoQhashtoaddress)-  bs <- maybe S.next return . decodeHex =<< S.param "hash"-  net <- asks (.config.store.net)-  hash <- either (const S.next) return (decode bs)-  addr <- maybe S.next return (addrToText net (PubKeyAddress hash))-  setHeaders-  addItemCount 1-  S.text $ TL.fromStrict addr--scottyBinfoAddrPubkey :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoAddrPubkey = do-  setMetrics (.binfoQaddrpubkey)-  hex <- S.param "pubkey"-  ctx <- lift $ asks (.config.store.ctx)-  pubkey <--    maybe S.next (return . pubKeyAddr ctx) $-      eitherToMaybe . unmarshal ctx =<< decodeHex hex-  net <- lift $ asks (.config.store.net)-  setHeaders-  case addrToText net pubkey of-    Nothing -> raise ThingNotFound-    Just a -> do-      addItemCount 1-      S.text $ TL.fromStrict a--scottyBinfoPubKeyAddr :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoPubKeyAddr = do-  setMetrics (.binfoQpubkeyaddr)-  addr <- getAddress "addr"-  mi <- strm addr-  i <- case mi of-    Nothing -> raise ThingNotFound-    Just i -> return i-  pk <- case extr addr i of-    Left e -> raise $ UserError e-    Right t -> return t-  setHeaders-  S.text $ encodeHexLazy $ L.fromStrict pk-  where-    strm addr = do-      counter <- getItemCounter-      runConduit $ do-        let f l = do-              ts <- getAddressTxs addr l-              counter (length ts)-              return ts-            l = let Limits {..} = def in Limits {limit = 8, ..}-        streamThings f (Just (.txid)) l-          .| concatMapMC (getTransaction . (.txid))-          .| iterMC (\_ -> counter 1)-          .| concatMapC (filter (inp addr) . (.inputs))-          .| headC-    inp addr StoreInput {address = Just a} = a == addr-    inp _ _ = False-    extr addr StoreInput {script, pkscript, witness} = do-      Script sig <- decode script-      Script pks <- decode pkscript-      case addr of-        PubKeyAddress {} ->-          case sig of-            [OP_PUSHDATA _ _, OP_PUSHDATA pub _] ->-              Right pub-            [OP_PUSHDATA _ _] ->-              case pks of-                [OP_PUSHDATA pub _, OP_CHECKSIG] ->-                  Right pub-                _ -> Left "Could not parse scriptPubKey"-            _ -> Left "Could not parse scriptSig"-        WitnessPubKeyAddress {} ->-          case witness of-            [_, pub] -> return pub-            _ -> Left "Could not parse scriptPubKey"-        _ -> Left "Address does not have public key"-    extr _ _ = Left "Incorrect input type"--scottyBinfoHashPubkey :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoHashPubkey = do-  setMetrics (.binfoQhashpubkey)-  ctx <- lift $ asks (.config.store.ctx)-  pkm <- (eitherToMaybe . unmarshal ctx <=< decodeHex) <$> S.param "pubkey"-  addr <- case pkm of-    Nothing -> raise $ UserError "Could not decode public key"-    Just pk -> return $ pubKeyAddr ctx pk-  setHeaders-  addItemCount 1-  S.text $ encodeHexLazy $ runPutL $ serialize $ addr.hash160---- GET Network Information ----scottyPeers ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetPeers ->-  WebT m [PeerInfo]-scottyPeers _ = do-  setMetrics (.peers)-  ps <- lift $ getPeersInformation =<< asks (.config.store.peerMgr)-  addItemCount (length ps)-  return ps---- | Obtain information about connected peers from peer manager process.-getPeersInformation ::-  (MonadLoggerIO m) => PeerMgr -> m [PeerInfo]-getPeersInformation mgr =-  mapMaybe toInfo <$> getPeers mgr-  where-    toInfo op = do-      ver <- op.version-      return-        PeerInfo-          { userAgent = ver.userAgent.get,-            address = show op.address,-            version = ver.version,-            services = ver.services,-            relay = ver.relay-          }--scottyHealth ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetHealth -> WebT m HealthCheck-scottyHealth _ = do-  setMetrics (.health)-  h <- asks (.health) >>= readTVarIO-  unless (isOK h) $ S.status status503-  addItemCount 1-  return h--blockHealthCheck ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>-  WebConfig ->-  m BlockHealth-blockHealthCheck cfg = do-  let ch = cfg.store.chain-  headers <- (.height) <$> chainGetBest ch-  blocks <--    maybe 0 (.height)-      <$> runMaybeT (MaybeT getBestBlock >>= MaybeT . getBlock)-  return-    BlockHealth-      { headers,-        blocks,-        max = fromIntegral cfg.maxLaggingBlocks-      }--lastBlockHealthCheck ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>-  Chain ->-  WebTimeouts ->-  m TimeHealth-lastBlockHealthCheck ch tos = do-  n <- fromIntegral . systemSeconds <$> liftIO getSystemTime-  t <- fromIntegral . (.header.timestamp) <$> chainGetBest ch-  return-    TimeHealth-      { age = n - t,-        max = fromIntegral tos.block-      }--lastTxHealthCheck ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>-  WebConfig ->-  m TimeHealth-lastTxHealthCheck WebConfig {..} = do-  n <- fromIntegral . systemSeconds <$> liftIO getSystemTime-  b <- fromIntegral . (.header.timestamp) <$> chainGetBest ch-  t <--    getMempool >>= \case-      t : _ ->-        let x = fromIntegral $ fst t-         in return $ max x b-      [] -> return b-  return-    TimeHealth-      { age = n - t,-        max = fromIntegral to-      }-  where-    ch = store.chain-    to =-      if noMempool-        then timeouts.block-        else timeouts.tx--pendingTxsHealthCheck ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>-  WebConfig ->-  m MaxHealth-pendingTxsHealthCheck cfg = do-  n <- blockStorePendingTxs cfg.store.block-  return-    MaxHealth-      { max = fromIntegral cfg.maxPendingTxs,-        count = fromIntegral n-      }--peerHealthCheck ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>-  WebConfig ->-  m CountHealth-peerHealthCheck cfg = do-  count <- fromIntegral . length <$> getPeers cfg.store.peerMgr-  return CountHealth {min = fromIntegral cfg.minPeers, count}--healthCheck ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>-  WebConfig ->-  m HealthCheck-healthCheck cfg = do-  blocks <- blockHealthCheck cfg-  lastBlock <- lastBlockHealthCheck cfg.store.chain cfg.timeouts-  lastTx <- lastTxHealthCheck cfg-  pendingTxs <- pendingTxsHealthCheck cfg-  peers <- peerHealthCheck cfg-  time <- round . utcTimeToPOSIXSeconds <$> liftIO getCurrentTime-  let check =-        HealthCheck-          { network = cfg.store.net.name,-            version = cfg.version,-            ..-          }-  unless (isOK check) $ do-    let t = toStrict $ encodeToLazyText check-    $(logErrorS) "Web" $ "Health check failed: " <> t-  return check--scottyDbStats :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyDbStats = do-  setMetrics (.db)-  setHeaders-  db <- lift $ asks (.config.store.db.db)-  statsM <- lift (getProperty db Stats)-  addItemCount 1-  S.text $ maybe "Could not get stats" cs statsM---------------------------- Parameter Parsing ------------------------------ | Returns @Nothing@ if the parameter is not supplied. Raises an exception on--- parse failure.-paramOptional :: (Param a, MonadIO m) => WebT m (Maybe a)-paramOptional = go Proxy-  where-    go :: (Param a, MonadIO m) => Proxy a -> WebT m (Maybe a)-    go proxy = do-      net <- lift $ asks (.config.store.net)-      ctx <- lift $ asks (.config.store.ctx)-      tsM :: Maybe [Text] <- p `S.rescue` const (return Nothing)-      case tsM of-        Nothing -> return Nothing -- Parameter was not supplied-        Just ts -> maybe (raise err) (return . Just) $ parseParam net ctx ts-      where-        l = proxyLabel proxy-        p = Just <$> S.param (cs l)-        err = UserError $ "Unable to parse param " <> cs l---- | Raises an exception if the parameter is not supplied-param :: (Param a, MonadIO m) => WebT m a-param = go Proxy-  where-    go :: (Param a, MonadIO m) => Proxy a -> WebT m a-    go proxy = do-      resM <- paramOptional-      case resM of-        Just res -> return res-        _ ->-          raise . UserError $-            "The param " <> cs (proxyLabel proxy) <> " was not defined"---- | Returns the default value of a parameter if it is not supplied. Raises an--- exception on parse failure.-paramDef :: (Default a, Param a, MonadIO m) => WebT m a-paramDef = fromMaybe def <$> paramOptional---- | Does not raise exceptions. Will call @Scotty.next@ if the parameter is--- not supplied or if parsing fails.-paramLazy :: (Param a, MonadIO m) => WebT m a-paramLazy = do-  resM <- paramOptional `S.rescue` const (return Nothing)-  maybe S.next return resM--parseBody :: (MonadIO m, Serial a) => WebT m a-parseBody = do-  b <- L.toStrict <$> S.body-  case hex b <> bin b of-    Left _ -> raise $ UserError "Failed to parse request body"-    Right x -> return x-  where-    bin = runGetS deserialize-    hex b =-      let ns = C.filter (not . isSpace) b-       in if isBase16 ns-            then bin . decodeBase16 $ assertBase16 ns-            else Left "Invalid hex input"--parseOffset :: (MonadIO m) => WebT m OffsetParam-parseOffset = do-  res@(OffsetParam o) <- paramDef-  limits <- lift $ asks (.config.limits)-  when (limits.maxOffset > 0 && fromIntegral o > limits.maxOffset) $-    raise . UserError $-      "offset exceeded: " <> show o <> " > " <> show limits.maxOffset-  return res--parseStart ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  Maybe StartParam ->-  WebT m (Maybe Start)-parseStart Nothing = return Nothing-parseStart (Just s) =-  runMaybeT $-    case s of-      StartParamHash {hash = h} -> start_tx h <|> start_block h-      StartParamHeight {height = h} -> start_height h-      StartParamTime {time = q} -> start_time q-  where-    start_height h = return $ AtBlock $ fromIntegral h-    start_block h = do-      b <- MaybeT $ getBlock (H.BlockHash h)-      return $ AtBlock b.height-    start_tx h = do-      _ <- MaybeT $ getTxData (TxHash h)-      return $ AtTx (TxHash h)-    start_time q = do-      ch <- lift $ asks (.config.store.chain)-      b <- MaybeT $ blockAtOrBefore ch q-      return $ AtBlock b.height--parseLimits :: (MonadIO m) => WebT m LimitsParam-parseLimits = LimitsParam <$> paramOptional <*> parseOffset <*> paramOptional--paramToLimits ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  Bool ->-  LimitsParam ->-  WebT m Limits-paramToLimits full (LimitsParam limitM o startM) = do-  wl <- lift $ asks (.config.limits)-  Limits (validateLimit wl full limitM) (fromIntegral o) <$> parseStart startM--validateLimit :: WebLimits -> Bool -> Maybe LimitParam -> Word32-validateLimit wl full limitM =-  f m $ maybe d (fromIntegral . (.get)) limitM-  where-    m-      | full && wl.maxFullItemCount > 0 = wl.maxFullItemCount-      | otherwise = wl.maxItemCount-    d = wl.defItemCount-    f a 0 = a-    f 0 b = b-    f a b = min a b-------------------- Utilities --------------------runInWebReader ::-  (MonadIO m) =>-  CacheT (DatabaseReaderT m) a ->-  ReaderT WebState m a-runInWebReader f = do-  bdb <- asks (.config.store.db)-  mc <- asks (.config.store.cache)-  lift $ runReaderT (withCache mc f) bdb--runNoCache :: (MonadIO m) => Bool -> ReaderT WebState m a -> ReaderT WebState m a-runNoCache False f = f-runNoCache True f = local g f-  where-    g s = s {config = h s.config}-    h c = c {store = i c.store}-    i s = s {cache = Nothing}--logIt ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  Maybe WebMetrics ->-  m Middleware-logIt metrics = do-  runner <- askRunInIO-  return $ \app req respond -> do-    var <- newTVarIO B.empty-    req' <--      let rb = req_body var (getRequestBodyChunk req)-          rq = req {requestBody = rb}-       in case metrics of-            Nothing -> return rq-            Just m -> do-              stat_var <- newTVarIO Nothing-              let vt =-                    V.insert m.key stat_var $-                      vault rq-              return rq {vault = vt}-    bracket start (end var runner req') $ \_ ->-      app req' $ \res -> do-        b <- readTVarIO var-        let s = responseStatus res-            msg = fmtReq b req' <> ": " <> fmtStatus s-        if statusIsSuccessful s-          then runner $ $(logDebugS) "Web" msg-          else runner $ $(logErrorS) "Web" msg-        respond res-  where-    start = systemToUTCTime <$> getSystemTime-    req_body var old_body = do-      b <- old_body-      unless (B.null b) . atomically $ modifyTVar var (<> b)-      return b-    add_stat d s = do-      addStatQuery s-      addStatTime s d-    end var runner req t1 = do-      t2 <- systemToUTCTime <$> getSystemTime-      let diff = round $ diffUTCTime t2 t1 * 1000-      case metrics of-        Nothing -> return ()-        Just m -> do-          let m_stat_var = V.lookup m.key (vault req)-          add_stat diff m.all-          case m_stat_var of-            Nothing -> return ()-            Just stat_var ->-              readTVarIO stat_var >>= \case-                Nothing -> return ()-                Just f -> add_stat diff (f m)-      when (diff > 10000) $ do-        b <- readTVarIO var-        runner $-          $(logWarnS) "Web" $-            "Slow [" <> cs (show diff) <> " ms]: " <> fmtReq b req--reqSizeLimit :: (Integral i) => i -> Middleware-reqSizeLimit i = requestSizeLimitMiddleware lim-  where-    max_len _req = return (Just (fromIntegral i))-    lim =-      setOnLengthExceeded too_big $-        setMaxLengthForRequest-          max_len-          defaultRequestSizeLimitSettings-    too_big _ _app _req send =-      send $-        waiExcept requestEntityTooLarge413 RequestTooLarge--reqTimeout :: (Integral i) => i -> Middleware-reqTimeout = timeoutAs res . fromIntegral-  where-    err = ServerTimeout-    res = responseLBS sta [hdr] (A.encode err)-    sta = errStatus err-    hdr = (hContentType, "application/json")--fmtReq :: ByteString -> Request -> Text-fmtReq bs req =-  let m = requestMethod req-      v = httpVersion req-      p = rawPathInfo req-      q = rawQueryString req-      txt = case T.decodeUtf8' bs of-        Left _ -> " {invalid utf8}"-        Right "" -> ""-        Right t -> " [" <> t <> "]"-   in T.decodeUtf8 (m <> " " <> p <> q <> " " <> cs (show v)) <> txt+    runWeb,+  )+where++import Conduit+  ( ConduitT,+    await,+    concatMapC,+    concatMapMC,+    dropC,+    dropWhileC,+    headC,+    mapC,+    runConduit,+    sinkList,+    takeC,+    takeWhileC,+    yield,+    (.|),+  )+import Control.Applicative ((<|>))+import Control.Lens ((.~), (^.))+import Control.Monad+  ( forM_,+    forever,+    unless,+    when,+    (<=<),+  )+import Control.Monad.Logger+  ( MonadLoggerIO,+    logDebugS,+    logErrorS,+  )+import Control.Monad.Reader+  ( ReaderT,+    asks,+    local,+    runReaderT,+  )+import Control.Monad.Trans (lift)+import Control.Monad.Trans.Control+  ( liftWith,+    restoreT,+  )+import Control.Monad.Trans.Maybe+  ( MaybeT (..),+    runMaybeT,+  )+import Data.Aeson+  ( Encoding,+    ToJSON (..),+    Value,+  )+import Data.Aeson qualified as A+import Data.Aeson.Encode.Pretty+  ( Config (..),+    defConfig,+    encodePretty',+  )+import Data.Aeson.Encoding+  ( encodingToLazyByteString,+    list,+  )+import Data.Aeson.Text (encodeToLazyText)+import Data.Base16.Types (assertBase16)+import Data.ByteString.Base16 (decodeBase16, isBase16)+import Data.ByteString.Builder (lazyByteString)+import Data.ByteString.Char8 qualified as C+import Data.ByteString.Lazy qualified as L+import Data.Bytes.Get+import Data.Bytes.Put+import Data.Bytes.Serial+import Data.Char (isSpace)+import Data.Default (Default (..))+import Data.Function ((&))+import Data.HashMap.Strict (HashMap)+import Data.HashMap.Strict qualified as HashMap+import Data.HashSet (HashSet)+import Data.HashSet qualified as HashSet+import Data.Int (Int64)+import Data.List (nub)+import Data.Maybe+  ( catMaybes,+    fromMaybe,+    isJust,+    mapMaybe,+    maybeToList,+  )+import Data.Proxy (Proxy (..))+import Data.Serialize (decode)+import Data.String (fromString)+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 (toStrict)+import Data.Text.Lazy qualified as TL+import Data.Time.Clock.POSIX (getPOSIXTime)+import Data.Word (Word32, Word64)+import Database.RocksDB+  ( Property (..),+    getProperty,+  )+import Haskoin.Address+import Haskoin.Block qualified as H+import Haskoin.Crypto.Keys+import Haskoin.Network+import Haskoin.Node+  ( Chain,+    OnlinePeer (..),+    PeerMgr,+    chainGetAncestor,+    chainGetBest,+    getPeers,+    sendMessage,+  )+import Haskoin.Script+import Haskoin.Store.BlockStore+import Haskoin.Store.Cache+import Haskoin.Store.Common+import Haskoin.Store.Data+import Haskoin.Store.Database.Reader+import Haskoin.Store.Manager+import Haskoin.Store.WebCommon+import Haskoin.Transaction+import Haskoin.Util+import NQE+  ( Inbox,+    receive,+    withSubscription,+  )+import Network.HTTP.Types+  ( Status (..),+    status400,+    status404,+    status409,+    status413,+    status500,+    status503,+    statusIsClientError,+    statusIsServerError,+    statusIsSuccessful,+  )+import Network.Wai+  ( Middleware,+    Request (..),+    responseStatus,+  )+import Network.Wai.Handler.Warp+  ( defaultSettings,+    setHost,+    setPort,+  )+import Network.Wai.Handler.WebSockets (websocketsOr)+import Network.WebSockets+  ( acceptRequest,+    defaultConnectionOptions,+    pendingRequest,+    rejectRequestWith,+    requestPath,+    sendTextData,+  )+import Network.WebSockets qualified as WebSockets+import Network.Wreq qualified as Wreq+import Network.Wreq.Session as Wreq (Session)+import Network.Wreq.Session qualified as Wreq.Session+import System.Metrics.StatsD+import UnliftIO+  ( MonadIO,+    MonadUnliftIO,+    TVar,+    askRunInIO,+    atomically,+    bracket,+    bracket_,+    handleAny,+    liftIO,+    newTVarIO,+    readTVarIO,+    timeout,+    withAsync,+    writeTVar,+  )+import UnliftIO.Concurrent (threadDelay)+import Web.Scotty.Internal.Types (ActionT)+import Web.Scotty.Trans qualified as S++type WebT m = ActionT Except (ReaderT WebState m)++data WebLimits = WebLimits+  { maxItemCount :: !Word32,+    maxFullItemCount :: !Word32,+    maxOffset :: !Word32,+    defItemCount :: !Word32,+    xpubGap :: !Word32,+    xpubGapInit :: !Word32,+    txTimeout :: !Word64,+    blockTimeout :: !Word64+  }+  deriving (Eq, Show)++instance Default WebLimits where+  def =+    WebLimits+      { maxItemCount = 200000,+        maxFullItemCount = 5000,+        maxOffset = 50000,+        defItemCount = 100,+        xpubGap = 32,+        xpubGapInit = 20,+        txTimeout = 3600 `div` 2,+        blockTimeout = 4 * 3600+      }++data WebConfig = WebConfig+  { host :: !String,+    port :: !Int,+    store :: !Store,+    maxLaggingBlocks :: !Int,+    maxPendingTxs :: !Int,+    minPeers :: !Int,+    limits :: !WebLimits,+    version :: !String,+    noMempool :: !Bool,+    stats :: !(Maybe Stats),+    tickerRefresh :: !Int,+    tickerURL :: !String,+    priceHistoryURL :: !String,+    noSlow :: !Bool,+    noBlockchainInfo :: !Bool,+    healthCheckInterval :: !Int+  }++data WebState = WebState+  { config :: !WebConfig,+    ticker :: !(TVar (HashMap Text BinfoTicker)),+    metrics :: !(Maybe WebMetrics),+    session :: !Wreq.Session,+    health :: !(TVar HealthCheck)+  }++data WebMetrics = WebMetrics+  { -- Addresses+    addressTx :: !StatTiming,+    addressTxFull :: !StatTiming,+    addressBalance :: !StatTiming,+    addressUnspent :: !StatTiming,+    xpub :: !StatTiming,+    xpubDelete :: !StatTiming,+    xpubTxFull :: !StatTiming,+    xpubTx :: !StatTiming,+    xpubBalance :: !StatTiming,+    xpubUnspent :: !StatTiming,+    -- Transactions+    tx :: !StatTiming,+    txRaw :: !StatTiming,+    txAfter :: !StatTiming,+    txBlock :: !StatTiming,+    txBlockRaw :: !StatTiming,+    txPost :: !StatTiming,+    mempool :: !StatTiming,+    -- Blocks+    block :: !StatTiming,+    blockRaw :: !StatTiming,+    -- Blockchain+    binfoMultiaddr :: !StatTiming,+    binfoBalance :: !StatTiming,+    binfoAddressRaw :: !StatTiming,+    binfoUnspent :: !StatTiming,+    binfoTxRaw :: !StatTiming,+    binfoBlock :: !StatTiming,+    binfoBlockHeight :: !StatTiming,+    binfoBlockLatest :: !StatTiming,+    binfoBlockRaw :: !StatTiming,+    binfoMempool :: !StatTiming,+    binfoExportHistory :: !StatTiming,+    -- Blockchain /q endpoints+    binfoQaddresstohash :: !StatTiming,+    binfoQhashtoaddress :: !StatTiming,+    binfoQaddrpubkey :: !StatTiming,+    binfoQpubkeyaddr :: !StatTiming,+    binfoQhashpubkey :: !StatTiming,+    binfoQgetblockcount :: !StatTiming,+    binfoQlatesthash :: !StatTiming,+    binfoQbcperblock :: !StatTiming,+    binfoQtxtotalbtcoutput :: !StatTiming,+    binfoQtxtotalbtcinput :: !StatTiming,+    binfoQtxfee :: !StatTiming,+    binfoQtxresult :: !StatTiming,+    binfoQgetreceivedbyaddress :: !StatTiming,+    binfoQgetsentbyaddress :: !StatTiming,+    binfoQaddressbalance :: !StatTiming,+    binfoQaddressfirstseen :: !StatTiming,+    -- Errors+    serverErrors :: StatCounter,+    clientErrors :: StatCounter,+    -- Others+    health :: !StatTiming,+    peers :: !StatTiming,+    db :: !StatTiming,+    events :: !StatGauge+  }++createMetrics :: (MonadIO m) => Stats -> m WebMetrics+createMetrics s = liftIO $ do+  -- Addresses+  addressTx <- d "address_transactions"+  addressTxFull <- d "address_transactions_full"+  addressBalance <- d "address_balance"+  addressUnspent <- d "address_unspent"+  xpub <- d "xpub"+  xpubDelete <- d "xpub_delete"+  xpubTxFull <- d "xpub_transactions_full"+  xpubTx <- d "xpub_transactions"+  xpubBalance <- d "xpub_balances"+  xpubUnspent <- d "xpub_unspent"++  -- Transactions+  tx <- d "transaction"+  txRaw <- d "transaction_raw"+  txAfter <- d "transaction_after"+  txPost <- d "transaction_post"+  txBlock <- d "transactions_block"+  txBlockRaw <- d "transactions_block_raw"+  mempool <- d "mempool"++  -- Blocks+  block <- d "block"+  blockRaw <- d "block_raw"++  -- Blockchain+  binfoMultiaddr <- d "blockchain_multiaddr"+  binfoBalance <- d "blockchain_balance"+  binfoAddressRaw <- d "blockchain_rawaddr"+  binfoUnspent <- d "blockchain_unspent"+  binfoTxRaw <- d "blockchain_rawtx"+  binfoBlock <- d "blockchain_blocks"+  binfoBlockHeight <- d "blockchain_block_height"+  binfoBlockLatest <- d "blockchain_latestblock"+  binfoBlockRaw <- d "blockchain_rawblock"+  binfoMempool <- d "blockchain_mempool"+  binfoExportHistory <- d "blockchain_export_history"++  -- Blockchain /q endpoints+  binfoQaddresstohash <- d "blockchain_q_addresstohash"+  binfoQhashtoaddress <- d "blockchain_q_hashtoaddress"+  binfoQaddrpubkey <- d "blockckhain_q_addrpubkey"+  binfoQpubkeyaddr <- d "blockchain_q_pubkeyaddr"+  binfoQhashpubkey <- d "blockchain_q_hashpubkey"+  binfoQgetblockcount <- d "blockchain_q_getblockcount"+  binfoQlatesthash <- d "blockchain_q_latesthash"+  binfoQbcperblock <- d "blockchain_q_bcperblock"+  binfoQtxtotalbtcoutput <- d "blockchain_q_txtotalbtcoutput"+  binfoQtxtotalbtcinput <- d "blockchain_q_txtotalbtcinput"+  binfoQtxfee <- d "blockchain_q_txfee"+  binfoQtxresult <- d "blockchain_q_txresult"+  binfoQgetreceivedbyaddress <- d "blockchain_q_getreceivedbyaddress"+  binfoQgetsentbyaddress <- d "blockchain_q_getsentbyaddress"+  binfoQaddressbalance <- d "blockchain_q_addressbalance"+  binfoQaddressfirstseen <- d "blockchain_q_addressfirstseen"++  -- Errors+  serverErrors <- c "server_errors"+  clientErrors <- c "client_errors"++  -- Others+  health <- d "health"+  peers <- d "peers"+  db <- d "dbstats"++  events <- g "events_connected"+  return WebMetrics {..}+  where+    d x = newStatTiming s ("web." <> x) 10+    g x = newStatGauge s ("web." <> x) 0+    c x = newStatCounter s ("web." <> x) 10++withGaugeIO :: (MonadUnliftIO m) => StatGauge -> m a -> m a+withGaugeIO g = bracket_ (incrementGauge g 1) (decrementGauge g 1)++withGaugeIncrease ::+  (MonadUnliftIO m) =>+  (WebMetrics -> StatGauge) ->+  WebT m a ->+  WebT m a+withGaugeIncrease gf go =+  lift (asks (.metrics)) >>= \case+    Nothing -> go+    Just m -> do+      s <- liftWith $ \run -> withGaugeIO (gf m) (run go)+      restoreT $ return s++withMetrics ::+  (MonadUnliftIO m) => (WebMetrics -> StatTiming) -> WebT m a -> WebT m a+withMetrics df go =+  asks (.metrics) >>= \case+    Nothing -> go+    Just m ->+      restoreT . return+        =<< liftWith (\run -> bracket time (stop m) (const (run go)))+  where+    time = round . (* 1000) <$> liftIO getPOSIXTime+    stop m t1 = do+      t2 <- time+      let ms = t2 - t1+      liftIO $ addTiming (df m) ms++data SerialAs = SerialAsBinary | SerialAsJSON | SerialAsPrettyJSON+  deriving (Eq, Show)++instance+  (MonadUnliftIO m) =>+  StoreReadBase (ReaderT WebState m)+  where+  getCtx = runInWebReader getCtx+  getNetwork = runInWebReader getNetwork+  getBestBlock = runInWebReader getBestBlock+  getBlocksAtHeight height = runInWebReader (getBlocksAtHeight height)+  getBlock bh = runInWebReader (getBlock bh)+  getTxData th = runInWebReader (getTxData th)+  getSpender op = runInWebReader (getSpender op)+  getUnspent op = runInWebReader (getUnspent op)+  getBalance a = runInWebReader (getBalance a)+  getMempool = runInWebReader getMempool++instance+  (MonadUnliftIO m, MonadLoggerIO m) =>+  StoreReadExtra (ReaderT WebState m)+  where+  getMaxGap = runInWebReader getMaxGap+  getInitialGap = runInWebReader getInitialGap+  getBalances as = runInWebReader (getBalances as)+  getAddressesTxs as = runInWebReader . getAddressesTxs as+  getAddressTxs a = runInWebReader . getAddressTxs a+  getAddressUnspents a = runInWebReader . getAddressUnspents a+  getAddressesUnspents as = runInWebReader . getAddressesUnspents as+  xPubBals = runInWebReader . xPubBals+  xPubUnspents xpub xbals = runInWebReader . xPubUnspents xpub xbals+  xPubTxs xpub xbals = runInWebReader . xPubTxs xpub xbals+  xPubTxCount xpub = runInWebReader . xPubTxCount xpub+  getNumTxData = runInWebReader . getNumTxData++instance (MonadUnliftIO m) => StoreReadBase (WebT m) where+  getCtx = lift getCtx+  getNetwork = lift getNetwork+  getBestBlock = lift getBestBlock+  getBlocksAtHeight = lift . getBlocksAtHeight+  getBlock = lift . getBlock+  getTxData = lift . getTxData+  getSpender = lift . getSpender+  getUnspent = lift . getUnspent+  getBalance = lift . getBalance+  getMempool = lift getMempool++instance (MonadUnliftIO m, MonadLoggerIO m) => StoreReadExtra (WebT m) where+  getBalances = lift . getBalances+  getAddressesTxs as = lift . getAddressesTxs as+  getAddressTxs a = lift . getAddressTxs a+  getAddressUnspents a = lift . getAddressUnspents a+  getAddressesUnspents as = lift . getAddressesUnspents as+  xPubBals = lift . xPubBals+  xPubUnspents xpub xbals = lift . xPubUnspents xpub xbals+  xPubTxs xpub xbals = lift . xPubTxs xpub xbals+  xPubTxCount xpub = lift . xPubTxCount xpub+  getMaxGap = lift getMaxGap+  getInitialGap = lift getInitialGap+  getNumTxData = lift . getNumTxData++-------------------+-- Path Handlers --+-------------------++runWeb :: (MonadUnliftIO m, MonadLoggerIO m) => WebConfig -> m ()+runWeb config = do+  ticker <- newTVarIO HashMap.empty+  metrics <- mapM createMetrics config.stats+  session <- liftIO Wreq.Session.newAPISession+  health <- runHealthCheck >>= newTVarIO+  let state = WebState {..}+  withAsync (priceUpdater session ticker) $ \_a1 ->+    withAsync (healthCheckLoop health) $ \_a2 -> do+      logger <- logIt+      runner <- askRunInIO+      S.scottyOptsT opts (runner . flip runReaderT state) $ do+        S.middleware $ webSocketEvents state+        S.middleware logger+        S.defaultHandler defHandler+        handlePaths config+        S.notFound $ raise ThingNotFound+  where+    priceUpdater session =+      unless (config.noSlow || config.noBlockchainInfo)+        . price+          config.store.net+          session+          config.tickerURL+          config.tickerRefresh+    runHealthCheck = runReaderT (healthCheck config) config.store.db+    healthCheckLoop v = forever $ do+      threadDelay (config.healthCheckInterval * 1000 * 1000)+      runHealthCheck >>= atomically . writeTVar v+    opts = def {S.settings = settings defaultSettings}+    settings = setPort config.port . setHost (fromString config.host)++getRates ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  Network ->+  Wreq.Session ->+  String ->+  Text ->+  [Word64] ->+  m [BinfoRate]+getRates net session url currency times = do+  handleAny err $ do+    r <-+      liftIO $+        Wreq.asJSON+          =<< Wreq.Session.postWith opts session url body+    return $ r ^. Wreq.responseBody+  where+    err _ = do+      $(logErrorS) "Web" "Could not get historic prices"+      return []+    body = toJSON times+    base =+      Wreq.defaults+        & Wreq.param "base" .~ [T.toUpper $ T.pack net.name]+    opts = base & Wreq.param "quote" .~ [currency]++price ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  Network ->+  Wreq.Session ->+  String ->+  Int ->+  TVar (HashMap Text BinfoTicker) ->+  m ()+price net session url pget v = forM_ purl $ \u -> forever $ do+  let err e = $(logErrorS) "Price" $ cs (show e)+  handleAny err $ do+    r <- liftIO $ Wreq.asJSON =<< Wreq.Session.get session u+    atomically . writeTVar v $ r ^. Wreq.responseBody+  threadDelay pget+  where+    purl = case code of+      Nothing -> Nothing+      Just x -> Just (url <> "?base=" <> x)+      where+        code+          | net == btc = Just "btc"+          | net == bch = Just "bch"+          | otherwise = Nothing++raise :: (MonadIO m) => Except -> WebT m a+raise err = do+  asks (.metrics) >>= mapM_ \m -> do+    let status = errStatus err+    liftIO $+      mapM_ (`incrementCounter` 1) $+        if+          | statusIsClientError status -> Just m.clientErrors+          | statusIsServerError status -> Just m.serverErrors+          | otherwise -> Nothing+  S.raise err++errStatus :: Except -> Status+errStatus ThingNotFound = status404+errStatus BadRequest = status400+errStatus UserError {} = status400+errStatus StringError {} = status400+errStatus ServerError = status500+errStatus TxIndexConflict {} = status409+errStatus ServerTimeout = status500+errStatus RequestTooLarge = status413++defHandler :: (Monad m) => Except -> WebT m ()+defHandler e = do+  setHeaders+  S.status $ errStatus e+  S.json e++handlePaths ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  WebConfig ->+  S.ScottyT Except (ReaderT WebState m) ()+handlePaths WebConfig {..} = do+  -- Block Paths+  pathCompact+    (GetBlock <$> paramLazy <*> paramDef)+    scottyBlock+    (marshalEncoding net)+    (marshalValue net)+  pathCompact+    (GetBlockBest <$> paramDef)+    scottyBlockBest+    (marshalEncoding net)+    (marshalValue net)+  pathCompact+    (GetBlockHeight <$> paramLazy <*> paramDef)+    (fmap SerialList . scottyBlockHeight)+    (list (marshalEncoding net) . (.get))+    (json_list (marshalValue net) . (.get))+  pathCompact+    (GetBlockTime <$> paramLazy <*> paramDef)+    scottyBlockTime+    (marshalEncoding net)+    (marshalValue net)+  pathCompact+    (GetBlockMTP <$> paramLazy <*> paramDef)+    scottyBlockMTP+    (marshalEncoding net)+    (marshalValue net)+  pathCompact+    (GetTx <$> paramLazy)+    scottyTx+    (marshalEncoding net)+    (marshalValue net)+  pathCompact+    (GetTxRaw <$> paramLazy)+    scottyTxRaw+    toEncoding+    toJSON+  pathCompact+    (PostTx <$> parseBody)+    scottyPostTx+    toEncoding+    toJSON+  pathCompact+    (GetMempool <$> paramOptional <*> parseOffset)+    (fmap SerialList . scottyMempool)+    toEncoding+    toJSON+  pathCompact+    (GetAddrTxs <$> paramLazy <*> parseLimits)+    (fmap SerialList . scottyAddrTxs)+    toEncoding+    toJSON+  pathCompact+    (GetAddrBalance <$> paramLazy)+    scottyAddrBalance+    (marshalEncoding net)+    (marshalValue net)+  pathCompact+    (GetAddrUnspent <$> paramLazy <*> parseLimits)+    (fmap SerialList . scottyAddrUnspent)+    (list (marshalEncoding net) . (.get))+    (json_list (marshalValue net) . (.get))+  pathCompact+    (GetPeers & return)+    (fmap SerialList . scottyPeers)+    toEncoding+    toJSON+  pathCompact+    (GetHealth & return)+    scottyHealth+    toEncoding+    toJSON+  S.get "/events" scottyEvents+  S.get "/dbstats" scottyDbStats+  unless noSlow $ do+    pathCompact+      (GetBlocks <$> param <*> paramDef)+      (fmap SerialList . scottyBlocks)+      (list (marshalEncoding net) . (.get))+      (json_list (marshalValue net) . (.get))+    pathCompact+      (GetBlockRaw <$> paramLazy)+      scottyBlockRaw+      toEncoding+      toJSON+    pathCompact+      (GetBlockBestRaw & return)+      scottyBlockBestRaw+      toEncoding+      toJSON+    pathCompact+      (GetBlockLatest <$> paramDef)+      (fmap SerialList . scottyBlockLatest)+      (list (marshalEncoding net) . (.get))+      (json_list (marshalValue net) . (.get))+    pathCompact+      (GetBlockHeights <$> param <*> paramDef)+      (fmap SerialList . scottyBlockHeights)+      (list (marshalEncoding net) . (.get))+      (json_list (marshalValue net) . (.get))+    pathCompact+      (GetBlockHeightRaw <$> paramLazy)+      scottyBlockHeightRaw+      toEncoding+      toJSON+    pathCompact+      (GetBlockTimeRaw <$> paramLazy)+      scottyBlockTimeRaw+      toEncoding+      toJSON+    pathCompact+      (GetBlockMTPRaw <$> paramLazy)+      scottyBlockMTPRaw+      toEncoding+      toJSON+    pathCompact+      (GetTxs <$> param)+      (fmap SerialList . scottyTxs)+      (list (marshalEncoding net) . (.get))+      (json_list (marshalValue net) . (.get))+    pathCompact+      (GetTxsRaw <$> param)+      scottyTxsRaw+      toEncoding+      toJSON+    pathCompact+      (GetTxsBlock <$> paramLazy)+      (fmap SerialList . scottyTxsBlock)+      (list (marshalEncoding net) . (.get))+      (json_list (marshalValue net) . (.get))+    pathCompact+      (GetTxsBlockRaw <$> paramLazy)+      scottyTxsBlockRaw+      toEncoding+      toJSON+    pathCompact+      (GetTxAfter <$> paramLazy <*> paramLazy)+      scottyTxAfter+      toEncoding+      toJSON+    pathCompact+      (GetAddrsTxs <$> param <*> parseLimits)+      (fmap SerialList . scottyAddrsTxs)+      toEncoding+      toJSON+    pathCompact+      (GetAddrTxsFull <$> paramLazy <*> parseLimits)+      (fmap SerialList . scottyAddrTxsFull)+      (list (marshalEncoding net) . (.get))+      (json_list (marshalValue net) . (.get))+    pathCompact+      (GetAddrsTxsFull <$> param <*> parseLimits)+      (fmap SerialList . scottyAddrsTxsFull)+      (list (marshalEncoding net) . (.get))+      (json_list (marshalValue net) . (.get))+    pathCompact+      (GetAddrsBalance <$> param)+      (fmap SerialList . scottyAddrsBalance)+      (list (marshalEncoding net) . (.get))+      (json_list (marshalValue net) . (.get))+    pathCompact+      (GetAddrsUnspent <$> param <*> parseLimits)+      (fmap SerialList . scottyAddrsUnspent)+      (list (marshalEncoding net) . (.get))+      (json_list (marshalValue net) . (.get))+    pathCompact+      (GetXPub <$> paramLazy <*> paramDef <*> paramDef)+      scottyXPub+      toEncoding+      toJSON+    pathCompact+      (GetXPubTxs <$> paramLazy <*> paramDef <*> parseLimits <*> paramDef)+      (fmap SerialList . scottyXPubTxs)+      toEncoding+      toJSON+    pathCompact+      (GetXPubTxsFull <$> paramLazy <*> paramDef <*> parseLimits <*> paramDef)+      (fmap SerialList . scottyXPubTxsFull)+      (list (marshalEncoding net) . (.get))+      (json_list (marshalValue net) . (.get))+    pathCompact+      (GetXPubBalances <$> paramLazy <*> paramDef <*> paramDef)+      (fmap SerialList . scottyXPubBalances)+      (list (marshalEncoding net) . (.get))+      (json_list (marshalValue net) . (.get))+    pathCompact+      (GetXPubUnspent <$> paramLazy <*> paramDef <*> parseLimits <*> paramDef)+      (fmap SerialList . scottyXPubUnspent)+      (list (marshalEncoding net) . (.get))+      (json_list (marshalValue net) . (.get))+    pathCompact+      (DelCachedXPub <$> paramLazy <*> paramDef)+      scottyDelXPub+      toEncoding+      toJSON+    unless noBlockchainInfo $ do+      S.post "/blockchain/multiaddr" scottyMultiAddr+      S.get "/blockchain/multiaddr" scottyMultiAddr+      S.get "/blockchain/balance" scottyShortBal+      S.post "/blockchain/balance" scottyShortBal+      S.get "/blockchain/rawaddr/:addr" scottyRawAddr+      S.get "/blockchain/address/:addr" scottyRawAddr+      S.get "/blockchain/xpub/:addr" scottyRawAddr+      S.post "/blockchain/unspent" scottyBinfoUnspent+      S.get "/blockchain/unspent" scottyBinfoUnspent+      S.get "/blockchain/rawtx/:txid" scottyBinfoTx+      S.get "/blockchain/rawblock/:block" scottyBinfoBlock+      S.get "/blockchain/latestblock" scottyBinfoLatest+      S.get "/blockchain/unconfirmed-transactions" scottyBinfoMempool+      S.get "/blockchain/block-height/:height" scottyBinfoBlockHeight+      S.get "/blockchain/blocks/:milliseconds" scottyBinfoBlocksDay+      S.get "/blockchain/export-history" scottyBinfoHistory+      S.post "/blockchain/export-history" scottyBinfoHistory+      S.get "/blockchain/q/addresstohash/:addr" scottyBinfoAddrToHash+      S.get "/blockchain/q/hashtoaddress/:hash" scottyBinfoHashToAddr+      S.get "/blockchain/q/addrpubkey/:pubkey" scottyBinfoAddrPubkey+      S.get "/blockchain/q/pubkeyaddr/:addr" scottyBinfoPubKeyAddr+      S.get "/blockchain/q/hashpubkey/:pubkey" scottyBinfoHashPubkey+      S.get "/blockchain/q/getblockcount" scottyBinfoGetBlockCount+      S.get "/blockchain/q/latesthash" scottyBinfoLatestHash+      S.get "/blockchain/q/bcperblock" scottyBinfoSubsidy+      S.get "/blockchain/q/txtotalbtcoutput/:txid" scottyBinfoTotalOut+      S.get "/blockchain/q/txtotalbtcinput/:txid" scottyBinfoTotalInput+      S.get "/blockchain/q/txfee/:txid" scottyBinfoTxFees+      S.get "/blockchain/q/txresult/:txid/:addr" scottyBinfoTxResult+      S.get "/blockchain/q/getreceivedbyaddress/:addr" scottyBinfoReceived+      S.get "/blockchain/q/getsentbyaddress/:addr" scottyBinfoSent+      S.get "/blockchain/q/addressbalance/:addr" scottyBinfoAddrBalance+      S.get "/blockchain/q/addressfirstseen/:addr" scottyFirstSeen+  where+    json_list f = toJSONList . map f+    net = store.net++pathCompact ::+  (ApiResource a b, MonadIO m) =>+  WebT m a ->+  (a -> WebT m b) ->+  (b -> Encoding) ->+  (b -> Value) ->+  S.ScottyT Except (ReaderT WebState m) ()+pathCompact parser action encJson encValue =+  pathCommon parser action encJson encValue False++pathCommon ::+  (ApiResource a b, MonadIO m) =>+  WebT m a ->+  (a -> WebT m b) ->+  (b -> Encoding) ->+  (b -> Value) ->+  Bool ->+  S.ScottyT Except (ReaderT WebState m) ()+pathCommon parser action encJson encValue pretty =+  S.addroute (resourceMethod proxy) (capturePath proxy) $ do+    setHeaders+    proto <- setupContentType pretty+    apiRes <- parser+    res <- action apiRes+    S.raw $ protoSerial proto encJson encValue res+  where+    toProxy :: WebT m a -> Proxy a+    toProxy = const Proxy+    proxy = toProxy parser++streamEncoding :: (Monad m) => Encoding -> WebT m ()+streamEncoding e = do+  S.setHeader "Content-Type" "application/json; charset=utf-8"+  S.raw (encodingToLazyByteString e)++protoSerial ::+  (Serial a) =>+  SerialAs ->+  (a -> Encoding) ->+  (a -> Value) ->+  a ->+  L.ByteString+protoSerial SerialAsBinary _ _ = runPutL . serialize+protoSerial SerialAsJSON f _ = encodingToLazyByteString . f+protoSerial SerialAsPrettyJSON _ g =+  encodePretty' defConfig {confTrailingNewline = True} . g++setHeaders :: (Monad m) => ActionT e m ()+setHeaders = S.setHeader "Access-Control-Allow-Origin" "*"++setupJSON :: (Monad m) => Bool -> ActionT Except m SerialAs+setupJSON pretty = do+  S.setHeader "Content-Type" "application/json"+  p <- S.param "pretty" `S.rescue` const (return pretty)+  return $ if p then SerialAsPrettyJSON else SerialAsJSON++setupBinary :: (Monad m) => ActionT Except m SerialAs+setupBinary = do+  S.setHeader "Content-Type" "application/octet-stream"+  return SerialAsBinary++setupContentType :: (Monad m) => Bool -> ActionT Except m SerialAs+setupContentType pretty = do+  accept <- S.header "accept"+  maybe (setupJSON pretty) setType accept+  where+    setType "application/octet-stream" = setupBinary+    setType _ = setupJSON pretty++-- GET Block / GET Blocks --++scottyBlock ::+  (MonadUnliftIO m) => GetBlock -> WebT m BlockData+scottyBlock (GetBlock h (NoTx noTx)) = withMetrics (.block) $ do+  getBlock h >>= \case+    Nothing ->+      raise ThingNotFound+    Just b -> do+      return $ pruneTx noTx b++getBlocks ::+  (MonadUnliftIO m) =>+  [H.BlockHash] ->+  Bool ->+  WebT m [BlockData]+getBlocks hs notx =+  (pruneTx notx <$>) . catMaybes <$> mapM getBlock (nub hs)++scottyBlocks ::+  (MonadUnliftIO m) => GetBlocks -> WebT m [BlockData]+scottyBlocks (GetBlocks hs (NoTx notx)) =+  withMetrics (.block) $+    getBlocks hs notx++pruneTx :: Bool -> BlockData -> BlockData+pruneTx False b = b+pruneTx True BlockData {..} = BlockData {txs = take 1 txs, ..}++-- GET BlockRaw --++scottyBlockRaw ::+  (MonadUnliftIO m) =>+  GetBlockRaw ->+  WebT m (RawResult H.Block)+scottyBlockRaw (GetBlockRaw h) =+  withMetrics (.blockRaw) $+    RawResult <$> getRawBlock h++getRawBlock ::+  (MonadUnliftIO m) =>+  H.BlockHash ->+  WebT m H.Block+getRawBlock h =+  getBlock h >>= maybe (raise ThingNotFound) return >>= lift . toRawBlock++toRawBlock :: (StoreReadBase m) => BlockData -> m H.Block+toRawBlock b = do+  txs <- map transactionData . catMaybes <$> mapM getTransaction b.txs+  return H.Block {header = b.header, txs}++-- GET BlockBest / BlockBestRaw --++scottyBlockBest ::+  (MonadUnliftIO m) => GetBlockBest -> WebT m BlockData+scottyBlockBest (GetBlockBest (NoTx notx)) =+  withMetrics (.block) $+    maybe (raise ThingNotFound) (return . pruneTx notx) <=< runMaybeT $+      MaybeT getBestBlock >>= MaybeT . getBlock++scottyBlockBestRaw ::+  (MonadUnliftIO m) =>+  GetBlockBestRaw ->+  WebT m (RawResult H.Block)+scottyBlockBestRaw _ =+  withMetrics (.blockRaw) $+    maybe (raise ThingNotFound) (return . RawResult) <=< runMaybeT $+      MaybeT getBestBlock >>= lift . getRawBlock++-- GET BlockLatest --++scottyBlockLatest ::+  (MonadUnliftIO m) =>+  GetBlockLatest ->+  WebT m [BlockData]+scottyBlockLatest (GetBlockLatest (NoTx noTx)) =+  withMetrics (.block) $+    getBestBlock >>= maybe (raise ThingNotFound) (go [] <=< getBlock)+  where+    go acc Nothing = return $ reverse acc+    go acc (Just b)+      | b.height <= 0 = return $ reverse acc+      | length acc == 99 = return . reverse $ pruneTx noTx b : acc+      | otherwise = go (pruneTx noTx b : acc) =<< getBlock b.header.prev++-- GET BlockHeight / BlockHeights / BlockHeightRaw --++scottyBlockHeight ::+  (MonadUnliftIO m) => GetBlockHeight -> WebT m [BlockData]+scottyBlockHeight (GetBlockHeight h (NoTx notx)) =+  withMetrics (.block) $+    getBlocksAtHeight (fromIntegral h)+      >>= (`getBlocks` notx)++scottyBlockHeights ::+  (MonadUnliftIO m) =>+  GetBlockHeights ->+  WebT m [BlockData]+scottyBlockHeights (GetBlockHeights (HeightsParam heights) (NoTx notx)) =+  withMetrics (.block) $+    mapM (getBlocksAtHeight . fromIntegral) heights+      >>= (`getBlocks` notx) . concat++scottyBlockHeightRaw ::+  (MonadUnliftIO m) =>+  GetBlockHeightRaw ->+  WebT m (RawResultList H.Block)+scottyBlockHeightRaw (GetBlockHeightRaw h) =+  withMetrics (.blockRaw) $+    fmap RawResultList $+      mapM getRawBlock =<< getBlocksAtHeight (fromIntegral h)++-- GET BlockTime / BlockTimeRaw --++scottyBlockTime ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  GetBlockTime ->+  WebT m BlockData+scottyBlockTime (GetBlockTime (TimeParam t) (NoTx notx)) =+  withMetrics (.block) $ do+    ch <- asks (.config.store.chain)+    blockAtOrBefore ch t >>= \case+      Nothing -> raise ThingNotFound+      Just b -> return $ pruneTx notx b++scottyBlockMTP ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  GetBlockMTP ->+  WebT m BlockData+scottyBlockMTP (GetBlockMTP (TimeParam t) (NoTx notx)) =+  withMetrics (.block) $ do+    ch <- asks (.config.store.chain)+    blockAtOrAfterMTP ch t >>= \case+      Nothing -> raise ThingNotFound+      Just b -> return $ pruneTx notx b++scottyBlockTimeRaw ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  GetBlockTimeRaw ->+  WebT m (RawResult H.Block)+scottyBlockTimeRaw (GetBlockTimeRaw (TimeParam t)) =+  withMetrics (.blockRaw) $ do+    ch <- asks (.config.store.chain)+    blockAtOrBefore ch t >>= \case+      Nothing -> raise ThingNotFound+      Just b -> RawResult <$> lift (toRawBlock b)++scottyBlockMTPRaw ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  GetBlockMTPRaw ->+  WebT m (RawResult H.Block)+scottyBlockMTPRaw (GetBlockMTPRaw (TimeParam t)) =+  withMetrics (.blockRaw) $ do+    ch <- asks (.config.store.chain)+    blockAtOrAfterMTP ch t >>= \case+      Nothing -> raise ThingNotFound+      Just b -> RawResult <$> lift (toRawBlock b)++-- GET Transactions --++scottyTx :: (MonadUnliftIO m) => GetTx -> WebT m Transaction+scottyTx (GetTx txid) =+  withMetrics (.tx) $+    getTransaction txid >>= maybe (raise ThingNotFound) return++scottyTxs ::+  (MonadUnliftIO m) => GetTxs -> WebT m [Transaction]+scottyTxs (GetTxs txids) =+  withMetrics (.tx) $+    catMaybes <$> mapM getTransaction (nub txids)++scottyTxRaw ::+  (MonadUnliftIO m) => GetTxRaw -> WebT m (RawResult Tx)+scottyTxRaw (GetTxRaw txid) =+  withMetrics (.txRaw) $+    getTransaction txid+      >>= \case+        Nothing -> raise ThingNotFound+        Just tx -> return $ RawResult (transactionData tx)++scottyTxsRaw ::+  (MonadUnliftIO m) =>+  GetTxsRaw ->+  WebT m (RawResultList Tx)+scottyTxsRaw (GetTxsRaw txids) =+  withMetrics (.txRaw) $+    RawResultList . map transactionData . catMaybes+      <$> mapM getTransaction (nub txids)++getTxsBlock ::+  (MonadUnliftIO m) =>+  H.BlockHash ->+  WebT m [Transaction]+getTxsBlock h =+  getBlock h >>= \case+    Nothing -> raise ThingNotFound+    Just b -> catMaybes <$> mapM getTransaction b.txs++scottyTxsBlock ::+  (MonadUnliftIO m) =>+  GetTxsBlock ->+  WebT m [Transaction]+scottyTxsBlock (GetTxsBlock h) =+  withMetrics (.txBlock) $ getTxsBlock h++scottyTxsBlockRaw ::+  (MonadUnliftIO m) =>+  GetTxsBlockRaw ->+  WebT m (RawResultList Tx)+scottyTxsBlockRaw (GetTxsBlockRaw h) =+  withMetrics (.txBlockRaw) $+    RawResultList . map transactionData <$> getTxsBlock h++-- GET TransactionAfterHeight --++scottyTxAfter ::+  (MonadUnliftIO m) =>+  GetTxAfter ->+  WebT m (GenericResult (Maybe Bool))+scottyTxAfter (GetTxAfter txid height) =+  withMetrics (.txAfter) $+    GenericResult . fst <$> cbAfterHeight (fromIntegral height) txid++-- | Check if any of the ancestors of this transaction is a coinbase after the+-- specified height. Returns 'Nothing' if answer cannot be computed before+-- hitting limits.+cbAfterHeight ::+  (StoreReadBase m) =>+  H.BlockHeight ->+  TxHash ->+  m (Maybe Bool, Int)+cbAfterHeight height txid =+  inputs n HashSet.empty HashSet.empty [txid]+  where+    n = 10000+    inputs 0 _ _ [] = return (Nothing, 10000)+    inputs i is ns [] =+      let is' = HashSet.union is ns+          ns' = HashSet.empty+          ts = HashSet.toList (HashSet.difference ns is)+       in case ts of+            [] -> return (Just False, n - i)+            _ -> inputs i is' ns' ts+    inputs i is ns (t : ts) =+      getTransaction t >>= \case+        Nothing -> return (Nothing, n - i)+        Just tx+          | height_check tx ->+              if cb_check tx+                then return (Just True, n - i + 1)+                else+                  let ns' = HashSet.union (ins tx) ns+                   in inputs (i - 1) is ns' ts+          | otherwise -> inputs (i - 1) is ns ts+    cb_check = any isCoinbase . (.inputs)+    ins = HashSet.fromList . map (.outpoint.hash) . (.inputs)+    height_check tx =+      case tx.block of+        BlockRef h _ -> h > height+        MemRef _ -> True++-- POST Transaction --++scottyPostTx :: (MonadUnliftIO m) => PostTx -> WebT m TxId+scottyPostTx (PostTx tx) =+  withMetrics (.txPost) $ do+    cfg <- asks (.config)+    lift (publishTx cfg tx) >>= \case+      Right () -> return (TxId (txHash tx))+      Left e@(PubReject _) -> raise $ UserError (show e)+      _ -> raise ServerError++-- | Publish a new transaction to the network.+publishTx ::+  (MonadUnliftIO m, StoreReadBase m) =>+  WebConfig ->+  Tx ->+  m (Either PubExcept ())+publishTx cfg tx =+  withSubscription pub $ \s ->+    getTransaction (txHash tx) >>= \case+      Just _ -> return $ Right ()+      Nothing -> go s+  where+    pub = cfg.store.pub+    mgr = cfg.store.peerMgr+    net = cfg.store.net+    go s =+      getPeers mgr >>= \case+        [] -> return $ Left PubNoPeers+        OnlinePeer {mailbox = p} : _ -> do+          MTx tx `sendMessage` p+          let v =+                if net.segWit+                  then InvWitnessTx+                  else InvTx+          sendMessage+            (MGetData (GetData [InvVector v (txHash tx).get]))+            p+          f p s+    t = 5 * 1000 * 1000+    f p s+      | cfg.noMempool = return $ Right ()+      | otherwise =+          liftIO (UnliftIO.timeout t (g p s)) >>= \case+            Nothing -> return $ Left PubTimeout+            Just (Left e) -> return $ Left e+            Just (Right ()) -> return $ Right ()+    g p s =+      receive s >>= \case+        StoreTxReject p' h' c _+          | p == p' && h' == txHash tx -> return . Left $ PubReject c+        StorePeerDisconnected p'+          | p == p' -> return $ Left PubPeerDisconnected+        StoreMempoolNew h'+          | h' == txHash tx -> return $ Right ()+        _ -> g p s++-- GET Mempool / Events --++scottyMempool ::+  (MonadUnliftIO m) => GetMempool -> WebT m [TxHash]+scottyMempool (GetMempool limitM (OffsetParam o)) =+  withMetrics (.mempool) $ do+    WebLimits {..} <- asks (.config.limits)+    let wl' = WebLimits {maxItemCount = 0, ..}+        l = Limits (validateLimit wl' False limitM) (fromIntegral o) Nothing+    map snd . applyLimits l <$> getMempool++webSocketEvents :: WebState -> Middleware+webSocketEvents s =+  websocketsOr defaultConnectionOptions (wrap . events)+  where+    pub = s.config.store.pub+    gauge = (.events) <$> s.metrics+    wrap f = case gauge of+      Nothing -> f+      Just g -> withGaugeIO g f+    events pending = withSubscription pub $ \sub -> do+      let path = requestPath $ pendingRequest pending+      if path == "/events"+        then do+          conn <- acceptRequest pending+          forever $+            receiveEvent sub >>= \case+              Nothing -> return ()+              Just event -> sendTextData conn (A.encode event)+        else+          rejectRequestWith+            pending+            WebSockets.defaultRejectRequest+              { WebSockets.rejectBody =+                  L.toStrict $ A.encode ThingNotFound,+                WebSockets.rejectCode =+                  404,+                WebSockets.rejectMessage =+                  "Not Found",+                WebSockets.rejectHeaders =+                  [("Content-Type", "application/json")]+              }++scottyEvents :: (MonadUnliftIO m) => WebT m ()+scottyEvents =+  withGaugeIncrease (.events) $ do+    setHeaders+    proto <- setupContentType False+    pub <- lift $ asks (.config.store.pub)+    S.stream $ \io flush' ->+      withSubscription pub $ \sub ->+        forever $ do+          flush'+          receiveEvent sub >>= \case+            Nothing -> return ()+            Just msg -> io (serial proto msg)+  where+    serial proto e =+      lazyByteString $+        protoSerial proto toEncoding toJSON e <> newLine proto+    newLine SerialAsBinary = mempty+    newLine SerialAsJSON = "\n"+    newLine SerialAsPrettyJSON = mempty++receiveEvent :: Inbox StoreEvent -> IO (Maybe Event)+receiveEvent sub =+  go <$> receive sub+  where+    go = \case+      StoreBestBlock b -> Just (EventBlock b)+      StoreMempoolNew t -> Just (EventTx t)+      StoreMempoolDelete t -> Just (EventTx t)+      _ -> Nothing++-- GET Address Transactions --++scottyAddrTxs ::+  (MonadUnliftIO m, MonadLoggerIO m) => GetAddrTxs -> WebT m [TxRef]+scottyAddrTxs (GetAddrTxs addr pLimits) =+  withMetrics (.addressTx) $+    getAddressTxs addr =<< paramToLimits False pLimits++scottyAddrsTxs ::+  (MonadUnliftIO m, MonadLoggerIO m) => GetAddrsTxs -> WebT m [TxRef]+scottyAddrsTxs (GetAddrsTxs addrs pLimits) =+  withMetrics (.addressTx) $+    getAddressesTxs addrs =<< paramToLimits False pLimits++scottyAddrTxsFull ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  GetAddrTxsFull ->+  WebT m [Transaction]+scottyAddrTxsFull (GetAddrTxsFull addr pLimits) =+  withMetrics (.addressTxFull) $ do+    txs <- getAddressTxs addr =<< paramToLimits True pLimits+    catMaybes <$> mapM (getTransaction . (.txid)) txs++scottyAddrsTxsFull ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  GetAddrsTxsFull ->+  WebT m [Transaction]+scottyAddrsTxsFull (GetAddrsTxsFull addrs pLimits) =+  withMetrics (.addressTxFull) $ do+    txs <- getAddressesTxs addrs =<< paramToLimits True pLimits+    catMaybes <$> mapM (getTransaction . (.txid)) txs++scottyAddrBalance :: (MonadUnliftIO m) => GetAddrBalance -> WebT m Balance+scottyAddrBalance (GetAddrBalance addr) =+  withMetrics (.addressBalance) $ getDefaultBalance addr++scottyAddrsBalance ::+  (MonadUnliftIO m, MonadLoggerIO m) => GetAddrsBalance -> WebT m [Balance]+scottyAddrsBalance (GetAddrsBalance addrs) =+  withMetrics (.addressBalance) $ getBalances addrs++scottyAddrUnspent ::+  (MonadUnliftIO m, MonadLoggerIO m) => GetAddrUnspent -> WebT m [Unspent]+scottyAddrUnspent (GetAddrUnspent addr pLimits) =+  withMetrics (.addressUnspent) $+    getAddressUnspents addr =<< paramToLimits False pLimits++scottyAddrsUnspent ::+  (MonadUnliftIO m, MonadLoggerIO m) => GetAddrsUnspent -> WebT m [Unspent]+scottyAddrsUnspent (GetAddrsUnspent addrs pLimits) =+  withMetrics (.addressUnspent) $+    getAddressesUnspents addrs =<< paramToLimits False pLimits++-- GET XPubs --++scottyXPub ::+  (MonadUnliftIO m, MonadLoggerIO m) => GetXPub -> WebT m XPubSummary+scottyXPub (GetXPub xpub deriv (NoCache noCache)) =+  withMetrics (.xpub) $+    let xs = XPubSpec xpub deriv+     in xPubSummary xs <$> lift (runNoCache noCache $ xPubBals xs)++scottyDelXPub ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  DelCachedXPub ->+  WebT m (GenericResult Bool)+scottyDelXPub (DelCachedXPub xpub deriv) =+  withMetrics (.xpubDelete) $ do+    c <- asks (.config.store.cache)+    n <- lift (withCache c $ cacheDelXPubs [XPubSpec xpub deriv])+    return $ GenericResult (n > 0)++getXPubTxs ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  XPubKey ->+  DeriveType ->+  LimitsParam ->+  Bool ->+  WebT m [TxRef]+getXPubTxs xpub deriv plimits nocache = do+  limits <- paramToLimits False plimits+  let xs = XPubSpec xpub deriv+  xbals <- xPubBals xs+  lift $ runNoCache nocache $ xPubTxs xs xbals limits++scottyXPubTxs ::+  (MonadUnliftIO m, MonadLoggerIO m) => GetXPubTxs -> WebT m [TxRef]+scottyXPubTxs (GetXPubTxs xpub deriv plimits (NoCache nocache)) =+  withMetrics (.xpubTx) $+    getXPubTxs xpub deriv plimits nocache++scottyXPubTxsFull ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  GetXPubTxsFull ->+  WebT m [Transaction]+scottyXPubTxsFull (GetXPubTxsFull xpub deriv plimits (NoCache nocache)) =+  withMetrics (.xpubTxFull) $+    fmap catMaybes $+      lift . runNoCache nocache . mapM (getTransaction . (.txid))+        =<< getXPubTxs xpub deriv plimits nocache++scottyXPubBalances ::+  (MonadUnliftIO m, MonadLoggerIO m) => GetXPubBalances -> WebT m [XPubBal]+scottyXPubBalances (GetXPubBalances xpub deriv (NoCache noCache)) =+  withMetrics (.xpubBalance) $+    lift $+      runNoCache noCache $+        xPubBals (XPubSpec xpub deriv)++scottyXPubUnspent ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  GetXPubUnspent ->+  WebT m [XPubUnspent]+scottyXPubUnspent (GetXPubUnspent xpub deriv pLimits (NoCache noCache)) =+  withMetrics (.xpubUnspent) $ do+    limits <- paramToLimits False pLimits+    let xspec = XPubSpec xpub deriv+    xbals <- xPubBals xspec+    lift $ runNoCache noCache $ xPubUnspents xspec xbals limits++---------------------------------------+-- Blockchain.info API Compatibility --+---------------------------------------++netBinfoSymbol :: Network -> BinfoSymbol+netBinfoSymbol net+  | net == btc =+      BinfoSymbol+        { code = "BTC",+          symbol = "BTC",+          name = "Bitcoin",+          conversion = 100 * 1000 * 1000,+          after = True,+          local = False+        }+  | net == bch =+      BinfoSymbol+        { code = "BCH",+          symbol = "BCH",+          name = "Bitcoin Cash",+          conversion = 100 * 1000 * 1000,+          after = True,+          local = False+        }+  | otherwise =+      BinfoSymbol+        { code = "XTS",+          symbol = "¤",+          name = "Test",+          conversion = 100 * 1000 * 1000,+          after = False,+          local = False+        }++binfoTickerToSymbol :: Text -> BinfoTicker -> BinfoSymbol+binfoTickerToSymbol code BinfoTicker {..} =+  BinfoSymbol+    { code,+      symbol,+      name,+      conversion = 100 * 1000 * 1000 / fifteen, -- sat/usd+      after = False,+      local = True+    }+  where+    name = case code of+      "EUR" -> "Euro"+      "USD" -> "U.S. dollar"+      "GBP" -> "British pound"+      x -> x++getBinfoAddrsParam ::+  (MonadIO m) =>+  Text ->+  WebT m (HashSet BinfoAddr)+getBinfoAddrsParam name = do+  net <- asks (.config.store.net)+  ctx <- asks (.config.store.ctx)+  p <- S.param (cs name) `S.rescue` const (return "")+  if T.null p+    then return HashSet.empty+    else case parseBinfoAddr net ctx p of+      Nothing -> raise (UserError "invalid address")+      Just xs -> return $ HashSet.fromList xs++getBinfoActive ::+  (MonadIO m) =>+  WebT m (HashSet XPubSpec, HashSet Address)+getBinfoActive = do+  active <- getBinfoAddrsParam "active"+  p2sh <- getBinfoAddrsParam "activeP2SH"+  bech32 <- getBinfoAddrsParam "activeBech32"+  let xspec d b = (`XPubSpec` d) <$> xpub b+      xspecs =+        HashSet.fromList $+          concat+            [ mapMaybe (xspec DeriveNormal) (HashSet.toList active),+              mapMaybe (xspec DeriveP2SH) (HashSet.toList p2sh),+              mapMaybe (xspec DeriveP2WPKH) (HashSet.toList bech32)+            ]+      addrs = HashSet.fromList $ mapMaybe addr $ HashSet.toList active+  return (xspecs, addrs)+  where+    addr (BinfoAddr a) = Just a+    addr (BinfoXpub _) = Nothing+    xpub (BinfoXpub x) = Just x+    xpub (BinfoAddr _) = Nothing++getNumTxId :: (MonadIO m) => WebT m Bool+getNumTxId = fmap not $ S.param "txidindex" `S.rescue` const (return False)++getChainHeight :: (MonadUnliftIO m) => WebT m H.BlockHeight+getChainHeight = fmap (.height) $ chainGetBest =<< asks (.config.store.chain)++scottyBinfoUnspent :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()+scottyBinfoUnspent =+  withMetrics (.binfoUnspent) $ do+    (xspecs, addrs) <- getBinfoActive+    numtxid <- getNumTxId+    limit <- get_limit+    min_conf <- get_min_conf+    net <- lift $ asks (.config.store.net)+    ctx <- lift $ asks (.config.store.ctx)+    height <- getChainHeight+    let mn u = min_conf > u.confirmations+    xbals <- lift $ getXBals xspecs+    bus <-+      lift . runConduit $+        getBinfoUnspents numtxid height xbals xspecs addrs+          .| (dropWhileC mn >> takeC limit .| sinkList)+    setHeaders+    streamEncoding (marshalEncoding (net, ctx) (BinfoUnspents bus))+  where+    get_limit = fmap (min 1000) $ S.param "limit" `S.rescue` const (return 250)+    get_min_conf = S.param "confirmations" `S.rescue` const (return 0)++getBinfoUnspents ::+  (StoreReadExtra m, MonadIO m) =>+  Bool ->+  H.BlockHeight ->+  HashMap XPubSpec [XPubBal] ->+  HashSet XPubSpec ->+  HashSet Address ->+  ConduitT () BinfoUnspent m ()+getBinfoUnspents numtxid height xbals xspecs addrs = do+  cs' <- conduits+  joinDescStreams cs' .| mapC (uncurry binfo)+  where+    binfo u xp =+      let conf = case u.block of+            MemRef _ -> 0+            BlockRef h _ -> height - h + 1+       in BinfoUnspent+            { txid = u.outpoint.hash,+              index = u.outpoint.index,+              script = u.script,+              value = u.value,+              confirmations = fromIntegral conf,+              txidx = encodeBinfoTxId numtxid u.outpoint.hash,+              xpub = xp+            }+    conduits = (<>) <$> xconduits <*> pure acounduits+    xconduits = lift $ do+      let f x (XPubUnspent u p) =+            let path = toSoft (listToPath p)+                xp = BinfoXPubPath x.key <$> path+             in (u, xp)+          g x = do+            let h = xPubUnspents x (xBals x xbals)+                l = let Limits {..} = def in Limits {limit = 16, ..}+            return $ streamThings h Nothing l .| mapC (f x)+      mapM g (HashSet.toList xspecs)+    acounduits =+      let f u = (u, Nothing)+          h = getAddressUnspents+          l = let Limits {..} = def in Limits {limit = 16, ..}+          g a = streamThings (h a) Nothing l .| mapC f+       in map g (HashSet.toList addrs)++getXBals ::+  (StoreReadExtra m) =>+  HashSet XPubSpec ->+  m (HashMap XPubSpec [XPubBal])+getXBals =+  fmap HashMap.fromList . mapM f . HashSet.toList+  where+    f x = (x,) . filter (not . nullBalance . (.balance)) <$> xPubBals x++xBals :: XPubSpec -> HashMap XPubSpec [XPubBal] -> [XPubBal]+xBals = HashMap.findWithDefault []++getBinfoTxs ::+  (StoreReadExtra m, MonadIO m) =>+  HashMap XPubSpec [XPubBal] -> -- xpub balances+  HashMap Address (Maybe BinfoXPubPath) -> -- address book+  HashSet XPubSpec -> -- show xpubs+  HashSet Address -> -- show addrs+  HashSet Address -> -- balance addresses+  BinfoFilter ->+  Bool -> -- numtxid+  Bool -> -- prune outputs+  Int64 -> -- starting balance+  ConduitT () BinfoTx m ()+getBinfoTxs+  xbals+  abook+  sxspecs+  saddrs+  baddrs+  bfilter+  numtxid+  prune+  bal = do+    cs' <- conduits+    joinDescStreams cs' .| go bal+    where+      sxspecs_ls = HashSet.toList sxspecs+      saddrs_ls = HashSet.toList saddrs+      conduits =+        (<>)+          <$> mapM xpub_c sxspecs_ls+          <*> pure (map addr_c saddrs_ls)+      xpub_c x = do+        let f = xPubTxs x (xBals x xbals)+            l = Limits {limit = 16, offset = 0, start = Nothing}+        lift . return $+          streamThings f (Just (.txid)) l+      addr_c a = do+        let l = Limits {limit = 16, offset = 0, start = Nothing}+        streamThings (getAddressTxs a) (Just (.txid)) l+      binfo_tx = toBinfoTx numtxid abook prune+      compute_bal_change t =+        let ins = map (.output) t.inputs+            out = t.outputs+            f b BinfoTxOutput {..} =+              let val = fromIntegral value+               in case address of+                    Nothing -> 0+                    Just a+                      | a `HashSet.member` baddrs ->+                          if b then val else negate val+                      | otherwise -> 0+         in sum $ map (f False) ins <> map (f True) out+      go b =+        await >>= \case+          Nothing -> return ()+          Just (TxRef _ t) ->+            lift (getTransaction t) >>= \case+              Nothing -> go b+              Just x -> do+                let a = binfo_tx b x+                    b' = b - compute_bal_change a+                    c = isJust a.blockHeight+                    Just (d, _) = a.balance+                    r = d + fromIntegral a.fee+                case bfilter of+                  BinfoFilterAll ->+                    yield a >> go b'+                  BinfoFilterSent+                    | 0 > r -> yield a >> go b'+                    | otherwise -> go b'+                  BinfoFilterReceived+                    | r > 0 -> yield a >> go b'+                    | otherwise -> go b'+                  BinfoFilterMoved+                    | r == 0 -> yield a >> go b'+                    | otherwise -> go b'+                  BinfoFilterConfirmed+                    | c -> yield a >> go b'+                    | otherwise -> go b'+                  BinfoFilterMempool+                    | c -> return ()+                    | otherwise -> yield a >> go b'++getCashAddr :: (Monad m) => WebT m Bool+getCashAddr = S.param "cashaddr" `S.rescue` const (return False)++getAddress :: (MonadUnliftIO m) => TL.Text -> WebT m Address+getAddress param' = do+  txt <- S.param param'+  net <- asks (.config.store.net)+  case textToAddr net txt of+    Nothing -> raise ThingNotFound+    Just a -> return a++getBinfoAddr :: (Monad m) => TL.Text -> WebT m BinfoAddr+getBinfoAddr param' = do+  txt <- S.param param'+  net <- asks (.config.store.net)+  ctx <- asks (.config.store.ctx)+  let x =+        BinfoAddr+          <$> textToAddr net txt+          <|> BinfoXpub <$> xPubImport net ctx txt+  maybe S.next return x++scottyBinfoHistory :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()+scottyBinfoHistory =+  withMetrics (.binfoExportHistory) $ do+    (xspecs, addrs) <- getBinfoActive+    (startM, endM) <- get_dates+    (code, price') <- getPrice+    xbals <- getXBals xspecs+    let xaddrs = HashSet.fromList $ concatMap (map get_addr) (HashMap.elems xbals)+        aaddrs = xaddrs <> addrs+        cur = price'.fifteen+        cs' = conduits (HashMap.toList xbals) addrs endM+    txs <-+      lift $+        runConduit $+          joinDescStreams cs'+            .| takeWhileC (is_newer startM)+            .| concatMapMC get_transaction+            .| sinkList+    let times = map (\Transaction {..} -> timestamp) txs+    net <- asks (.config.store.net)+    url <- asks (.config.priceHistoryURL)+    session <- asks (.session)+    rates <- map (.price) <$> lift (getRates net session url code times)+    let hs = zipWith (convert cur aaddrs) txs (rates <> repeat 0.0)+    setHeaders+    streamEncoding $ toEncoding hs+  where+    is_newer+      (Just BlockData {height = bh})+      TxRef {block = BlockRef {height = th}} =+        bh <= th+    is_newer Nothing TxRef {} = True+    get_addr = (.balance.address)+    get_transaction TxRef {txid = h} =+      getTransaction h+    convert cur addrs tx rate =+      let ins = tx.inputs+          outs = tx.outputs+          fins = filter (input_addr addrs) ins+          fouts = filter (output_addr addrs) outs+          vin = fromIntegral . sum $ map (.value) fins+          vout = fromIntegral . sum $ map (.value) fouts+          fee = tx.fee+          v = vout - vin+          t = tx.timestamp+          h = txHash $ transactionData tx+       in toBinfoHistory v t rate cur fee h+    input_addr addrs' StoreInput {address = Just a} =+      a `HashSet.member` addrs'+    input_addr _ _ = False+    output_addr addrs' StoreOutput {address = Just a} =+      a `HashSet.member` addrs'+    output_addr _ _ = False+    get_dates = do+      BinfoDate start <- S.param "start"+      BinfoDate end' <- S.param "end"+      let end = end' + 24 * 60 * 60+      ch <- lift $ asks (.config.store.chain)+      startM <- blockAtOrAfter ch start+      endM <- blockAtOrBefore ch end+      return (startM, endM)+    conduits xpubs addrs endM =+      map (uncurry (xpub_c endM)) xpubs+        <> map (addr_c endM) (HashSet.toList addrs)+    lim endM =+      Limits+        { limit = 16,+          offset = 0,+          start = AtBlock . (.height) <$> endM+        }+    addr_c endM a =+      streamThings (getAddressTxs a) (Just (.txid)) (lim endM)+    xpub_c endM x bs =+      streamThings (xPubTxs x bs) (Just (.txid)) (lim endM)++getPrice :: (MonadIO m) => WebT m (Text, BinfoTicker)+getPrice = do+  code <- T.toUpper <$> S.param "currency" `S.rescue` const (return "USD")+  ticker <- lift $ asks (.ticker)+  prices <- readTVarIO ticker+  case HashMap.lookup code prices of+    Nothing -> return (code, def)+    Just p -> return (code, p)++getSymbol :: (MonadIO m) => WebT m BinfoSymbol+getSymbol = uncurry binfoTickerToSymbol <$> getPrice++scottyBinfoBlocksDay :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()+scottyBinfoBlocksDay =+  withMetrics (.binfoBlock) $ do+    t <- min h . (`div` 1000) <$> S.param "milliseconds"+    ch <- asks (.config.store.chain)+    m <- blockAtOrBefore ch t+    bs <- go (d t) m+    streamEncoding $ toEncoding $ map toBinfoBlockInfo bs+  where+    h = fromIntegral (maxBound :: H.Timestamp)+    d = subtract (24 * 3600)+    go _ Nothing = return []+    go t (Just b)+      | b.header.timestamp <= fromIntegral t =+          return []+      | otherwise = do+          b' <- getBlock b.header.prev+          (b :) <$> go t b'++scottyMultiAddr :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()+scottyMultiAddr =+  withMetrics (.binfoMultiaddr) $ do+    (addrs', _, saddrs, sxpubs, xspecs) <- get_addrs+    numtxid <- getNumTxId+    cashaddr <- getCashAddr+    local' <- getSymbol+    offset <- getBinfoOffset+    n <- getBinfoCount "n"+    prune <- get_prune+    fltr <- get_filter+    xbals <- getXBals xspecs+    xtxns <- get_xpub_tx_count xbals xspecs+    let sxbals = only_show_xbals sxpubs xbals+        xabals = compute_xabals xbals+        addrs = addrs' `HashSet.difference` HashMap.keysSet xabals+    abals <- get_abals addrs+    let sxspecs = only_show_xspecs sxpubs xspecs+        sxabals = compute_xabals sxbals+        sabals = only_show_abals saddrs abals+        sallbals = sabals <> sxabals+        sbal = compute_bal sallbals+        allbals = abals <> xabals+        abook = compute_abook addrs xbals+        sxaddrs = compute_xaddrs sxbals+        salladdrs = saddrs <> sxaddrs+        bal = compute_bal allbals+        ibal = fromIntegral sbal+    ftxs <-+      lift . runConduit $+        getBinfoTxs+          xbals+          abook+          sxspecs+          saddrs+          salladdrs+          fltr+          numtxid+          prune+          ibal+          .| (dropC offset >> takeC n .| sinkList)+    net <- asks (.config.store.net)+    ctx <- asks (.config.store.ctx)+    best <- get_best_block+    peers <- get_peers+    let baddrs = toBinfoAddrs sabals sxbals xtxns+        abaddrs = toBinfoAddrs abals xbals xtxns+        recv = sum $ map (.received) abaddrs+        sent' = sum $ map (.sent) abaddrs+        txn = fromIntegral $ length ftxs+        wallet =+          BinfoWallet+            { balance = bal,+              txs = txn,+              filtered = txn,+              received = recv,+              sent = sent'+            }+        coin = netBinfoSymbol net+    let block =+          BinfoBlockInfo+            { hash = H.headerHash best.header,+              height = best.height,+              timestamp = best.header.timestamp,+              index = best.height+            }+    let info =+          BinfoInfo+            { connected = peers,+              conversion = 100 * 1000 * 1000,+              fiat = local',+              crypto = coin,+              head = block+            }+    let multiaddr =+          BinfoMultiAddr+            { addresses = baddrs,+              wallet = wallet,+              txs = ftxs,+              info = info,+              recommendFee = True,+              cashAddr = cashaddr+            }+    setHeaders+    streamEncoding $ marshalEncoding (net, ctx) multiaddr+  where+    get_xpub_tx_count xbals =+      fmap HashMap.fromList+        . mapM+          ( \x ->+              (x,)+                . fromIntegral+                <$> xPubTxCount x (xBals x xbals)+          )+        . HashSet.toList+    get_filter =+      S.param "filter" `S.rescue` const (return BinfoFilterAll)+    get_best_block =+      getBestBlock >>= \case+        Nothing -> raise ThingNotFound+        Just bh ->+          getBlock bh >>= \case+            Nothing -> raise ThingNotFound+            Just b -> return b+    get_prune =+      fmap not $+        S.param "no_compact"+          `S.rescue` const (return False)+    only_show_xbals sxpubs =+      HashMap.filterWithKey (\k _ -> k.key `HashSet.member` sxpubs)+    only_show_xspecs sxpubs =+      HashSet.filter (\k -> k.key `HashSet.member` sxpubs)+    only_show_abals saddrs =+      HashMap.filterWithKey (\k _ -> k `HashSet.member` saddrs)+    addr (BinfoAddr a) = Just a+    addr (BinfoXpub _) = Nothing+    xpub (BinfoXpub x) = Just x+    xpub (BinfoAddr _) = Nothing+    get_addrs = do+      (xspecs, addrs) <- getBinfoActive+      sh <- getBinfoAddrsParam "onlyShow"+      let xpubs = HashSet.map (.key) xspecs+          actives =+            HashSet.map BinfoAddr addrs+              <> HashSet.map BinfoXpub xpubs+          sh' = if HashSet.null sh then actives else sh+          saddrs = HashSet.fromList $ mapMaybe addr $ HashSet.toList sh'+          sxpubs = HashSet.fromList $ mapMaybe xpub $ HashSet.toList sh'+      return (addrs, xpubs, saddrs, sxpubs, xspecs)+    get_abals =+      let f b = (b.address, b)+          g = HashMap.fromList . map f+       in fmap g . getBalances . HashSet.toList+    get_peers = do+      ps <- lift $ getPeersInformation =<< asks (.config.store.peerMgr)+      return (fromIntegral (length ps))+    compute_xabals =+      let f b = (b.balance.address, b.balance)+       in HashMap.fromList . concatMap (map f) . HashMap.elems+    compute_bal =+      let f b = b.confirmed + b.unconfirmed+       in sum . map f . HashMap.elems+    compute_abook addrs xbals =+      let f xs xb =+            let a = xb.balance.address+                e = error "lions and tigers and bears"+                s = toSoft (listToPath xb.path)+             in (a, Just (BinfoXPubPath xs.key (fromMaybe e s)))+          amap =+            HashMap.map (const Nothing) $+              HashSet.toMap addrs+          xmap =+            HashMap.fromList+              . concatMap (uncurry (map . f))+              $ HashMap.toList xbals+       in amap <> xmap+    compute_xaddrs =+      let f = map (.balance.address)+       in HashSet.fromList . concatMap f . HashMap.elems++getBinfoCount :: (MonadUnliftIO m) => TL.Text -> WebT m Int+getBinfoCount str = do+  d <- asks (.config.limits.defItemCount)+  x <- asks (.config.limits.maxFullItemCount)+  i <- min x <$> (S.param str `S.rescue` const (return d))+  return (fromIntegral i :: Int)++getBinfoOffset ::+  (MonadUnliftIO m) =>+  WebT m Int+getBinfoOffset = do+  x <- asks (.config.limits.maxOffset)+  o <- S.param "offset" `S.rescue` const (return 0)+  when (o > x) $+    raise $+      UserError $+        "offset exceeded: " <> show o <> " > " <> show x+  return (fromIntegral o :: Int)++scottyRawAddr :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()+scottyRawAddr =+  withMetrics (.binfoAddressRaw) $+    getBinfoAddr "addr" >>= \case+      BinfoAddr addr -> do_addr addr+      BinfoXpub xpub -> do_xpub xpub+  where+    do_xpub xpub = do+      numtxid <- getNumTxId+      derive <- S.param "derive" `S.rescue` const (return DeriveNormal)+      let xspec = XPubSpec xpub derive+      n <- getBinfoCount "limit"+      off <- getBinfoOffset+      xbals <- getXBals $ HashSet.singleton xspec+      net <- lift $ asks (.config.store.net)+      let summary = xPubSummary xspec (xBals xspec xbals)+          abook = compute_abook xpub (xBals xspec xbals)+          xspecs = HashSet.singleton xspec+          saddrs = HashSet.empty+          baddrs = HashMap.keysSet abook+          bfilter = BinfoFilterAll+          amnt = summary.confirmed + summary.unconfirmed+      txs <-+        lift $+          runConduit $+            getBinfoTxs+              xbals+              abook+              xspecs+              saddrs+              baddrs+              bfilter+              numtxid+              False+              (fromIntegral amnt)+              .| (dropC off >> takeC n .| sinkList)+      let ra =+            BinfoRawAddr+              { address = BinfoXpub xpub,+                balance = amnt,+                ntx = fromIntegral $ length txs,+                utxo = summary.utxo,+                received = summary.received,+                sent = fromIntegral summary.received - fromIntegral amnt,+                txs = txs+              }+      setHeaders+      ctx <- asks (.config.store.ctx)+      streamEncoding $ marshalEncoding (net, ctx) ra+    compute_abook xpub xbals =+      let f xb =+            let a = xb.balance.address+                e = error "black hole swallows all your code"+                s = toSoft $ listToPath xb.path+                m = fromMaybe e s+             in (a, Just (BinfoXPubPath xpub m))+       in HashMap.fromList $ map f xbals+    do_addr addr = do+      numtxid <- getNumTxId+      n <- getBinfoCount "limit"+      off <- getBinfoOffset+      bal <- fromMaybe (zeroBalance addr) <$> getBalance addr+      net <- lift $ asks (.config.store.net)+      let abook = HashMap.singleton addr Nothing+          xspecs = HashSet.empty+          saddrs = HashSet.singleton addr+          bfilter = BinfoFilterAll+          balance = bal.confirmed + bal.unconfirmed+      txs <-+        lift . runConduit $+          getBinfoTxs+            HashMap.empty+            abook+            xspecs+            saddrs+            saddrs+            bfilter+            numtxid+            False+            (fromIntegral balance)+            .| (dropC off >> takeC n .| sinkList)+      let ra =+            BinfoRawAddr+              { address = BinfoAddr addr,+                balance,+                ntx = bal.txs,+                utxo = bal.utxo,+                received = bal.received,+                sent = fromIntegral bal.received - fromIntegral balance,+                txs+              }+      setHeaders+      ctx <- asks (.config.store.ctx)+      streamEncoding $ marshalEncoding (net, ctx) ra++scottyBinfoReceived :: (MonadUnliftIO m) => WebT m ()+scottyBinfoReceived =+  withMetrics (.binfoQgetreceivedbyaddress) $ do+    a <- getAddress "addr"+    b <- fromMaybe (zeroBalance a) <$> getBalance a+    setHeaders+    S.text $ cs $ show b.received++scottyBinfoSent :: (MonadUnliftIO m) => WebT m ()+scottyBinfoSent =+  withMetrics (.binfoQgetsentbyaddress) $ do+    a <- getAddress "addr"+    b <- fromMaybe (zeroBalance a) <$> getBalance a+    setHeaders+    S.text $ cs $ show $ b.received - b.confirmed - b.unconfirmed++scottyBinfoAddrBalance :: (MonadUnliftIO m) => WebT m ()+scottyBinfoAddrBalance =+  withMetrics (.binfoQaddressbalance) $ do+    a <- getAddress "addr"+    b <- fromMaybe (zeroBalance a) <$> getBalance a+    setHeaders+    S.text $ cs $ show $ b.confirmed + b.unconfirmed++scottyFirstSeen :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()+scottyFirstSeen =+  withMetrics (.binfoQaddressfirstseen) $ do+    a <- getAddress "addr"+    ch <- asks (.config.store.chain)+    bb <- chainGetBest ch+    let top = bb.height+        bot = 0+    i <- go ch bb a bot top+    setHeaders+    S.text $ cs $ show i+  where+    go ch bb a bot top = do+      let mid = bot + (top - bot) `div` 2+          n = top - bot < 2+      x <- hasone a bot+      y <- hasone a mid+      z <- hasone a top+      if+        | x -> getblocktime ch bb bot+        | n -> getblocktime ch bb top+        | y -> go ch bb a bot mid+        | z -> go ch bb a mid top+        | otherwise -> return 0+    getblocktime ch bb h =+      chainGetAncestor h bb ch >>= \case+        Just b -> return b.header.timestamp+        Nothing -> do+          lift . $(logErrorS) "Web" $+            "Could not get ancestor at height "+              <> cs (show h)+              <> " for block "+              <> H.blockHashToHex (H.headerHash bb.header)+          error "Block ancestor retreival error"+    hasone a h = do+      let l = Limits 1 0 (Just (AtBlock h))+      not . null <$> getAddressTxs a l++scottyShortBal :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()+scottyShortBal =+  withMetrics (.binfoBalance) $ do+    (xspecs, addrs) <- getBinfoActive+    cashaddr <- getCashAddr+    net <- asks (.config.store.net)+    abals <- catMaybes <$> mapM (getabal net cashaddr) (HashSet.toList addrs)+    xbals <- mapM (getxbal net) (HashSet.toList xspecs)+    let res = HashMap.fromList (abals <> xbals)+    setHeaders+    streamEncoding $ toEncoding res+  where+    shorten bal =+      BinfoShortBal+        { final = bal.confirmed + bal.unconfirmed,+          ntx = bal.txs,+          received = bal.received+        }+    getabal net cashaddr a =+      let net' =+            if+              | cashaddr -> net+              | net == bch -> btc+              | net == bchTest -> btcTest+              | net == bchTest4 -> btcTest+              | otherwise -> net+       in case addrToText net' a of+            Nothing -> return Nothing+            Just a' ->+              getBalance a >>= \case+                Nothing -> return $ Just (a', shorten (zeroBalance a))+                Just b -> return $ Just (a', shorten b)+    getxbal net xpub = do+      xbals <- xPubBals xpub+      ntx <- fromIntegral <$> xPubTxCount xpub xbals+      let val = sum $ map (.balance.confirmed) xbals+          zro = sum $ map (.balance.unconfirmed) xbals+          exs = [x | x@XPubBal {path = 0 : _} <- xbals]+          final = val + zro+          received = sum $ map (.balance.received) exs+          sbl = BinfoShortBal {final, ntx, received}+      ctx <- asks (.config.store.ctx)+      return (xPubExport net ctx xpub.key, sbl)++getBinfoHex :: (Monad m) => WebT m Bool+getBinfoHex =+  (== ("hex" :: Text))+    <$> S.param "format" `S.rescue` const (return "json")++scottyBinfoBlockHeight :: (MonadUnliftIO m) => WebT m ()+scottyBinfoBlockHeight =+  withMetrics (.binfoBlockHeight) $ do+    numtxid <- getNumTxId+    height <- S.param "height"+    bs <- fmap catMaybes $ getBlocksAtHeight height >>= mapM getBlock+    ns <- fmap catMaybes $ getBlocksAtHeight (height + 1) >>= mapM getBlock+    is <- mapM (get_binfo_blocks numtxid ns) bs+    setHeaders+    net <- asks (.config.store.net)+    ctx <- asks (.config.store.ctx)+    streamEncoding $ marshalEncoding (net, ctx) is+  where+    get_binfo_blocks numtxid ns b = do+      txs <- catMaybes <$> mapM getTransaction b.txs+      let h x = H.headerHash x.header+          nbs = [h n | n <- ns, n.header.prev == h b]+          bts = map (toBinfoTxSimple numtxid) txs+      return $ toBinfoBlock b bts nbs++scottyBinfoLatest :: (MonadUnliftIO m) => WebT m ()+scottyBinfoLatest =+  withMetrics (.binfoBlockLatest) $ do+    numtxid <- getNumTxId+    mb <- runMaybeT $ MaybeT getBestBlock >>= MaybeT . getBlock+    b <- maybe (raise ThingNotFound) return mb+    streamEncoding $+      toEncoding+        BinfoHeader+          { hash = H.headerHash b.header,+            timestamp = b.header.timestamp,+            index = b.height,+            height = b.height,+            txids = map (encodeBinfoTxId numtxid) b.txs+          }++scottyBinfoBlock :: (MonadUnliftIO m) => WebT m ()+scottyBinfoBlock =+  withMetrics (.binfoBlockRaw) $ do+    numtxid <- getNumTxId+    hex <- getBinfoHex+    S.param "block" >>= \case+      BinfoBlockHash bh -> go numtxid hex bh+      BinfoBlockIndex i ->+        getBlocksAtHeight i >>= \case+          [] -> raise ThingNotFound+          bh : _ -> go numtxid hex bh+  where+    go numtxid hex bh = do+      b <- maybe (raise ThingNotFound) return =<< getBlock bh+      txs <- catMaybes <$> mapM getTransaction b.txs+      nhs <- fmap catMaybes $ getBlocksAtHeight (b.height + 1) >>= mapM getBlock+      let h x = H.headerHash x.header+          nxt = [h n | n <- nhs, n.header.prev == h b]+      if hex+        then do+          let x = H.Block b.header (map transactionData txs)+          setHeaders+          S.text . encodeHexLazy . runPutL $ serialize x+        else do+          let bts = map (toBinfoTxSimple numtxid) txs+              y = toBinfoBlock b bts nxt+          setHeaders+          net <- asks (.config.store.net)+          ctx <- asks (.config.store.ctx)+          streamEncoding $ marshalEncoding (net, ctx) y++getBinfoTx ::+  (MonadLoggerIO m, MonadUnliftIO m) =>+  BinfoTxId ->+  WebT m (Either Except Transaction)+getBinfoTx txid = do+  tx <- case txid of+    BinfoTxIdHash h -> maybeToList <$> getTransaction h+    BinfoTxIdIndex i -> getNumTransaction i+  case tx of+    [t] -> return $ Right t+    [] -> return $ Left ThingNotFound+    ts ->+      let tids = map (txHash . transactionData) ts+       in return $ Left (TxIndexConflict tids)++scottyBinfoTx :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()+scottyBinfoTx =+  withMetrics (.binfoTxRaw) $ do+    numtxid <- getNumTxId+    hex <- getBinfoHex+    txid <- S.param "txid"+    tx <-+      getBinfoTx txid >>= \case+        Right t -> return t+        Left e -> raise e+    if hex then hx tx else js numtxid tx+  where+    js numtxid t = do+      net <- asks (.config.store.net)+      ctx <- asks (.config.store.ctx)+      setHeaders+      streamEncoding $ marshalEncoding (net, ctx) $ toBinfoTxSimple numtxid t+    hx t = do+      setHeaders+      S.text . encodeHexLazy . runPutL . serialize $ transactionData t++scottyBinfoTotalOut :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()+scottyBinfoTotalOut =+  withMetrics (.binfoQtxtotalbtcoutput) $ do+    txid <- S.param "txid"+    tx <-+      getBinfoTx txid >>= \case+        Right t -> return t+        Left e -> raise e+    S.text $ cs $ show $ sum $ map (.value) tx.outputs++scottyBinfoTxFees :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()+scottyBinfoTxFees =+  withMetrics (.binfoQtxfee) $ do+    txid <- S.param "txid"+    tx <-+      getBinfoTx txid >>= \case+        Right t -> return t+        Left e -> raise e+    let i = sum $ map (.value) $ filter f tx.inputs+        o = sum $ map (.value) tx.outputs+    S.text . cs . show $ i - o+  where+    f StoreInput {} = True+    f StoreCoinbase {} = False++scottyBinfoTxResult :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()+scottyBinfoTxResult =+  withMetrics (.binfoQtxresult) $ do+    txid <- S.param "txid"+    addr <- getAddress "addr"+    tx <-+      getBinfoTx txid >>= \case+        Right t -> return t+        Left e -> raise e+    let i = toInteger $ sum $ map (.value) $ filter (f addr) tx.inputs+        o = toInteger $ sum $ map (.value) $ filter (g addr) tx.outputs+    S.text $ cs $ show $ o - i+  where+    f addr StoreInput {address = Just a} = a == addr+    f _ _ = False+    g addr StoreOutput {address = Just a} = a == addr+    g _ _ = False++scottyBinfoTotalInput :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()+scottyBinfoTotalInput =+  withMetrics (.binfoQtxtotalbtcinput) $ do+    txid <- S.param "txid"+    tx <-+      getBinfoTx txid >>= \case+        Right t -> return t+        Left e -> raise e+    S.text $ cs $ show $ sum $ map (.value) $ filter f $ tx.inputs+  where+    f StoreInput {} = True+    f StoreCoinbase {} = False++scottyBinfoMempool :: (MonadUnliftIO m) => WebT m ()+scottyBinfoMempool =+  withMetrics (.binfoMempool) $ do+    numtxid <- getNumTxId+    offset <- getBinfoOffset+    n <- getBinfoCount "limit"+    mempool <- getMempool+    let txids = map snd $ take n $ drop offset mempool+    txs <- catMaybes <$> mapM getTransaction txids+    net <- lift $ asks (.config.store.net)+    setHeaders+    let mem = BinfoMempool $ map (toBinfoTxSimple numtxid) txs+    ctx <- lift $ asks (.config.store.ctx)+    streamEncoding $ marshalEncoding (net, ctx) mem++scottyBinfoGetBlockCount :: (MonadUnliftIO m) => WebT m ()+scottyBinfoGetBlockCount =+  withMetrics (.binfoQgetblockcount) $ do+    ch <- asks (.config.store.chain)+    bn <- chainGetBest ch+    setHeaders+    S.text $ cs $ show bn.height++scottyBinfoLatestHash :: (MonadUnliftIO m) => WebT m ()+scottyBinfoLatestHash =+  withMetrics (.binfoQlatesthash) $ do+    ch <- asks (.config.store.chain)+    bn <- chainGetBest ch+    setHeaders+    S.text $ TL.fromStrict $ H.blockHashToHex $ H.headerHash bn.header++scottyBinfoSubsidy :: (MonadUnliftIO m) => WebT m ()+scottyBinfoSubsidy =+  withMetrics (.binfoQbcperblock) $ do+    ch <- asks (.config.store.chain)+    net <- asks (.config.store.net)+    bn <- chainGetBest ch+    setHeaders+    S.text $+      cs $+        show $+          (/ (100 * 1000 * 1000 :: Double)) $+            fromIntegral $+              H.computeSubsidy net (bn.height + 1)++scottyBinfoAddrToHash :: (MonadUnliftIO m) => WebT m ()+scottyBinfoAddrToHash =+  withMetrics (.binfoQaddresstohash) $ do+    addr <- getAddress "addr"+    setHeaders+    S.text $ encodeHexLazy $ runPutL $ serialize addr.hash160++scottyBinfoHashToAddr :: (MonadUnliftIO m) => WebT m ()+scottyBinfoHashToAddr =+  withMetrics (.binfoQhashtoaddress) $ do+    bs <- maybe S.next return . decodeHex =<< S.param "hash"+    net <- asks (.config.store.net)+    hash <- either (const S.next) return $ decode bs+    addr <- maybe S.next return $ addrToText net $ PubKeyAddress hash+    setHeaders+    S.text $ TL.fromStrict addr++scottyBinfoAddrPubkey :: (MonadUnliftIO m) => WebT m ()+scottyBinfoAddrPubkey =+  withMetrics (.binfoQaddrpubkey) $ do+    hex <- S.param "pubkey"+    ctx <- asks (.config.store.ctx)+    pubkey <-+      maybe S.next (return . pubKeyAddr ctx) $+        eitherToMaybe . unmarshal ctx =<< decodeHex hex+    net <- asks (.config.store.net)+    setHeaders+    case addrToText net pubkey of+      Nothing -> raise ThingNotFound+      Just a -> do+        S.text $ TL.fromStrict a++scottyBinfoPubKeyAddr :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()+scottyBinfoPubKeyAddr =+  withMetrics (.binfoQpubkeyaddr) $ do+    addr <- getAddress "addr"+    mi <- strm addr+    i <- case mi of+      Nothing -> raise ThingNotFound+      Just i -> return i+    pk <- case extr addr i of+      Left e -> raise $ UserError e+      Right t -> return t+    setHeaders+    S.text $ encodeHexLazy $ L.fromStrict pk+  where+    strm addr = do+      runConduit $ do+        let l = Limits {limit = 8, offset = 0, start = Nothing}+        streamThings (getAddressTxs addr) (Just (.txid)) l+          .| concatMapMC (getTransaction . (.txid))+          .| concatMapC (filter (inp addr) . (.inputs))+          .| headC+    inp addr StoreInput {address = Just a} = a == addr+    inp _ _ = False+    extr addr StoreInput {script, pkscript, witness} = do+      Script sig <- decode script+      Script pks <- decode pkscript+      case addr of+        PubKeyAddress {} ->+          case sig of+            [OP_PUSHDATA _ _, OP_PUSHDATA pub _] ->+              Right pub+            [OP_PUSHDATA _ _] ->+              case pks of+                [OP_PUSHDATA pub _, OP_CHECKSIG] ->+                  Right pub+                _ -> Left "Could not parse scriptPubKey"+            _ -> Left "Could not parse scriptSig"+        WitnessPubKeyAddress {} ->+          case witness of+            [_, pub] -> return pub+            _ -> Left "Could not parse scriptPubKey"+        _ -> Left "Address does not have public key"+    extr _ _ = Left "Incorrect input type"++scottyBinfoHashPubkey :: (MonadUnliftIO m) => WebT m ()+scottyBinfoHashPubkey =+  withMetrics (.binfoQhashpubkey) $ do+    ctx <- asks (.config.store.ctx)+    pkm <- (eitherToMaybe . unmarshal ctx <=< decodeHex) <$> S.param "pubkey"+    addr <- case pkm of+      Nothing -> raise $ UserError "Could not decode public key"+      Just pk -> return $ pubKeyAddr ctx pk+    setHeaders+    S.text $ encodeHexLazy $ runPutL $ serialize addr.hash160++-- GET Network Information --++scottyPeers ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  GetPeers ->+  WebT m [PeerInfo]+scottyPeers _ =+  withMetrics (.peers) $ do+    lift . getPeersInformation =<< asks (.config.store.peerMgr)++-- | Obtain information about connected peers from peer manager process.+getPeersInformation ::+  (MonadLoggerIO m) => PeerMgr -> m [PeerInfo]+getPeersInformation mgr =+  mapMaybe toInfo <$> getPeers mgr+  where+    toInfo op = do+      ver <- op.version+      return+        PeerInfo+          { userAgent = ver.userAgent.get,+            address = show op.address,+            version = ver.version,+            services = ver.services,+            relay = ver.relay+          }++scottyHealth ::+  (MonadUnliftIO m) => GetHealth -> WebT m HealthCheck+scottyHealth _ =+  withMetrics (.health) $ do+    h <- asks (.health) >>= readTVarIO+    unless (isOK h) $ S.status status503+    return h++blockHealthCheck ::+  (MonadUnliftIO m, StoreReadBase m) =>+  WebConfig ->+  m BlockHealth+blockHealthCheck cfg = do+  let ch = cfg.store.chain+  headers <- (.height) <$> chainGetBest ch+  blocks <-+    maybe 0 (.height)+      <$> runMaybeT (MaybeT getBestBlock >>= MaybeT . getBlock)+  return+    BlockHealth+      { headers,+        blocks,+        max = fromIntegral cfg.maxLaggingBlocks+      }++lastBlockHealthCheck ::+  (MonadUnliftIO m, StoreReadBase m) =>+  Chain ->+  WebLimits ->+  m TimeHealth+lastBlockHealthCheck ch WebLimits {blockTimeout} = do+  n <- round <$> liftIO getPOSIXTime+  t <- fromIntegral . (.header.timestamp) <$> chainGetBest ch+  return+    TimeHealth+      { age = n - t,+        max = fromIntegral blockTimeout+      }++lastTxHealthCheck ::+  (MonadUnliftIO m, StoreReadBase m) =>+  WebConfig ->+  m TimeHealth+lastTxHealthCheck WebConfig {noMempool, store, limits} = do+  n <- round <$> liftIO getPOSIXTime+  b <- fromIntegral . (.header.timestamp) <$> chainGetBest ch+  t <-+    getMempool >>= \case+      t : _ ->+        let x = fromIntegral $ fst t+         in return $ max x b+      [] -> return b+  return+    TimeHealth+      { age = n - t,+        max = fromIntegral to+      }+  where+    ch = store.chain+    to =+      if noMempool+        then limits.blockTimeout+        else limits.txTimeout++pendingTxsHealthCheck ::+  (MonadUnliftIO m, StoreReadBase m) =>+  WebConfig ->+  m MaxHealth+pendingTxsHealthCheck cfg = do+  n <- blockStorePendingTxs cfg.store.block+  return+    MaxHealth+      { max = fromIntegral cfg.maxPendingTxs,+        count = fromIntegral n+      }++peerHealthCheck ::+  (MonadUnliftIO m, StoreReadBase m) =>+  WebConfig ->+  m CountHealth+peerHealthCheck cfg = do+  count <- fromIntegral . length <$> getPeers cfg.store.peerMgr+  return CountHealth {min = fromIntegral cfg.minPeers, count}++healthCheck ::+  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>+  WebConfig ->+  m HealthCheck+healthCheck cfg = do+  blocks <- blockHealthCheck cfg+  lastBlock <- lastBlockHealthCheck cfg.store.chain cfg.limits+  lastTx <- lastTxHealthCheck cfg+  pendingTxs <- pendingTxsHealthCheck cfg+  peers <- peerHealthCheck cfg+  time <- round <$> liftIO getPOSIXTime+  let check =+        HealthCheck+          { network = cfg.store.net.name,+            version = cfg.version,+            ..+          }+  unless (isOK check) $ do+    let t = toStrict $ encodeToLazyText check+    $(logErrorS) "Web" $ "Health check failed: " <> t+  return check++scottyDbStats :: (MonadUnliftIO m) => WebT m ()+scottyDbStats =+  withMetrics (.db) $ do+    setHeaders+    db <- lift $ asks (.config.store.db.db)+    statsM <- lift (getProperty db Stats)+    S.text $ maybe "Could not get stats" cs statsM++-----------------------+-- Parameter Parsing --+-----------------------++-- | Returns @Nothing@ if the parameter is not supplied. Raises an exception on+-- parse failure.+paramOptional :: (Param a, MonadIO m) => WebT m (Maybe a)+paramOptional = go Proxy+  where+    go :: (Param a, MonadIO m) => Proxy a -> WebT m (Maybe a)+    go proxy = do+      net <- lift $ asks (.config.store.net)+      ctx <- lift $ asks (.config.store.ctx)+      tsM :: Maybe [Text] <- p `S.rescue` const (return Nothing)+      case tsM of+        Nothing -> return Nothing -- Parameter was not supplied+        Just ts -> maybe (raise err) (return . Just) $ parseParam net ctx ts+      where+        l = proxyLabel proxy+        p = Just <$> S.param (cs l)+        err = UserError $ "Unable to parse param " <> cs l++-- | Raises an exception if the parameter is not supplied+param :: (Param a, MonadIO m) => WebT m a+param = go Proxy+  where+    go :: (Param a, MonadIO m) => Proxy a -> WebT m a+    go proxy = do+      resM <- paramOptional+      case resM of+        Just res -> return res+        _ ->+          raise . UserError $+            "The param " <> cs (proxyLabel proxy) <> " was not defined"++-- | Returns the default value of a parameter if it is not supplied. Raises an+-- exception on parse failure.+paramDef :: (Default a, Param a, MonadIO m) => WebT m a+paramDef = fromMaybe def <$> paramOptional++-- | Does not raise exceptions. Will call @Scotty.next@ if the parameter is+-- not supplied or if parsing fails.+paramLazy :: (Param a, MonadIO m) => WebT m a+paramLazy = do+  resM <- paramOptional `S.rescue` const (return Nothing)+  maybe S.next return resM++parseBody :: (MonadIO m, Serial a) => WebT m a+parseBody = do+  b <- L.toStrict <$> S.body+  case hex b <> bin b of+    Left _ -> raise $ UserError "Failed to parse request body"+    Right x -> return x+  where+    bin = runGetS deserialize+    hex b =+      let ns = C.filter (not . isSpace) b+       in if isBase16 ns+            then bin . decodeBase16 $ assertBase16 ns+            else Left "Invalid hex input"++parseOffset :: (MonadIO m) => WebT m OffsetParam+parseOffset = do+  res@(OffsetParam o) <- paramDef+  limits <- lift $ asks (.config.limits)+  when (limits.maxOffset > 0 && fromIntegral o > limits.maxOffset) $+    raise . UserError $+      "offset exceeded: " <> show o <> " > " <> show limits.maxOffset+  return res++parseStart ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  Maybe StartParam ->+  WebT m (Maybe Start)+parseStart Nothing = return Nothing+parseStart (Just s) =+  runMaybeT $+    case s of+      StartParamHash {hash = h} -> start_tx h <|> start_block h+      StartParamHeight {height = h} -> start_height h+      StartParamTime {time = q} -> start_time q+  where+    start_height h = return $ AtBlock $ fromIntegral h+    start_block h = do+      b <- MaybeT $ getBlock (H.BlockHash h)+      return $ AtBlock b.height+    start_tx h = do+      _ <- MaybeT $ getTxData (TxHash h)+      return $ AtTx (TxHash h)+    start_time q = do+      ch <- lift $ asks (.config.store.chain)+      b <- MaybeT $ blockAtOrBefore ch q+      return $ AtBlock b.height++parseLimits :: (MonadIO m) => WebT m LimitsParam+parseLimits = LimitsParam <$> paramOptional <*> parseOffset <*> paramOptional++paramToLimits ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  Bool ->+  LimitsParam ->+  WebT m Limits+paramToLimits full (LimitsParam limitM o startM) = do+  wl <- lift $ asks (.config.limits)+  Limits (validateLimit wl full limitM) (fromIntegral o) <$> parseStart startM++validateLimit :: WebLimits -> Bool -> Maybe LimitParam -> Word32+validateLimit wl full limitM =+  f m $ maybe d (fromIntegral . (.get)) limitM+  where+    m+      | full && wl.maxFullItemCount > 0 = wl.maxFullItemCount+      | otherwise = wl.maxItemCount+    d = wl.defItemCount+    f a 0 = a+    f 0 b = b+    f a b = min a b++---------------+-- Utilities --+---------------++runInWebReader ::+  (MonadIO m) =>+  CacheT (DatabaseReaderT m) a ->+  ReaderT WebState m a+runInWebReader f = do+  bdb <- asks (.config.store.db)+  mc <- asks (.config.store.cache)+  lift $ runReaderT (withCache mc f) bdb++runNoCache ::+  (MonadIO m) =>+  Bool ->+  ReaderT WebState m a ->+  ReaderT WebState m a+runNoCache False = id+runNoCache True = local $ \s ->+  s {config = s.config {store = s.config.store {cache = Nothing}}}++logIt ::+  (MonadUnliftIO m, MonadLoggerIO m) =>+  m Middleware+logIt = do+  runner <- askRunInIO+  return $ \app req respond ->+    app req $ \res -> do+      let s = responseStatus res+          msg = fmtReq req <> ": " <> fmtStatus s+      if statusIsSuccessful s+        then runner $ $(logDebugS) "Web" msg+        else runner $ $(logErrorS) "Web" msg+      respond res++fmtReq :: Request -> Text+fmtReq req =+  let m = requestMethod req+      v = httpVersion req+      p = rawPathInfo req+      q = rawQueryString req+   in T.decodeUtf8 (m <> " " <> p <> q <> " " <> cs (show v))  fmtStatus :: Status -> Text fmtStatus s = cs (show (statusCode s)) <> " " <> cs (statusMessage s)
test/Haskoin/StoreSpec.hs view
@@ -110,7 +110,7 @@                 peerTimeout = 60,                 maxPeerLife = 48 * 3600,                 connect = dummyPeerConnect net ad,-                statsStore = Nothing,+                stats = Nothing,                 redisSyncInterval = 30               }       withStore cfg $ \Store {..} ->