packages feed

haskoin-store 0.65.12 → 1.0.0

raw patch · 16 files changed

+7178/−5481 lines, 16 filesdep ~base16dep ~haskoin-coredep ~haskoin-nodePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base16, haskoin-core, haskoin-node, haskoin-store-data

API changes (from Hackage documentation)

- Haskoin.Store: [atBlockHeight] :: Start -> !BlockHeight
- Haskoin.Store: [atTxHash] :: Start -> !TxHash
- Haskoin.Store: [cacheChain] :: CacheConfig -> !Chain
- Haskoin.Store: [cacheConn] :: CacheConfig -> !Connection
- Haskoin.Store: [cacheMax] :: CacheConfig -> !Integer
- Haskoin.Store: [cacheMetrics] :: CacheConfig -> !Maybe CacheMetrics
- Haskoin.Store: [cacheMin] :: CacheConfig -> !Int
- Haskoin.Store: [limit] :: Limits -> !Word32
- Haskoin.Store: [offset] :: Limits -> !Word32
- Haskoin.Store: [start] :: Limits -> !Maybe Start
- Haskoin.Store: [storeBlock] :: Store -> !BlockStore
- Haskoin.Store: [storeCache] :: Store -> !Maybe CacheConfig
- Haskoin.Store: [storeChain] :: Store -> !Chain
- Haskoin.Store: [storeConfCacheMempoolSync] :: StoreConfig -> !Int
- Haskoin.Store: [storeConfCacheMin] :: StoreConfig -> !Int
- Haskoin.Store: [storeConfCache] :: StoreConfig -> !Maybe String
- Haskoin.Store: [storeConfConnect] :: StoreConfig -> !SockAddr -> WithConnection
- Haskoin.Store: [storeConfDB] :: StoreConfig -> !FilePath
- Haskoin.Store: [storeConfDiscover] :: StoreConfig -> !Bool
- Haskoin.Store: [storeConfGap] :: StoreConfig -> !Word32
- Haskoin.Store: [storeConfInitPeers] :: StoreConfig -> ![String]
- Haskoin.Store: [storeConfInitialGap] :: StoreConfig -> !Word32
- Haskoin.Store: [storeConfMaxKeys] :: StoreConfig -> !Integer
- Haskoin.Store: [storeConfMaxPeers] :: StoreConfig -> !Int
- Haskoin.Store: [storeConfNetwork] :: StoreConfig -> !Network
- Haskoin.Store: [storeConfNoMempool] :: StoreConfig -> !Bool
- Haskoin.Store: [storeConfPeerMaxLife] :: StoreConfig -> !NominalDiffTime
- Haskoin.Store: [storeConfPeerTimeout] :: StoreConfig -> !NominalDiffTime
- Haskoin.Store: [storeConfStats] :: StoreConfig -> !Maybe Store
- Haskoin.Store: [storeConfSyncMempool] :: StoreConfig -> !Bool
- Haskoin.Store: [storeConfWipeMempool] :: StoreConfig -> !Bool
- Haskoin.Store: [storeDB] :: Store -> !DatabaseReader
- Haskoin.Store: [storeManager] :: Store -> !PeerManager
- Haskoin.Store: [storeNetwork] :: Store -> !Network
- Haskoin.Store: [storePublisher] :: Store -> !Publisher StoreEvent
- Haskoin.Store.BlockStore: [blockConfChain] :: BlockStoreConfig -> !Chain
- Haskoin.Store.BlockStore: [blockConfDB] :: BlockStoreConfig -> !DatabaseReader
- Haskoin.Store.BlockStore: [blockConfListener] :: BlockStoreConfig -> !Publisher StoreEvent
- Haskoin.Store.BlockStore: [blockConfManager] :: BlockStoreConfig -> !PeerManager
- Haskoin.Store.BlockStore: [blockConfNet] :: BlockStoreConfig -> !Network
- Haskoin.Store.BlockStore: [blockConfNoMempool] :: BlockStoreConfig -> !Bool
- Haskoin.Store.BlockStore: [blockConfPeerTimeout] :: BlockStoreConfig -> !NominalDiffTime
- Haskoin.Store.BlockStore: [blockConfStats] :: BlockStoreConfig -> !Maybe Store
- Haskoin.Store.BlockStore: [blockConfSyncMempool] :: BlockStoreConfig -> !Bool
- Haskoin.Store.BlockStore: [blockConfWipeMempool] :: BlockStoreConfig -> !Bool
- Haskoin.Store.Cache: [cacheChain] :: CacheConfig -> !Chain
- Haskoin.Store.Cache: [cacheConn] :: CacheConfig -> !Connection
- Haskoin.Store.Cache: [cacheMax] :: CacheConfig -> !Integer
- Haskoin.Store.Cache: [cacheMetrics] :: CacheConfig -> !Maybe CacheMetrics
- Haskoin.Store.Cache: [cacheMin] :: CacheConfig -> !Int
- Haskoin.Store.Common: [atBlockHeight] :: Start -> !BlockHeight
- Haskoin.Store.Common: [atTxHash] :: Start -> !TxHash
- Haskoin.Store.Common: [dataAddrTxCount] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [dataBalanceCount] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [dataBestCount] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [dataBlockCount] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [dataMempoolCount] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [dataTxCount] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [dataUnspentCount] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [dataXPubBals] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [dataXPubTxCount] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [dataXPubTxs] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [dataXPubUnspents] :: DataMetrics -> !Counter
- Haskoin.Store.Common: [limit] :: Limits -> !Word32
- Haskoin.Store.Common: [offset] :: Limits -> !Word32
- Haskoin.Store.Common: [start] :: Limits -> !Maybe Start
- Haskoin.Store.Database.Reader: [databaseHandle] :: DatabaseReader -> !DB
- Haskoin.Store.Database.Reader: [databaseInitialGap] :: DatabaseReader -> !Word32
- Haskoin.Store.Database.Reader: [databaseMaxGap] :: DatabaseReader -> !Word32
- Haskoin.Store.Database.Reader: [databaseMetrics] :: DatabaseReader -> !Maybe DataMetrics
- Haskoin.Store.Database.Reader: [databaseNetwork] :: DatabaseReader -> !Network
- Haskoin.Store.Database.Types: [addrOutKeyA] :: AddrOutKey -> !Address
- Haskoin.Store.Database.Types: [addrOutKeyB] :: AddrOutKey -> !BlockRef
- Haskoin.Store.Database.Types: [addrOutKeyP] :: AddrOutKey -> !OutPoint
- Haskoin.Store.Database.Types: [addrTxKeyA] :: AddrTxKey -> !Address
- Haskoin.Store.Database.Types: [addrTxKeyB] :: AddrTxKey -> !BlockRef
- Haskoin.Store.Database.Types: [addrTxKeyT] :: AddrTxKey -> !TxRef
- Haskoin.Store.Database.Types: [balValAmount] :: BalVal -> !Word64
- Haskoin.Store.Database.Types: [balValTotalReceived] :: BalVal -> !Word64
- Haskoin.Store.Database.Types: [balValTxCount] :: BalVal -> !Word64
- Haskoin.Store.Database.Types: [balValUnspentCount] :: BalVal -> !Word64
- Haskoin.Store.Database.Types: [balValZero] :: BalVal -> !Word64
- Haskoin.Store.Database.Types: [balanceKey] :: BalKey -> !Address
- Haskoin.Store.Database.Types: [blockKey] :: BlockKey -> BlockHash
- Haskoin.Store.Database.Types: [heightKey] :: HeightKey -> BlockHeight
- Haskoin.Store.Database.Types: [outValAmount] :: OutVal -> !Word64
- Haskoin.Store.Database.Types: [outValScript] :: OutVal -> !ByteString
- Haskoin.Store.Database.Types: [txKeyShort] :: TxKey -> (Word32, Word16)
- Haskoin.Store.Database.Types: [txKey] :: TxKey -> TxHash
- Haskoin.Store.Database.Types: [unspentKeyS] :: UnspentKey -> !TxHash
- Haskoin.Store.Database.Types: [unspentKey] :: UnspentKey -> !OutPoint
- Haskoin.Store.Database.Types: [unspentValAmount] :: UnspentVal -> !Word64
- Haskoin.Store.Database.Types: [unspentValBlock] :: UnspentVal -> !BlockRef
- Haskoin.Store.Database.Types: [unspentValScript] :: UnspentVal -> !ByteString
- Haskoin.Store.Manager: [storeBlock] :: Store -> !BlockStore
- Haskoin.Store.Manager: [storeCache] :: Store -> !Maybe CacheConfig
- Haskoin.Store.Manager: [storeChain] :: Store -> !Chain
- Haskoin.Store.Manager: [storeConfCacheMempoolSync] :: StoreConfig -> !Int
- Haskoin.Store.Manager: [storeConfCacheMin] :: StoreConfig -> !Int
- Haskoin.Store.Manager: [storeConfCache] :: StoreConfig -> !Maybe String
- Haskoin.Store.Manager: [storeConfConnect] :: StoreConfig -> !SockAddr -> WithConnection
- Haskoin.Store.Manager: [storeConfDB] :: StoreConfig -> !FilePath
- Haskoin.Store.Manager: [storeConfDiscover] :: StoreConfig -> !Bool
- Haskoin.Store.Manager: [storeConfGap] :: StoreConfig -> !Word32
- Haskoin.Store.Manager: [storeConfInitPeers] :: StoreConfig -> ![String]
- Haskoin.Store.Manager: [storeConfInitialGap] :: StoreConfig -> !Word32
- Haskoin.Store.Manager: [storeConfMaxKeys] :: StoreConfig -> !Integer
- Haskoin.Store.Manager: [storeConfMaxPeers] :: StoreConfig -> !Int
- Haskoin.Store.Manager: [storeConfNetwork] :: StoreConfig -> !Network
- Haskoin.Store.Manager: [storeConfNoMempool] :: StoreConfig -> !Bool
- Haskoin.Store.Manager: [storeConfPeerMaxLife] :: StoreConfig -> !NominalDiffTime
- Haskoin.Store.Manager: [storeConfPeerTimeout] :: StoreConfig -> !NominalDiffTime
- Haskoin.Store.Manager: [storeConfStats] :: StoreConfig -> !Maybe Store
- Haskoin.Store.Manager: [storeConfSyncMempool] :: StoreConfig -> !Bool
- Haskoin.Store.Manager: [storeConfWipeMempool] :: StoreConfig -> !Bool
- Haskoin.Store.Manager: [storeDB] :: Store -> !DatabaseReader
- Haskoin.Store.Manager: [storeManager] :: Store -> !PeerManager
- Haskoin.Store.Manager: [storeNetwork] :: Store -> !Network
- Haskoin.Store.Manager: [storePublisher] :: Store -> !Publisher StoreEvent
- Haskoin.Store.Web: [blockTimeout] :: WebTimeouts -> !Word64
- Haskoin.Store.Web: [maxLimitBody] :: WebLimits -> !Word32
- Haskoin.Store.Web: [maxLimitCount] :: WebLimits -> !Word32
- Haskoin.Store.Web: [maxLimitDefault] :: WebLimits -> !Word32
- Haskoin.Store.Web: [maxLimitFull] :: WebLimits -> !Word32
- Haskoin.Store.Web: [maxLimitGap] :: WebLimits -> !Word32
- Haskoin.Store.Web: [maxLimitInitialGap] :: WebLimits -> !Word32
- Haskoin.Store.Web: [maxLimitOffset] :: WebLimits -> !Word32
- Haskoin.Store.Web: [maxLimitTimeout] :: WebLimits -> !Word32
- Haskoin.Store.Web: [txTimeout] :: WebTimeouts -> !Word64
- Haskoin.Store.Web: [webBlockchain] :: WebConfig -> !Bool
- Haskoin.Store.Web: [webHealthCheckInterval] :: WebConfig -> !Int
- Haskoin.Store.Web: [webHistoryURL] :: WebConfig -> !String
- Haskoin.Store.Web: [webHost] :: WebConfig -> !String
- Haskoin.Store.Web: [webMaxDiff] :: WebConfig -> !Int
- Haskoin.Store.Web: [webMaxLimits] :: WebConfig -> !WebLimits
- Haskoin.Store.Web: [webMaxPending] :: WebConfig -> !Int
- Haskoin.Store.Web: [webNoMempool] :: WebConfig -> !Bool
- Haskoin.Store.Web: [webPort] :: WebConfig -> !Int
- Haskoin.Store.Web: [webPriceGet] :: WebConfig -> !Int
- Haskoin.Store.Web: [webSlow] :: WebConfig -> !Bool
- Haskoin.Store.Web: [webStats] :: WebConfig -> !Maybe Store
- Haskoin.Store.Web: [webStore] :: WebConfig -> !Store
- Haskoin.Store.Web: [webTickerURL] :: WebConfig -> !String
- Haskoin.Store.Web: [webTimeouts] :: WebConfig -> !WebTimeouts
- Haskoin.Store.Web: [webVersion] :: WebConfig -> !String
+ Haskoin.Store: [$sel:atBlockHeight:AtTx] :: Start -> !BlockHeight
+ Haskoin.Store: [$sel:atTxHash:AtTx] :: Start -> !TxHash
+ Haskoin.Store: [$sel:block:Store] :: Store -> !BlockStore
+ Haskoin.Store: [$sel:cache:Store] :: Store -> !Maybe CacheConfig
+ Haskoin.Store: [$sel:chain:CacheConfig] :: CacheConfig -> !Chain
+ Haskoin.Store: [$sel:chain:Store] :: Store -> !Chain
+ Haskoin.Store: [$sel:connect:StoreConfig] :: StoreConfig -> !SockAddr -> WithConnection
+ Haskoin.Store: [$sel:ctx:StoreConfig] :: StoreConfig -> !Ctx
+ Haskoin.Store: [$sel:ctx:Store] :: Store -> !Ctx
+ Haskoin.Store: [$sel:db:StoreConfig] :: StoreConfig -> !FilePath
+ Haskoin.Store: [$sel:db:Store] :: Store -> !DatabaseReader
+ Haskoin.Store: [$sel:discover:StoreConfig] :: StoreConfig -> !Bool
+ Haskoin.Store: [$sel:gap:StoreConfig] :: StoreConfig -> !Word32
+ Haskoin.Store: [$sel:initGap:StoreConfig] :: StoreConfig -> !Word32
+ Haskoin.Store: [$sel:initPeers:StoreConfig] :: StoreConfig -> ![String]
+ Haskoin.Store: [$sel:limit:Limits] :: Limits -> !Word32
+ Haskoin.Store: [$sel:maxKeys:CacheConfig] :: CacheConfig -> !Integer
+ Haskoin.Store: [$sel:maxPeerLife:StoreConfig] :: StoreConfig -> !NominalDiffTime
+ Haskoin.Store: [$sel:maxPeers:StoreConfig] :: StoreConfig -> !Int
+ Haskoin.Store: [$sel:metrics:CacheConfig] :: CacheConfig -> !Maybe CacheMetrics
+ Haskoin.Store: [$sel:minAddrs:CacheConfig] :: CacheConfig -> !Int
+ Haskoin.Store: [$sel:net:StoreConfig] :: StoreConfig -> !Network
+ Haskoin.Store: [$sel:net:Store] :: Store -> !Network
+ Haskoin.Store: [$sel:noMempool:StoreConfig] :: StoreConfig -> !Bool
+ Haskoin.Store: [$sel:offset:Limits] :: Limits -> !Word32
+ Haskoin.Store: [$sel:peerMgr:Store] :: Store -> !PeerMgr
+ Haskoin.Store: [$sel:peerTimeout:StoreConfig] :: StoreConfig -> !NominalDiffTime
+ Haskoin.Store: [$sel:pub:Store] :: Store -> !Publisher StoreEvent
+ Haskoin.Store: [$sel:redis:CacheConfig] :: CacheConfig -> !Connection
+ Haskoin.Store: [$sel:redis:StoreConfig] :: StoreConfig -> !Maybe String
+ Haskoin.Store: [$sel:redisMaxKeys:StoreConfig] :: StoreConfig -> !Integer
+ Haskoin.Store: [$sel:redisMinAddrs:StoreConfig] :: StoreConfig -> !Int
+ Haskoin.Store: [$sel:redisSyncInterval:StoreConfig] :: StoreConfig -> !Int
+ Haskoin.Store: [$sel:start:Limits] :: Limits -> !Maybe Start
+ Haskoin.Store: [$sel:statsStore:StoreConfig] :: StoreConfig -> !Maybe Store
+ Haskoin.Store: [$sel:syncMempool:StoreConfig] :: StoreConfig -> !Bool
+ Haskoin.Store: [$sel:wipeMempool:StoreConfig] :: StoreConfig -> !Bool
+ Haskoin.Store: getCtx :: StoreReadBase m => m Ctx
+ Haskoin.Store.BlockStore: [$sel:chain:BlockStoreConfig] :: BlockStoreConfig -> !Chain
+ Haskoin.Store.BlockStore: [$sel:ctx:BlockStoreConfig] :: BlockStoreConfig -> !Ctx
+ Haskoin.Store.BlockStore: [$sel:db:BlockStoreConfig] :: BlockStoreConfig -> !DatabaseReader
+ Haskoin.Store.BlockStore: [$sel:net:BlockStoreConfig] :: BlockStoreConfig -> !Network
+ Haskoin.Store.BlockStore: [$sel:noMempool:BlockStoreConfig] :: BlockStoreConfig -> !Bool
+ Haskoin.Store.BlockStore: [$sel:peerMgr:BlockStoreConfig] :: BlockStoreConfig -> !PeerMgr
+ Haskoin.Store.BlockStore: [$sel:peerTimeout:BlockStoreConfig] :: BlockStoreConfig -> !NominalDiffTime
+ Haskoin.Store.BlockStore: [$sel:pub:BlockStoreConfig] :: BlockStoreConfig -> !Publisher StoreEvent
+ Haskoin.Store.BlockStore: [$sel:statsStore:BlockStoreConfig] :: BlockStoreConfig -> !Maybe Store
+ Haskoin.Store.BlockStore: [$sel:syncMempool:BlockStoreConfig] :: BlockStoreConfig -> !Bool
+ Haskoin.Store.BlockStore: [$sel:wipeMempool:BlockStoreConfig] :: BlockStoreConfig -> !Bool
+ Haskoin.Store.Cache: [$sel:chain:CacheConfig] :: CacheConfig -> !Chain
+ Haskoin.Store.Cache: [$sel:maxKeys:CacheConfig] :: CacheConfig -> !Integer
+ Haskoin.Store.Cache: [$sel:metrics:CacheConfig] :: CacheConfig -> !Maybe CacheMetrics
+ Haskoin.Store.Cache: [$sel:minAddrs:CacheConfig] :: CacheConfig -> !Int
+ Haskoin.Store.Cache: [$sel:redis:CacheConfig] :: CacheConfig -> !Connection
+ 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: [$sel:limit:Limits] :: Limits -> !Word32
+ Haskoin.Store.Common: [$sel:offset:Limits] :: Limits -> !Word32
+ Haskoin.Store.Common: [$sel:start:Limits] :: Limits -> !Maybe Start
+ Haskoin.Store.Common: getCtx :: StoreReadBase m => m Ctx
+ Haskoin.Store.Database.Reader: [$sel:ctx:DatabaseReader] :: DatabaseReader -> !Ctx
+ Haskoin.Store.Database.Reader: [$sel:db:DatabaseReader] :: DatabaseReader -> !DB
+ Haskoin.Store.Database.Reader: [$sel:initGap:DatabaseReader] :: DatabaseReader -> !Word32
+ Haskoin.Store.Database.Reader: [$sel:maxGap:DatabaseReader] :: DatabaseReader -> !Word32
+ Haskoin.Store.Database.Reader: [$sel:metrics:DatabaseReader] :: DatabaseReader -> !Maybe DataMetrics
+ Haskoin.Store.Database.Reader: [$sel:net:DatabaseReader] :: DatabaseReader -> !Network
+ Haskoin.Store.Database.Types: [$sel:address:AddrOutKey] :: AddrOutKey -> !Address
+ Haskoin.Store.Database.Types: [$sel:address:AddrTxKey] :: AddrTxKey -> !Address
+ Haskoin.Store.Database.Types: [$sel:address:BalKey] :: BalKey -> !Address
+ Haskoin.Store.Database.Types: [$sel:block:AddrOutKey] :: AddrOutKey -> !BlockRef
+ Haskoin.Store.Database.Types: [$sel:block:AddrTxKey] :: AddrTxKey -> !BlockRef
+ Haskoin.Store.Database.Types: [$sel:block:UnspentVal] :: UnspentVal -> !BlockRef
+ Haskoin.Store.Database.Types: [$sel:confirmed:BalVal] :: BalVal -> !Word64
+ Haskoin.Store.Database.Types: [$sel:hash:BlockKey] :: BlockKey -> BlockHash
+ Haskoin.Store.Database.Types: [$sel:height:HeightKey] :: HeightKey -> BlockHeight
+ Haskoin.Store.Database.Types: [$sel:outpoint:AddrOutKey] :: AddrOutKey -> !OutPoint
+ Haskoin.Store.Database.Types: [$sel:outpoint:UnspentKey] :: UnspentKey -> !OutPoint
+ Haskoin.Store.Database.Types: [$sel:received:BalVal] :: BalVal -> !Word64
+ Haskoin.Store.Database.Types: [$sel:script:OutVal] :: OutVal -> !ByteString
+ Haskoin.Store.Database.Types: [$sel:script:UnspentVal] :: UnspentVal -> !ByteString
+ Haskoin.Store.Database.Types: [$sel:short:TxKey] :: TxKey -> (Word32, Word16)
+ Haskoin.Store.Database.Types: [$sel:tx:AddrTxKey] :: AddrTxKey -> !TxRef
+ Haskoin.Store.Database.Types: [$sel:txid:TxKey] :: TxKey -> TxHash
+ Haskoin.Store.Database.Types: [$sel:txid:UnspentKey] :: UnspentKey -> !TxHash
+ Haskoin.Store.Database.Types: [$sel:txs:BalVal] :: BalVal -> !Word64
+ Haskoin.Store.Database.Types: [$sel:unconfirmed:BalVal] :: BalVal -> !Word64
+ Haskoin.Store.Database.Types: [$sel:utxo:BalVal] :: BalVal -> !Word64
+ Haskoin.Store.Database.Types: [$sel:value:OutVal] :: OutVal -> !Word64
+ Haskoin.Store.Database.Types: [$sel:value:UnspentVal] :: UnspentVal -> !Word64
+ Haskoin.Store.Manager: [$sel:block:Store] :: Store -> !BlockStore
+ Haskoin.Store.Manager: [$sel:cache:Store] :: Store -> !Maybe CacheConfig
+ Haskoin.Store.Manager: [$sel:chain:Store] :: Store -> !Chain
+ Haskoin.Store.Manager: [$sel:connect:StoreConfig] :: StoreConfig -> !SockAddr -> WithConnection
+ Haskoin.Store.Manager: [$sel:ctx:StoreConfig] :: StoreConfig -> !Ctx
+ Haskoin.Store.Manager: [$sel:ctx:Store] :: Store -> !Ctx
+ Haskoin.Store.Manager: [$sel:db:StoreConfig] :: StoreConfig -> !FilePath
+ Haskoin.Store.Manager: [$sel:db:Store] :: Store -> !DatabaseReader
+ Haskoin.Store.Manager: [$sel:discover:StoreConfig] :: StoreConfig -> !Bool
+ Haskoin.Store.Manager: [$sel:gap:StoreConfig] :: StoreConfig -> !Word32
+ Haskoin.Store.Manager: [$sel:initGap:StoreConfig] :: StoreConfig -> !Word32
+ Haskoin.Store.Manager: [$sel:initPeers:StoreConfig] :: StoreConfig -> ![String]
+ Haskoin.Store.Manager: [$sel:maxPeerLife:StoreConfig] :: StoreConfig -> !NominalDiffTime
+ Haskoin.Store.Manager: [$sel:maxPeers:StoreConfig] :: StoreConfig -> !Int
+ Haskoin.Store.Manager: [$sel:net:StoreConfig] :: StoreConfig -> !Network
+ Haskoin.Store.Manager: [$sel:net:Store] :: Store -> !Network
+ Haskoin.Store.Manager: [$sel:noMempool:StoreConfig] :: StoreConfig -> !Bool
+ Haskoin.Store.Manager: [$sel:peerMgr:Store] :: Store -> !PeerMgr
+ Haskoin.Store.Manager: [$sel:peerTimeout:StoreConfig] :: StoreConfig -> !NominalDiffTime
+ Haskoin.Store.Manager: [$sel:pub:Store] :: Store -> !Publisher StoreEvent
+ Haskoin.Store.Manager: [$sel:redis:StoreConfig] :: StoreConfig -> !Maybe String
+ Haskoin.Store.Manager: [$sel:redisMaxKeys:StoreConfig] :: StoreConfig -> !Integer
+ Haskoin.Store.Manager: [$sel:redisMinAddrs:StoreConfig] :: StoreConfig -> !Int
+ Haskoin.Store.Manager: [$sel:redisSyncInterval:StoreConfig] :: StoreConfig -> !Int
+ Haskoin.Store.Manager: [$sel:statsStore:StoreConfig] :: StoreConfig -> !Maybe Store
+ Haskoin.Store.Manager: [$sel:syncMempool:StoreConfig] :: StoreConfig -> !Bool
+ Haskoin.Store.Manager: [$sel:wipeMempool:StoreConfig] :: StoreConfig -> !Bool
+ Haskoin.Store.Web: [$sel:block:WebTimeouts] :: WebTimeouts -> !Word64
+ Haskoin.Store.Web: [$sel:defItemCount:WebLimits] :: WebLimits -> !Word32
+ Haskoin.Store.Web: [$sel:healthCheckInterval:WebConfig] :: WebConfig -> !Int
+ Haskoin.Store.Web: [$sel:host:WebConfig] :: WebConfig -> !String
+ Haskoin.Store.Web: [$sel:limits:WebConfig] :: WebConfig -> !WebLimits
+ Haskoin.Store.Web: [$sel:maxBodySize:WebLimits] :: WebLimits -> !Word32
+ Haskoin.Store.Web: [$sel:maxFullItemCount:WebLimits] :: WebLimits -> !Word32
+ Haskoin.Store.Web: [$sel:maxItemCount:WebLimits] :: WebLimits -> !Word32
+ Haskoin.Store.Web: [$sel:maxLaggingBlocks:WebConfig] :: WebConfig -> !Int
+ Haskoin.Store.Web: [$sel:maxOffset:WebLimits] :: WebLimits -> !Word32
+ Haskoin.Store.Web: [$sel:maxPendingTxs:WebConfig] :: WebConfig -> !Int
+ Haskoin.Store.Web: [$sel:minPeers:WebConfig] :: WebConfig -> !Int
+ Haskoin.Store.Web: [$sel:noBlockchainInfo:WebConfig] :: WebConfig -> !Bool
+ Haskoin.Store.Web: [$sel:noMempool:WebConfig] :: WebConfig -> !Bool
+ Haskoin.Store.Web: [$sel:noSlow:WebConfig] :: WebConfig -> !Bool
+ Haskoin.Store.Web: [$sel:port:WebConfig] :: WebConfig -> !Int
+ Haskoin.Store.Web: [$sel:priceHistoryURL:WebConfig] :: WebConfig -> !String
+ Haskoin.Store.Web: [$sel:requestTimeout:WebLimits] :: WebLimits -> !Word32
+ Haskoin.Store.Web: [$sel:statsStore:WebConfig] :: WebConfig -> !Maybe Store
+ Haskoin.Store.Web: [$sel:store:WebConfig] :: WebConfig -> !Store
+ Haskoin.Store.Web: [$sel:tickerRefresh:WebConfig] :: WebConfig -> !Int
+ Haskoin.Store.Web: [$sel:tickerURL:WebConfig] :: WebConfig -> !String
+ Haskoin.Store.Web: [$sel:timeouts:WebConfig] :: WebConfig -> !WebTimeouts
+ Haskoin.Store.Web: [$sel:tx:WebTimeouts] :: WebTimeouts -> !Word64
+ Haskoin.Store.Web: [$sel:version:WebConfig] :: WebConfig -> !String
+ Haskoin.Store.Web: [$sel:xpubGap:WebLimits] :: WebLimits -> !Word32
+ Haskoin.Store.Web: [$sel:xpubGapInit:WebLimits] :: WebLimits -> !Word32
- Haskoin.Store: Store :: !PeerManager -> !Chain -> !BlockStore -> !DatabaseReader -> !Maybe CacheConfig -> !Publisher StoreEvent -> !Network -> Store
+ Haskoin.Store: Store :: !PeerMgr -> !Chain -> !BlockStore -> !DatabaseReader -> !Maybe CacheConfig -> !Publisher StoreEvent -> !Network -> !Ctx -> Store
- Haskoin.Store: StoreConfig :: !Int -> ![String] -> !Bool -> !FilePath -> !Network -> !Maybe String -> !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 Store -> !Int -> StoreConfig
- Haskoin.Store: class Monad m => StoreReadBase m
+ Haskoin.Store: class (Monad m) => StoreReadBase m
- Haskoin.Store: class StoreReadBase m => StoreReadExtra m
+ Haskoin.Store: class (StoreReadBase m) => StoreReadExtra m
- Haskoin.Store.BlockStore: BlockStoreConfig :: !PeerManager -> !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 Store -> BlockStoreConfig
- Haskoin.Store.Common: class Monad m => StoreReadBase m
+ Haskoin.Store.Common: class (Monad m) => StoreReadBase m
- Haskoin.Store.Common: class StoreReadBase m => StoreReadExtra m
+ Haskoin.Store.Common: class (StoreReadBase m) => StoreReadExtra m
- Haskoin.Store.Common: deriveFunction :: DeriveType -> DeriveAddr
+ Haskoin.Store.Common: deriveFunction :: Ctx -> DeriveType -> DeriveAddr
- Haskoin.Store.Database.Reader: DatabaseReader :: !DB -> !Word32 -> !Word32 -> !Network -> !Maybe DataMetrics -> DatabaseReader
+ Haskoin.Store.Database.Reader: DatabaseReader :: !DB -> !Word32 -> !Word32 -> !Network -> !Maybe DataMetrics -> !Ctx -> DatabaseReader
- Haskoin.Store.Database.Reader: withDatabaseReader :: MonadUnliftIO m => Network -> Word32 -> Word32 -> FilePath -> Maybe DataMetrics -> DatabaseReaderT m a -> m a
+ Haskoin.Store.Database.Reader: withDatabaseReader :: MonadUnliftIO m => Network -> Ctx -> Word32 -> Word32 -> FilePath -> Maybe DataMetrics -> DatabaseReaderT m a -> m a
- Haskoin.Store.Database.Types: toUnspent :: AddrOutKey -> OutVal -> Unspent
+ Haskoin.Store.Database.Types: toUnspent :: Ctx -> AddrOutKey -> OutVal -> Unspent
- Haskoin.Store.Database.Types: valToUnspent :: OutPoint -> UnspentVal -> Unspent
+ Haskoin.Store.Database.Types: valToUnspent :: Ctx -> OutPoint -> UnspentVal -> Unspent
- Haskoin.Store.Database.Writer: runWriter :: MonadIO m => DatabaseReader -> WriterT m a -> m a
+ Haskoin.Store.Database.Writer: runWriter :: MonadIO m => Network -> Ctx -> DatabaseReader -> WriterT m a -> m a
- Haskoin.Store.Manager: Store :: !PeerManager -> !Chain -> !BlockStore -> !DatabaseReader -> !Maybe CacheConfig -> !Publisher StoreEvent -> !Network -> Store
+ Haskoin.Store.Manager: Store :: !PeerMgr -> !Chain -> !BlockStore -> !DatabaseReader -> !Maybe CacheConfig -> !Publisher StoreEvent -> !Network -> !Ctx -> Store
- Haskoin.Store.Manager: StoreConfig :: !Int -> ![String] -> !Bool -> !FilePath -> !Network -> !Maybe String -> !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 Store -> !Int -> StoreConfig
- Haskoin.Store.Web: WebConfig :: !String -> !Int -> !Store -> !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 -> !WebTimeouts -> !String -> !Bool -> !Maybe Store -> !Int -> !String -> !String -> !Bool -> !Bool -> !Int -> WebConfig
- Haskoin.Store.Web: data Except
+ Haskoin.Store.Web: data () => Except

Files

+ CHANGELOG.md view
@@ -0,0 +1,1758 @@+# 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.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 view
@@ -0,0 +1,57 @@+# 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,815 +1,645 @@ {-# LANGUAGE ApplicativeDo #-} {-# LANGUAGE CPP #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE TupleSections #-}--module Main where--import Control.Applicative ((<|>))-import Control.Arrow (second)-import Control.Monad (when)-import Control.Monad.Logger-  ( LogLevel (..),-    filterLogger,-    logInfoS,-    runStderrLoggingT,-  )-import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)-import Data.Char (toLower)-import Data.Default (Default (..))-import Data.List (intercalate)-import Data.Maybe (fromMaybe)-import Data.String.Conversions (cs)-import qualified Data.Text as T-import Data.Word (Word32)-import Haskoin-  ( Network (..),-    allNets,-    bch,-    bchRegTest,-    bchTest,-    bchTest4,-    btc,-    btcRegTest,-    btcTest,-    eitherToMaybe,-  )-import Haskoin.Node (withConnection)-import Haskoin.Store-  ( StoreConfig (..),-    WebConfig (..),-    WebLimits (..),-    WebTimeouts (..),-    runWeb,-    withStore,-  )-import Haskoin.Store.Stats (withStats)-import Options.Applicative-  ( Parser,-    auto,-    eitherReader,-    execParser,-    flag,-    fullDesc,-    header,-    help,-    helper,-    info,-    long,-    many,-    metavar,-    option,-    progDesc,-    short,-    showDefault,-    strOption,-    switch,-    value,-  )-import System.Exit (exitSuccess)-import System.FilePath ((</>))-import System.IO.Unsafe (unsafePerformIO)-import Text.Read (readMaybe)-import UnliftIO (MonadIO)-import UnliftIO.Directory-  ( createDirectoryIfMissing,-    getAppUserDataDirectory,-  )-import UnliftIO.Environment (lookupEnv)--version :: String--#ifdef CURRENT_PACKAGE_VERSION-version = CURRENT_PACKAGE_VERSION-#else-version = "Unavailable"-#endif--data Config = Config-  { configDir :: !FilePath,-    configHost :: !String,-    configPort :: !Int,-    configNetwork :: !String,-    configDiscover :: !Bool,-    configPeers :: ![String],-    configVersion :: !Bool,-    configDebug :: !Bool,-    configMaxPending :: !Int,-    configWebLimits :: !WebLimits,-    configWebTimeouts :: !WebTimeouts,-    configRedis :: !Bool,-    configRedisURL :: !String,-    configRedisMin :: !Int,-    configRedisMax :: !Integer,-    configWipeMempool :: !Bool,-    configNoMempool :: !Bool,-    configSyncMempool :: !Bool,-    configPeerTimeout :: !Int,-    configPeerMaxLife :: !Int,-    configMaxPeers :: !Int,-    configMaxDiff :: !Int,-    configStatsd :: !Bool,-    configStatsdHost :: !String,-    configStatsdPort :: !Int,-    configStatsdPrefix :: !String,-    configWebPriceGet :: !Int,-    configWebTickerURL :: !String,-    configWebHistoryURL :: !String,-    configWebNoBlockchain :: !Bool,-    configWebNoSlow :: !Bool,-    configHealthCheckInterval :: !Int,-    configCacheMempoolSync :: !Int-  }--instance Default Config where-  def =-    Config-      { configDir = defDirectory,-        configHost = defHost,-        configPort = defPort,-        configNetwork = defNetwork,-        configDiscover = defDiscover,-        configPeers = defPeers,-        configVersion = False,-        configDebug = defDebug,-        configMaxPending = defMaxPending,-        configWebLimits = defWebLimits,-        configWebTimeouts = defWebTimeouts,-        configRedis = defRedis,-        configRedisURL = defRedisURL,-        configRedisMin = defRedisMin,-        configRedisMax = defRedisMax,-        configWipeMempool = defWipeMempool,-        configNoMempool = defNoMempool,-        configSyncMempool = defSyncMempool,-        configPeerTimeout = defPeerTimeout,-        configPeerMaxLife = defPeerMaxLife,-        configMaxPeers = defMaxPeers,-        configMaxDiff = defMaxDiff,-        configStatsd = defStatsd,-        configStatsdHost = defStatsdHost,-        configStatsdPort = defStatsdPort,-        configStatsdPrefix = defStatsdPrefix,-        configWebPriceGet = defWebPriceGet,-        configWebTickerURL = defWebTickerURL,-        configWebHistoryURL = defWebHistoryURL,-        configWebNoBlockchain = defWebNoBlockchain,-        configWebNoSlow = defWebNoSlow,-        configHealthCheckInterval = defHealthCheckInterval,-        configCacheMempoolSync = defCacheMempoolSync-      }--defEnv :: MonadIO m => String -> a -> (String -> Maybe a) -> m a-defEnv e d p = do-  ms <- lookupEnv e-  return $ fromMaybe d $ p =<< ms--defMaxPending :: Int-defMaxPending =-  unsafePerformIO $-    defEnv "MAX_PENDING_TXS" 100 readMaybe-{-# NOINLINE defMaxPending #-}--defMaxPeers :: Int-defMaxPeers =-  unsafePerformIO $-    defEnv "MAX_PEERS" 20 readMaybe-{-# NOINLINE defMaxPeers #-}--defDirectory :: FilePath-defDirectory = unsafePerformIO $ do-  d <- getAppUserDataDirectory "haskoin-store"-  defEnv "DIR" d pure-{-# NOINLINE defDirectory #-}--defHost :: String-defHost =-  unsafePerformIO $-    defEnv "HOST" "*" pure-{-# NOINLINE defHost #-}--defPort :: Int-defPort =-  unsafePerformIO $-    defEnv "PORT" 3000 readMaybe-{-# NOINLINE defPort #-}--defNetwork :: String-defNetwork =-  unsafePerformIO $-    defEnv "NET" "bch" pure-{-# NOINLINE defNetwork #-}--defRedisMin :: Int-defRedisMin =-  unsafePerformIO $-    defEnv "CACHE_MIN" 100 readMaybe-{-# NOINLINE defRedisMin #-}--defRedis :: Bool-defRedis =-  unsafePerformIO $-    defEnv "CACHE" False parseBool-{-# NOINLINE defRedis #-}--defSyncMempool :: Bool-defSyncMempool =-  unsafePerformIO $-    defEnv "SYNC_MEMPOOL" False parseBool-{-# NOINLINE defSyncMempool #-}--defDiscover :: Bool-defDiscover =-  unsafePerformIO $-    defEnv "DISCOVER" False parseBool-{-# NOINLINE defDiscover #-}--defPeers :: [String]-defPeers =-  unsafePerformIO $-    defEnv "PEER" [] (pure . words)-{-# NOINLINE defPeers #-}--defDebug :: Bool-defDebug =-  unsafePerformIO $-    defEnv "DEBUG" False parseBool-{-# NOINLINE defDebug #-}--defWebLimits :: WebLimits-defWebLimits = unsafePerformIO $ do-  max_limit <- defEnv "MAX_LIMIT" (maxLimitCount def) readMaybe-  max_full <- defEnv "MAX_FULL" (maxLimitFull def) readMaybe-  max_offset <- defEnv "MAX_OFFSET" (maxLimitOffset def) readMaybe-  def_limit <- defEnv "DEF_LIMIT" (maxLimitDefault def) readMaybe-  max_gap <- defEnv "MAX_GAP" (maxLimitGap def) readMaybe-  init_gap <- defEnv "INIT_GAP" (maxLimitInitialGap def) readMaybe-  max_body <- defEnv "MAX_BODY" (maxLimitBody def) readMaybe-  max_timeout <- defEnv "MAX_TIMEOUT" (maxLimitTimeout def) readMaybe-  return-    WebLimits-      { maxLimitCount = max_limit,-        maxLimitFull = max_full,-        maxLimitOffset = max_offset,-        maxLimitDefault = def_limit,-        maxLimitGap = max_gap,-        maxLimitInitialGap = init_gap,-        maxLimitBody = max_body,-        maxLimitTimeout = max_timeout-      }-{-# NOINLINE defWebLimits #-}--defWebNoBlockchain :: Bool-defWebNoBlockchain =-  unsafePerformIO $-    defEnv "NO_BLOCKCHAIN" False parseBool-{-# NOINLINE defWebNoBlockchain #-}--defWebNoSlow :: Bool-defWebNoSlow =-  unsafePerformIO $-    defEnv "NO_SLOW" False parseBool-{-# NOINLINE defWebNoSlow #-}--defWebTimeouts :: WebTimeouts-defWebTimeouts = unsafePerformIO $ do-  block_timeout <- defEnv "BLOCK_TIMEOUT" (blockTimeout def) readMaybe-  tx_timeout <- defEnv "TX_TIMEOUT" (txTimeout def) readMaybe-  return-    WebTimeouts-      { txTimeout = tx_timeout,-        blockTimeout = block_timeout-      }-{-# NOINLINE defWebTimeouts #-}--defWipeMempool :: Bool-defWipeMempool =-  unsafePerformIO $-    defEnv "WIPE_MEMPOOL" False parseBool-{-# NOINLINE defWipeMempool #-}--defNoMempool :: Bool-defNoMempool =-  unsafePerformIO $-    defEnv "NO_MEMPOOL" False parseBool-{-# NOINLINE defNoMempool #-}--defRedisURL :: String-defRedisURL =-  unsafePerformIO $-    defEnv "REDIS" "" pure-{-# NOINLINE defRedisURL #-}--defRedisMax :: Integer-defRedisMax =-  unsafePerformIO $-    defEnv "CACHE_KEYS" 100000000 readMaybe-{-# NOINLINE defRedisMax #-}--defPeerTimeout :: Int-defPeerTimeout =-  unsafePerformIO $-    defEnv "PEER_TIMEOUT" 120 readMaybe-{-# NOINLINE defPeerTimeout #-}--defPeerMaxLife :: Int-defPeerMaxLife =-  unsafePerformIO $-    defEnv "PEER_MAX_LIFE" (48 * 3600) readMaybe-{-# NOINLINE defPeerMaxLife #-}--defMaxDiff :: Int-defMaxDiff =-  unsafePerformIO $-    defEnv "MAX_DIFF" 2 readMaybe-{-# NOINLINE defMaxDiff #-}--defStatsd :: Bool-defStatsd =-  unsafePerformIO $-    defEnv "STATSD" False parseBool-{-# NOINLINE defStatsd #-}--defStatsdHost :: String-defStatsdHost =-  unsafePerformIO $-    defEnv "STATSD_HOST" "localhost" pure-{-# NOINLINE defStatsdHost #-}--defStatsdPort :: Int-defStatsdPort =-  unsafePerformIO $-    defEnv "STATSD_PORT" 8125 readMaybe-{-# NOINLINE defStatsdPort #-}--defWebPriceGet :: Int-defWebPriceGet =-  unsafePerformIO $-    defEnv "WEB_PRICE_GET" (90 * 1000 * 1000) readMaybe-{-# NOINLINE defWebPriceGet #-}--defWebTickerURL :: String-defWebTickerURL =-  unsafePerformIO $-    defEnv "PRICE_TICKER_URL" "https://api.blockchain.info/ticker" pure-{-# NOINLINE defWebTickerURL #-}--defWebHistoryURL :: String-defWebHistoryURL =-  unsafePerformIO $-    defEnv "PRICE_HISTORY_URL" "https://api.blockchain.info/price/index-series" pure-{-# NOINLINE defWebHistoryURL #-}--defStatsdPrefix :: String-defStatsdPrefix =-  unsafePerformIO $-    runMaybeT go >>= \case-      Nothing -> return "haskoin_store"-      Just x -> return x-  where-    go = prefix <|> nomad-    prefix = MaybeT $ lookupEnv "STATSD_PREFIX"-    nomad = do-      task <- MaybeT $ lookupEnv "NOMAD_TASK_NAME"-      service <- MaybeT $ lookupEnv "NOMAD_ALLOC_INDEX"-      return $ "app." <> task <> "." <> service-{-# NOINLINE defStatsdPrefix #-}--defHealthCheckInterval :: Int-defHealthCheckInterval =-  unsafePerformIO $-    defEnv "HEALTH_CHECK_INTERVAL" 30 readMaybe-{-# NOINLINE defHealthCheckInterval #-}--defCacheMempoolSync :: Int-defCacheMempoolSync =-  unsafePerformIO $-    defEnv "CACHE_MEMPOOL_SYNC" 30 readMaybe-{-# NOINLINE defCacheMempoolSync #-}--netNames :: String-netNames = intercalate "|" (map getNetworkName allNets)--parseBool :: String -> Maybe Bool-parseBool str = case map toLower str of-  "yes" -> Just True-  "true" -> Just True-  "on" -> Just True-  "1" -> Just True-  "no" -> Just False-  "false" -> Just False-  "off" -> Just False-  "0" -> Just False-  _ -> Nothing--config :: Parser Config-config = do-  configDir <--    strOption $-      metavar "PATH"-        <> long "dir"-        <> short 'd'-        <> help "Data directory"-        <> showDefault-        <> value (configDir def)-  configHost <--    strOption $-      metavar "HOST"-        <> long "host"-        <> help "Host to bind"-        <> showDefault-        <> value (configHost def)-  configPort <--    option auto $-      metavar "INT"-        <> long "port"-        <> help "REST API listening port"-        <> showDefault-        <> value (configPort def)-  configNetwork <--    strOption $-      metavar netNames-        <> long "net"-        <> short 'n'-        <> help "Network to connect to"-        <> showDefault-        <> value (configNetwork def)-  configDiscover <--    flag (configDiscover def) True $-      long "discover"-        <> help "Peer discovery"-  configPeers <--    fmap (mappend defPeers) $-      many $-        option auto $-          metavar "HOST"-            <> long "peer"-            <> short 'p'-            <> help "Network peer (as many as required)"-  configMaxPeers <--    option auto $-      metavar "INT"-        <> long "max-peers"-        <> help "Do not connect to more than this many peers"-        <> showDefault-        <> value (configMaxPeers def)-  configVersion <--    switch $-      long "version"-        <> short 'v'-        <> help "Show version"-  configDebug <--    flag (configDebug def) True $-      long "debug"-        <> help "Show debug messages"-  maxLimitCount <--    option auto $-      metavar "INT"-        <> long "max-limit"-        <> help "Hard limit for simple listings (0 = inf)"-        <> showDefault-        <> value (maxLimitCount (configWebLimits def))-  maxLimitFull <--    option auto $-      metavar "INT"-        <> long "max-full"-        <> help "Hard limit for full listings (0 = inf)"-        <> showDefault-        <> value (maxLimitFull (configWebLimits def))-  maxLimitOffset <--    option auto $-      metavar "INT"-        <> long "max-offset"-        <> help "Hard limit for offsets (0 = inf)"-        <> showDefault-        <> value (maxLimitOffset (configWebLimits def))-  maxLimitDefault <--    option auto $-      metavar "INT"-        <> long "def-limit"-        <> help "Soft default limit (0 = inf)"-        <> showDefault-        <> value (maxLimitDefault (configWebLimits def))-  maxLimitGap <--    option auto $-      metavar "INT"-        <> long "max-gap"-        <> help "Max gap for xpub queries"-        <> showDefault-        <> value (maxLimitGap (configWebLimits def))-  maxLimitInitialGap <--    option auto $-      metavar "INT"-        <> long "init-gap"-        <> help "Max gap for empty xpub"-        <> showDefault-        <> value (maxLimitInitialGap (configWebLimits def))-  maxLimitBody <--    option auto $-      metavar "BYTES"-        <> long "max-body"-        <> help "Maximum request body size"-        <> showDefault-        <> value (maxLimitBody (configWebLimits def))-  maxLimitTimeout <--    option auto $-      metavar "SECONDS"-        <> long "max-timeout"-        <> help "Web request timeout"-        <> showDefault-        <> value (maxLimitTimeout (configWebLimits def))-  blockTimeout <--    option auto $-      metavar "SECONDS"-        <> long "block-timeout"-        <> help "Last block mined health timeout"-        <> showDefault-        <> value (blockTimeout (configWebTimeouts def))-  txTimeout <--    option auto $-      metavar "SECONDS"-        <> long "tx-timeout"-        <> help "Last tx recived health timeout"-        <> showDefault-        <> value (txTimeout (configWebTimeouts def))-  configPeerTimeout <--    option auto $-      metavar "SECONDS"-        <> long "peer-timeout"-        <> help "Unresponsive peer timeout"-        <> showDefault-        <> value (configPeerTimeout def)-  configPeerMaxLife <--    option auto $-      metavar "SECONDS"-        <> long "peer-max-life"-        <> help "Disconnect peers older than this"-        <> showDefault-        <> value (configPeerMaxLife def)-  configMaxPending <--    option auto $-      metavar "INT"-        <> long "max-pending-txs"-        <> help "Maximum pending txs to fail health check"-        <> showDefault-        <> value (configMaxPending def)-  configRedis <--    flag (configRedis def) True $-      long "cache"-        <> help "Redis cache for extended public keys"-  configRedisURL <--    strOption $-      metavar "URL"-        <> long "redis"-        <> help "URL for Redis cache"-        <> value (configRedisURL def)-  configRedisMin <--    option auto $-      metavar "INT"-        <> long "cache-min"-        <> help "Minimum used xpub addresses to cache"-        <> showDefault-        <> value (configRedisMin def)-  configRedisMax <--    option auto $-      metavar "INT"-        <> long "cache-keys"-        <> help "Maximum number of keys in Redis xpub cache"-        <> showDefault-        <> value (configRedisMax def)-  configNoMempool <--    flag (configNoMempool def) True $-      long "no-mempool"-        <> help "Do not index new mempool transactions"-  configWipeMempool <--    flag (configWipeMempool def) True $-      long "wipe-mempool"-        <> help "Wipe mempool at start"-  configSyncMempool <--    flag (configSyncMempool def) True $-      long "sync-mempool"-        <> help "Sync mempool from peers"-  configMaxDiff <--    option auto $-      metavar "INT"-        <> long "max-diff"-        <> help "Maximum difference between headers and blocks"-        <> showDefault-        <> value (configMaxDiff def)-  configStatsd <--    flag (configStatsd def) True $-      long "statsd"-        <> help "Enable statsd metrics"-  configStatsdHost <--    strOption $-      metavar "HOST"-        <> long "statsd-host"-        <> help "Host to send statsd metrics"-        <> showDefault-        <> value (configStatsdHost def)-  configStatsdPort <--    option auto $-      metavar "PORT"-        <> long "statsd-port"-        <> help "Port to send statsd metrics"-        <> showDefault-        <> value (configStatsdPort def)-  configStatsdPrefix <--    strOption $-      metavar "PREFIX"-        <> long "statsd-prefix"-        <> help "Prefix for statsd metrics"-        <> showDefault-        <> value (configStatsdPrefix def)-  configWebPriceGet <--    option auto $-      metavar "MICROSECONDS"-        <> long "web-price-get"-        <> help "How often to retrieve price information"-        <> showDefault-        <> value (configWebPriceGet def)-  configWebNoBlockchain <--    flag (configWebNoBlockchain def) False $-      long "no-blockchain"-        <> help "Disable Blockchain.info compatibility layer"-  configWebTickerURL <--    strOption $-      metavar "URL"-        <> long "price-ticker-url"-        <> help "Blockchain.info price ticker URL"-        <> showDefault-        <> value (configWebTickerURL def)-  configWebHistoryURL <--    strOption $-      metavar "URL"-        <> long "price-history-url"-        <> help "Blockchain.info price history URL"-        <> showDefault-        <> value (configWebHistoryURL def)-  configWebNoSlow <--    flag (configWebNoSlow def) False $-      long "no-slow"-        <> help "Disable non-scalable API calls (recommend also --no-blockchain)"-  configHealthCheckInterval <--    option auto $-      metavar "SECONDS"-        <> long "health-check-interval"-        <> help "Background check update interval"-        <> showDefault-        <> value (configHealthCheckInterval def)-  configCacheMempoolSync <--    option auto $-      metavar "SECONDS"-        <> long "cache-mempool-sync"-        <> help "Sync mempool to cache interval"-        <> showDefault-        <> value (configCacheMempoolSync def)-  pure-    Config-      { configWebLimits = WebLimits {..},-        configWebTimeouts = WebTimeouts {..},-        ..-      }--networkReader :: String -> Either String Network-networkReader s-  | s == getNetworkName btc = Right btc-  | s == getNetworkName btcTest = Right btcTest-  | s == getNetworkName btcRegTest = Right btcRegTest-  | s == getNetworkName bch = Right bch-  | s == getNetworkName bchTest = Right bchTest-  | s == getNetworkName bchTest4 = Right bchTest4-  | s == getNetworkName bchRegTest = Right bchRegTest-  | otherwise = Left "Network name invalid"--peerReader :: String -> Either String String-peerReader "" = Left "Peer cannot be blank"-peerReader s = Right s--main :: IO ()-main = do-  conf <- execParser opts-  when (configVersion conf) $ do-    putStrLn version-    exitSuccess-  if null (configPeers conf) && not (configDiscover conf)-    then run conf {configDiscover = True}-    else run conf-  where-    opts =-      info (helper <*> config) $-        fullDesc-          <> progDesc "Bitcoin (BCH & BTC) block chain index with HTTP API"-          <> Options.Applicative.header-            ("haskoin-store version " <> version)--run :: Config -> IO ()-run-  Config-    { configHost = host,-      configPort = port,-      configNetwork = net_str,-      configDiscover = disc,-      configPeers = peers,-      configDir = db_dir,-      configDebug = deb,-      configMaxPending = pend,-      configWebLimits = limits,-      configWebTimeouts = tos,-      configRedis = redis,-      configRedisURL = redisurl,-      configRedisMin = cachemin,-      configRedisMax = redismax,-      configWipeMempool = wipemempool,-      configPeerTimeout = peertimeout,-      configPeerMaxLife = peerlife,-      configMaxPeers = maxpeers,-      configMaxDiff = maxdiff,-      configNoMempool = nomem,-      configSyncMempool = syncmem,-      configStatsd = statsd,-      configStatsdHost = statsdhost,-      configStatsdPort = statsdport,-      configStatsdPrefix = statsdpfx,-      configWebPriceGet = wpget,-      configWebTickerURL = wturl,-      configWebHistoryURL = whurl,-      configWebNoSlow = no_slow,-      configWebNoBlockchain = no_blockchain,-      configHealthCheckInterval = health_check_interval,-      configCacheMempoolSync = cachesync-    } =-    runStderrLoggingT . filterLogger l . with_stats $ \stats -> do-      net <- case networkReader net_str of-        Right n -> return n-        Left e -> error e-      $(logInfoS) "Main" $-        "Creating working directory if not found: " <> cs (wd net)-      createDirectoryIfMissing True (wd net)-      let scfg =-            StoreConfig-              { storeConfMaxPeers = maxpeers,-                storeConfInitPeers = peers,-                storeConfDiscover = disc,-                storeConfDB = wd net </> "db",-                storeConfNetwork = net,-                storeConfCache =-                  if redis-                    then Just redisurl-                    else Nothing,-                storeConfGap = maxLimitGap limits,-                storeConfInitialGap = maxLimitInitialGap limits,-                storeConfCacheMin = cachemin,-                storeConfMaxKeys = redismax,-                storeConfWipeMempool = wipemempool,-                storeConfNoMempool = nomem,-                storeConfSyncMempool = syncmem,-                storeConfPeerTimeout = fromIntegral peertimeout,-                storeConfPeerMaxLife = fromIntegral peerlife,-                storeConfConnect = withConnection,-                storeConfStats = stats,-                storeConfCacheMempoolSync = cachesync-              }-      withStore scfg $ \st ->-        runWeb-          WebConfig-            { webHost = host,-              webPort = port,-              webStore = st,-              webMaxLimits = limits,-              webTimeouts = tos,-              webMaxPending = pend,-              webVersion = version,-              webMaxDiff = maxdiff,-              webNoMempool = nomem,-              webStats = stats,-              webPriceGet = wpget,-              webTickerURL = wturl,-              webHistoryURL = whurl,-              webSlow = not no_slow,-              webBlockchain = not no_blockchain,-              webHealthCheckInterval = health_check_interval-            }-    where-      with_stats go-        | statsd = do-            $(logInfoS) "Main" $-              "Sending stats to "-                <> T.pack statsdhost-                <> ":"-                <> cs (show statsdport)-                <> " with prefix: "-                <> T.pack statsdpfx-            withStats-              (T.pack statsdhost)-              statsdport-              (T.pack statsdpfx)-              (go . Just)-        | otherwise = go Nothing-      l _ lvl-        | deb = True-        | otherwise = LevelInfo <= lvl-      wd net = db_dir </> getNetworkName net+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE ImportQualifiedPost #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE NoFieldSelectors #-}++module Main where++import Control.Applicative ((<|>))+import Control.Arrow (second)+import Control.Monad (when)+import Control.Monad.Cont (ContT (ContT), lift, runContT)+import Control.Monad.Logger+  ( LogLevel (..),+    filterLogger,+    logInfoS,+    runStderrLoggingT,+  )+import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)+import Data.Char (toLower)+import Data.Default (Default (..))+import Data.List (intercalate)+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,+  )+import Haskoin.Node (withConnection)+import Haskoin.Store+  ( StoreConfig (..),+    WebConfig (..),+    WebLimits (..),+    WebTimeouts (..),+    runWeb,+    withStore,+  )+import Haskoin.Store.Stats (withStats)+import Options.Applicative+  ( Parser,+    auto,+    eitherReader,+    execParser,+    flag,+    fullDesc,+    header,+    help,+    helper,+    info,+    long,+    many,+    metavar,+    option,+    progDesc,+    short,+    showDefault,+    strOption,+    switch,+    value,+  )+import System.Exit (exitSuccess)+import System.FilePath ((</>))+import System.IO.Unsafe (unsafePerformIO)+import Text.Read (readMaybe)+import UnliftIO (MonadIO)+import UnliftIO.Directory+  ( createDirectoryIfMissing,+    getAppUserDataDirectory,+  )+import UnliftIO.Environment (lookupEnv)++version :: String++#ifdef CURRENT_PACKAGE_VERSION+version = CURRENT_PACKAGE_VERSION+#else+version = "Unavailable"+#endif++data Config = Config+  { dir :: !FilePath,+    host :: !String,+    port :: !Int,+    net :: !String,+    discover :: !Bool,+    peers :: ![String],+    version :: !Bool,+    debug :: !Bool,+    maxPendingTxs :: !Int,+    maxLaggingBlocks :: !Int,+    minPeers :: !Int,+    webLimits :: !WebLimits,+    webTimeouts :: !WebTimeouts,+    redis :: !Bool,+    redisURL :: !String,+    redisMinAddrs :: !Int,+    redisMaxKeys :: !Integer,+    redisSyncInterval :: !Int,+    noMempool :: !Bool,+    wipeMempool :: !Bool,+    syncMempool :: !Bool,+    peerTimeout :: !Int,+    maxPeerLife :: !Int,+    maxPeers :: !Int,+    statsd :: !Bool,+    statsdHost :: !String,+    statsdPort :: !Int,+    statsdPrefix :: !String,+    tickerRefresh :: !Int,+    tickerURL :: !String,+    priceHistoryURL :: !String,+    noBlockchainInfo :: !Bool,+    noSlow :: !Bool,+    healthCheckInterval :: !Int+  }++env :: (MonadIO m) => String -> a -> (String -> Maybe a) -> m a+env e d p = do+  ms <- lookupEnv e+  return $ fromMaybe d $ p =<< ms++defConfig :: (MonadIO m) => m Config+defConfig = do+  dir <-+    getDir+  host <-+    env "HOST" "*" pure+  port <-+    env "PORT" 3000 readMaybe+  net <-+    env "NET" "bch" pure+  discover <-+    env "DISCOVER" False parseBool+  peers <-+    env "PEER" [] (pure . words)+  debug <-+    env "DEBUG" False parseBool+  maxPendingTxs <-+    env "MAX_PENDING_TXS" 10000 readMaybe+  maxLaggingBlocks <-+    env "MAX_LAGGING_BLOCKS" 3 readMaybe+  minPeers <-+    env "MIN_PEERS" 1 readMaybe+  webLimits <-+    getWebLimits+  webTimeouts <-+    getWebTimeouts+  redis <-+    env "REDIS" False parseBool+  redisURL <-+    env "REDIS_URL" "" pure+  redisMinAddrs <-+    env "REDIS_MIN_ADDRS" 100 readMaybe+  redisMaxKeys <-+    env "REDIS_MAX_KEYS" 100000000 readMaybe+  redisSyncInterval <-+    env "REDIS_SYNC_INTERVAL" 30 readMaybe+  noMempool <-+    env "NO_MEMPOOL" False parseBool+  wipeMempool <-+    env "WIPE_MEMPOOL" False parseBool+  syncMempool <-+    env "SYNC_MEMPOOL" False parseBool+  peerTimeout <-+    env "PEER_TIMEOUT" 120 readMaybe+  maxPeerLife <-+    env "MAX_PEER_LIFE" (48 * 3600) readMaybe+  maxPeers <-+    env "MAX_PEERS" 20 readMaybe+  statsd <-+    env "STATSD" False parseBool+  statsdHost <-+    env "STATSD_HOST" "localhost" pure+  statsdPort <-+    env "STATSD_PORT" 8125 readMaybe+  statsdPrefix <-+    getStatsdPrefix+  tickerRefresh <-+    env "TICKER_REFRESH" (90 * 1000 * 1000) readMaybe+  tickerURL <-+    env "TICKER_URL" tickerString pure+  priceHistoryURL <-+    env "PRICE_HISTORY_URL" priceHistoryString pure+  noBlockchainInfo <-+    env "NO_BLOCKCHAIN_INFO" False parseBool+  noSlow <-+    env "NO_SLOW" False parseBool+  healthCheckInterval <-+    env "HEALTH_CHECK_INTERVAL" 30 readMaybe+  return Config {version = False, ..}+  where+    tickerString =+      "https://api.blockchain.info/ticker"+    priceHistoryString =+      "https://api.blockchain.info/price/index-series"+    getDir =+      getAppUserDataDirectory "haskoin-store" >>= \d ->+        env "DIR" d pure+    getWebLimits = do+      let WebLimits {..} = def+      maxItemCount <-+        env "MAX_ITEM_COUNT" maxItemCount readMaybe+      maxFullItemCount <-+        env "MAX_FULL_ITEM_COUNT" maxFullItemCount readMaybe+      maxOffset <-+        env "MAX_OFFSET" maxOffset readMaybe+      defItemCount <-+        env "DEF_ITEM_COUNT" defItemCount readMaybe+      xpubGap <-+        env "XPUB_GAP" xpubGap readMaybe+      xpubGapInit <-+        env "XPUB_GAP_INIT" xpubGapInit readMaybe+      maxBodySize <-+        env "MAX_BODY_SIZE" maxBodySize readMaybe+      requestTimeout <-+        env "REQUEST_TIMEOUT" requestTimeout 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 =+            MaybeT $ lookupEnv "STATSD_PREFIX"+          nomad = do+            task <-+              MaybeT $ lookupEnv "NOMAD_TASK_NAME"+            service <-+              MaybeT $ lookupEnv "NOMAD_ALLOC_INDEX"+            return $ "app." <> task <> "." <> service+      fromMaybe "haskoin_store" <$> runMaybeT go++netNames :: String+netNames = intercalate "|" $ map (.name) allNets++parseBool :: String -> Maybe Bool+parseBool str = case map toLower str of+  "yes" -> Just True+  "true" -> Just True+  "on" -> Just True+  "1" -> Just True+  "no" -> Just False+  "false" -> Just False+  "off" -> Just False+  "0" -> Just False+  _ -> Nothing++config :: Config -> Parser Config+config c = do+  dir <-+    strOption $+      metavar "DIRECTORY"+        <> long "dir"+        <> short 'd'+        <> help "Data directory"+        <> showDefault+        <> value c.dir+  host <-+    strOption $+      metavar "ADDRESS"+        <> long "host"+        <> help "Network address to bind"+        <> showDefault+        <> value c.host+  port <-+    option auto $+      metavar "PORT"+        <> long "port"+        <> help "REST API listening port"+        <> showDefault+        <> value c.port+  net <-+    strOption $+      metavar netNames+        <> long "net"+        <> short 'n'+        <> help "Network to connect to"+        <> showDefault+        <> value c.net+  discover <-+    flag c.discover True $+      long "discover"+        <> help "Peer discovery"+  peers <-+    fmap (mappend c.peers) $+      many $+        option auto $+          metavar "HOSTNAME[:PORT]"+            <> long "peer"+            <> short 'p'+            <> help "Network peer (as many as required)"+  version <-+    switch $+      long "version"+        <> short 'v'+        <> help "Show version"+  debug <-+    flag c.debug True $+      long "debug"+        <> help "Show debug messages"+  maxPendingTxs <-+    option auto $+      metavar "COUNT"+        <> long "max-pending-txs"+        <> help "Maximum pending txs to fail health check"+        <> showDefault+        <> value c.maxPendingTxs+  maxLaggingBlocks <-+    option auto $+      metavar "COUNT"+        <> long "max-lagging-blocks"+        <> help "Maximum number of unindexed blocks"+        <> showDefault+        <> value c.maxLaggingBlocks+  minPeers <-+    option auto $+      metavar "COUNT"+        <> long "min-peers"+        <> help "Minimum number of connected peers for health check"+        <> showDefault+        <> value c.minPeers+  webLimits <- do+    maxItemCount <-+      option auto $+        metavar "COUNT"+          <> long "max-item-count"+          <> help "Hard limit for simple listings (0 = inf)"+          <> showDefault+          <> value c.webLimits.maxItemCount+    maxFullItemCount <-+      option auto $+        metavar "COUNT"+          <> long "max-full-item-count"+          <> help "Hard limit for full listings (0 = inf)"+          <> showDefault+          <> value c.webLimits.maxFullItemCount+    maxOffset <-+      option auto $+        metavar "OFFSET"+          <> long "max-offset"+          <> help "Hard limit for offsets (0 = inf)"+          <> showDefault+          <> value c.webLimits.maxOffset+    defItemCount <-+      option auto $+        metavar "COUNT"+          <> long "def-item-count"+          <> help "Soft default limit (0 = inf)"+          <> showDefault+          <> value c.webLimits.defItemCount+    xpubGap <-+      option auto $+        metavar "GAP"+          <> long "xpub-gap"+          <> help "Max gap for xpub queries"+          <> showDefault+          <> value c.webLimits.xpubGap+    xpubGapInit <-+      option auto $+        metavar "GAP"+          <> long "xpub-gap-init"+          <> 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 <-+      option auto $+        metavar "SECONDS"+          <> long "block-timeout"+          <> help "Block lag health timeout"+          <> showDefault+          <> value c.webTimeouts.block+    tx <-+      option auto $+        metavar "SECONDS"+          <> long "tx-timeout"+          <> help "Last tx received health timeout"+          <> showDefault+          <> value c.webTimeouts.tx+    return WebTimeouts {..}+  redis <-+    flag c.redis True $+      long "redis"+        <> help "Redis cache for xpub data"+  redisURL <-+    strOption $+      metavar "URL"+        <> long "redis-url"+        <> help "URL for Redis cache"+        <> value c.redisURL+  redisMinAddrs <-+    option auto $+      metavar "GAP"+        <> long "redis-min-gap"+        <> help "Minimum xpub address count to cache in Redis"+        <> showDefault+        <> value c.redisMinAddrs+  redisMaxKeys <-+    option auto $+      metavar "COUNT"+        <> long "redis-max-keys"+        <> help "Maximum number of keys in Redis"+        <> showDefault+        <> value c.redisMaxKeys+  redisSyncInterval <-+    option auto $+      metavar "SECONDS"+        <> long "redis-sync-interval"+        <> help "Sync mempool to Redis interval"+        <> showDefault+        <> value c.redisSyncInterval+  noMempool <-+    flag c.noMempool True $+      long "no-mempool"+        <> help "Do not index mempool transactions"+  wipeMempool <-+    flag c.wipeMempool True $+      long "wipe-mempool"+        <> help "Wipe indexed mempool at start"+  syncMempool <-+    flag c.syncMempool True $+      long "sync-mempool"+        <> help "Attempt to download peer mempools"+  peerTimeout <-+    option auto $+      metavar "SECONDS"+        <> long "peer-timeout"+        <> help "Unresponsive peer timeout"+        <> showDefault+        <> value c.peerTimeout+  maxPeerLife <-+    option auto $+      metavar "SECONDS"+        <> long "max-peer-life"+        <> help "Maximum peer connection time"+        <> showDefault+        <> value c.maxPeerLife+  maxPeers <-+    option auto $+      metavar "COUNT"+        <> long "max-peers"+        <> help "Do not connect to more than this many peers"+        <> showDefault+        <> value c.maxPeers+  statsd <-+    flag c.statsd True $+      long "statsd"+        <> help "Enable statsd metrics"+  statsdHost <-+    strOption $+      metavar "HOSTNAME"+        <> long "statsd-host"+        <> help "Host to send statsd metrics"+        <> showDefault+        <> value c.statsdHost+  statsdPort <-+    option auto $+      metavar "PORT"+        <> long "statsd-port"+        <> help "Port to send statsd metrics"+        <> showDefault+        <> value c.statsdPort+  statsdPrefix <-+    strOption $+      metavar "PREFIX"+        <> long "statsd-prefix"+        <> help "Prefix for statsd metrics"+        <> showDefault+        <> value c.statsdPrefix+  tickerRefresh <-+    option auto $+      metavar "MICROSECONDS"+        <> long "ticker-refresh"+        <> help "How often to retrieve price information"+        <> showDefault+        <> value c.tickerRefresh+  tickerURL <-+    strOption $+      metavar "URL"+        <> long "ticker-url"+        <> help "Blockchain.info price ticker URL"+        <> showDefault+        <> value c.tickerURL+  priceHistoryURL <-+    strOption $+      metavar "URL"+        <> long "price-history-url"+        <> help "Blockchain.info price history URL"+        <> showDefault+        <> value c.priceHistoryURL+  noBlockchainInfo <-+    flag c.noBlockchainInfo False $+      long "no-blockchain-info"+        <> help "Disable Blockchain.info-style API endpoints"+  noSlow <-+    flag c.noSlow False $+      long "no-slow"+        <> help "Disable potentially slow API endpoints"+  healthCheckInterval <-+    option auto $+      metavar "SECONDS"+        <> long "health-check-interval"+        <> help "Background check update interval"+        <> showDefault+        <> value c.healthCheckInterval+  pure Config {..}++networkReader :: String -> Either String Network+networkReader s =+  case netByName s of+    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+    exitSuccess+  if null c.peers && not c.discover+    then run $ let Config {..} = c in Config {discover = True, ..}+    else run c+  where+    opts c =+      info (helper <*> config c) $+        fullDesc+          <> progDesc "Bitcoin (BCH & BTC) block chain index with HTTP API"+          <> Options.Applicative.header+            ("haskoin-store version " <> version)++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+      let dir = cfg.dir </> net.name+      $(logInfoS) "haskoin-store" $+        "Creating working directory (if not found): " <> cs dir+      createDirectoryIfMissing True dir+      store <-+        ContT $+          withStore+            StoreConfig+              { maxPeers = cfg.maxPeers,+                initPeers = cfg.peers,+                discover = cfg.discover,+                db = dir </> "db",+                net = net,+                ctx = ctx,+                redis = if cfg.redis then Just cfg.redisURL else Nothing,+                gap = cfg.webLimits.xpubGap,+                initGap = cfg.webLimits.xpubGapInit,+                redisMinAddrs = cfg.redisMinAddrs,+                redisMaxKeys = cfg.redisMaxKeys,+                wipeMempool = cfg.wipeMempool,+                noMempool = cfg.noMempool,+                syncMempool = cfg.syncMempool,+                peerTimeout = fromIntegral cfg.peerTimeout,+                maxPeerLife = fromIntegral cfg.maxPeerLife,+                connect = withConnection,+                statsStore = stats,+                redisSyncInterval = cfg.redisSyncInterval+              }+      lift $+        runWeb+          WebConfig+            { host = cfg.host,+              port = cfg.port,+              store = store,+              limits = cfg.webLimits,+              timeouts = cfg.webTimeouts,+              maxPendingTxs = cfg.maxPendingTxs,+              maxLaggingBlocks = cfg.maxLaggingBlocks,+              minPeers = cfg.minPeers,+              version = version,+              noMempool = cfg.noMempool,+              statsStore = stats,+              tickerRefresh = cfg.tickerRefresh,+              tickerURL = cfg.tickerURL,+              priceHistoryURL = cfg.priceHistoryURL,+              noSlow = cfg.noSlow,+              noBlockchainInfo = cfg.noBlockchainInfo,+              healthCheckInterval = cfg.healthCheckInterval+            }+  where+    with_stats go+      | cfg.statsd = do+          $(logInfoS) "Main" $+            "Sending stats to "+              <> T.pack cfg.statsdHost+              <> ":"+              <> cs (show cfg.statsdPort)+              <> " with prefix: "+              <> T.pack cfg.statsdPrefix+          withStats+            (T.pack cfg.statsdHost)+            cfg.statsdPort+            (T.pack cfg.statsdPrefix)+            (go . Just)+      | otherwise = go Nothing+    l _ lvl+      | cfg.debug = True+      | otherwise = LevelInfo <= lvl
haskoin-store.cabal view
@@ -1,11 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.36.0.+-- This file has been generated from package.yaml by hpack version 0.35.2. -- -- see: https://github.com/sol/hpack  name:           haskoin-store-version:        0.65.12+version:        1.0.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,6 +17,9 @@ license:        MIT license-file:   LICENSE build-type:     Simple+extra-source-files:+    README.md+    CHANGELOG.md  source-repository head   type: git@@ -43,7 +46,7 @@       aeson >=1.4.7.1     , aeson-pretty >=0.8.8     , base >=4.9 && <5-    , base16 >=0.3.0+    , base16 >=1.0     , bytes >=0.17     , bytestring >=0.10.10.0     , cereal >=0.5.8.1@@ -56,9 +59,9 @@     , foldl >=1.4.10     , hashable >=1.3.0.0     , hashtables >=1.2.4.2-    , haskoin-core >=0.21.1-    , haskoin-node >=0.18.0-    , haskoin-store-data ==0.65.12+    , haskoin-core >=1.0.0+    , haskoin-node >=1.0.0+    , haskoin-store-data ==1.0.0     , hedis >=0.12.13     , http-types >=0.12.3     , lens >=4.18.1@@ -98,7 +101,7 @@       aeson >=1.4.7.1     , aeson-pretty >=0.8.8     , base >=4.9 && <5-    , base16 >=0.3.0+    , base16 >=1.0     , bytes >=0.17     , bytestring >=0.10.10.0     , cereal >=0.5.8.1@@ -112,10 +115,10 @@     , foldl >=1.4.10     , hashable >=1.3.0.0     , hashtables >=1.2.4.2-    , haskoin-core >=0.21.1-    , haskoin-node >=0.18.0+    , haskoin-core >=1.0.0+    , haskoin-node >=1.0.0     , haskoin-store-    , haskoin-store-data ==0.65.12+    , haskoin-store-data ==1.0.0     , hedis >=0.12.13     , http-types >=0.12.3     , lens >=4.18.1@@ -145,7 +148,7 @@     , wreq >=0.5.3.2   default-language: Haskell2010 -test-suite haskoin-store-test+test-suite spec   type: exitcode-stdio-1.0   main-is: Spec.hs   other-modules:@@ -160,7 +163,7 @@     , aeson >=1.4.7.1     , aeson-pretty >=0.8.8     , base >=4.9 && <5-    , base16 >=0.3.0+    , base16 >=1.0     , base64 >=0.4.1     , bytes >=0.17     , bytestring >=0.10.10.0@@ -174,10 +177,10 @@     , foldl >=1.4.10     , hashable >=1.3.0.0     , hashtables >=1.2.4.2-    , haskoin-core >=0.21.1-    , haskoin-node >=0.18.0+    , haskoin-core >=1.0.0+    , haskoin-node >=1.0.0     , haskoin-store-    , haskoin-store-data ==0.65.12+    , haskoin-store-data ==1.0.0     , hedis >=0.12.13     , hspec >=2.7.1     , http-types >=0.12.3
src/Haskoin/Store/BlockStore.hs view
@@ -1,12 +1,17 @@ {-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-}-{-# LANGUAGE ImportQualifiedPost #-}+{-# LANGUAGE NoFieldSelectors #-}+{-# OPTIONS_GHC -Wno-ambiguous-fields #-}  module Haskoin.Store.BlockStore   ( -- * Block Store@@ -61,7 +66,8 @@     asks,   ) import Control.Monad.Trans (lift)-import Control.Monad.Trans.Maybe (runMaybeT)+import Control.Monad.Trans.Maybe (MaybeT (MaybeT), runMaybeT)+import Data.Bool (bool) import Data.ByteString qualified as B import Data.HashMap.Strict (HashMap) import Data.HashMap.Strict qualified as HashMap@@ -71,6 +77,7 @@ import Data.Maybe   ( catMaybes,     fromJust,+    fromMaybe,     isJust,     mapMaybe,   )@@ -78,6 +85,7 @@ import Data.String (fromString) import Data.String.Conversions (cs) import Data.Text (Text)+import Data.Text qualified as T import Data.Time.Clock   ( NominalDiffTime,     UTCTime,@@ -98,6 +106,7 @@     BlockHeader (..),     BlockHeight,     BlockNode (..),+    Ctx,     GetData (..),     InvType (..),     InvVector (..),@@ -115,9 +124,9 @@ import Haskoin.Node   ( Chain,     OnlinePeer (..),-    Peer,+    Peer (..),     PeerException (..),-    PeerManager,+    PeerMgr,     chainBlockMain,     chainGetAncestor,     chainGetBest,@@ -125,7 +134,6 @@     chainGetParents,     getPeers,     killPeer,-    peerText,     sendMessage,     setBusy,     setFree,@@ -198,50 +206,50 @@   deriving (Show, Eq, Ord, Exception)  data Syncing = Syncing-  { syncingPeer :: !Peer,-    syncingTime :: !UTCTime,-    syncingBlocks :: ![BlockHash]+  { peer :: !Peer,+    time :: !UTCTime,+    blocks :: ![BlockHash]   }  data PendingTx = PendingTx-  { pendingTxTime :: !UTCTime,-    pendingTx :: !Tx,-    pendingDeps :: !(HashSet TxHash)+  { time :: !UTCTime,+    tx :: !Tx,+    deps :: !(HashSet TxHash)   }   deriving (Show, Eq, Ord)  -- | Block store process state. data BlockStore = BlockStore-  { myMailbox :: !(Mailbox BlockStoreMessage),-    myConfig :: !BlockStoreConfig,-    myPeer :: !(TVar (Maybe Syncing)),-    myTxs :: !(TVar (HashMap TxHash PendingTx)),+  { mailbox :: !(Mailbox BlockStoreMessage),+    config :: !BlockStoreConfig,+    peer :: !(TVar (Maybe Syncing)),+    txs :: !(TVar (HashMap TxHash PendingTx)),     requested :: !(TVar (HashSet TxHash)),-    myMetrics :: !(Maybe StoreMetrics)+    metrics :: !(Maybe StoreMetrics)   }  data StoreMetrics = StoreMetrics   { storeHeight :: !Metrics.Gauge,     headersHeight :: !Metrics.Gauge,-    storePendingTxs :: !Metrics.Gauge,-    storePeersConnected :: !Metrics.Gauge,-    storeMempoolSize :: !Metrics.Gauge+    pendingTxs :: !Metrics.Gauge,+    connectedPeers :: !Metrics.Gauge,+    mempoolSize :: !Metrics.Gauge   } -newStoreMetrics :: MonadIO m => Metrics.Store -> m StoreMetrics+newStoreMetrics :: (MonadIO m) => Metrics.Store -> m StoreMetrics newStoreMetrics s = liftIO $ do   storeHeight <- g "blockchain.height"   headersHeight <- g "blockchain.headers"-  storePendingTxs <- g "mempool.pending_txs"-  storePeersConnected <- g "network.peers_connected"-  storeMempoolSize <- g "mempool.size"+  pendingTxs <- g "mempool.pending_txs"+  connectedPeers <- g "network.peers_connected"+  mempoolSize <- g "mempool.size"   return StoreMetrics {..}   where     g x = Metrics.createGauge ("store." <> x) s -setStoreHeight :: MonadIO m => BlockT m ()+setStoreHeight :: (MonadIO m) => BlockT m () setStoreHeight =-  asks myMetrics >>= \case+  asks (.metrics) >>= \case     Nothing -> return ()     Just m ->       getBestBlock >>= \case@@ -249,79 +257,84 @@         Just bb ->           getBlock bb >>= \case             Nothing -> setit m 0-            Just b -> setit m (blockDataHeight b)+            Just b -> setit m b.height   where-    setit m i = liftIO $ storeHeight m `Metrics.Gauge.set` fromIntegral i+    setit m i = liftIO $ m.storeHeight `Metrics.Gauge.set` fromIntegral i -setHeadersHeight :: MonadIO m => BlockT m ()+setHeadersHeight :: (MonadIO m) => BlockT m () setHeadersHeight =-  asks myMetrics >>= \case+  asks (.metrics) >>= \case     Nothing -> return ()     Just m -> do-      h <- fmap nodeHeight $ chainGetBest =<< asks (blockConfChain . myConfig)-      liftIO $ headersHeight m `Metrics.Gauge.set` fromIntegral h+      h <- fmap (.height) $ chainGetBest =<< asks (.config.chain)+      liftIO $ m.headersHeight `Metrics.Gauge.set` fromIntegral h -setPendingTxs :: MonadIO m => BlockT m ()+setPendingTxs :: (MonadIO m) => BlockT m () setPendingTxs =-  asks myMetrics >>= \case+  asks (.metrics) >>= \case     Nothing -> return ()     Just m -> do-      s <- asks myTxs >>= \t -> atomically (HashMap.size <$> readTVar t)-      liftIO $ storePendingTxs m `Metrics.Gauge.set` fromIntegral s+      s <- asks (.txs) >>= \t -> atomically (HashMap.size <$> readTVar t)+      liftIO $ m.pendingTxs `Metrics.Gauge.set` fromIntegral s -setPeersConnected :: MonadIO m => BlockT m ()+setPeersConnected :: (MonadIO m) => BlockT m () setPeersConnected =-  asks myMetrics >>= \case+  asks (.metrics) >>= \case     Nothing -> return ()     Just m -> do-      ps <- fmap length $ getPeers =<< asks (blockConfManager . myConfig)-      liftIO $ storePeersConnected m `Metrics.Gauge.set` fromIntegral ps+      ps <- fmap length $ getPeers =<< asks (.config.peerMgr)+      liftIO $ m.connectedPeers `Metrics.Gauge.set` fromIntegral ps -setMempoolSize :: MonadIO m => BlockT m ()+setMempoolSize :: (MonadIO m) => BlockT m () setMempoolSize =-  asks myMetrics >>= \case+  asks (.metrics) >>= \case     Nothing -> return ()     Just m -> do       s <- length <$> getMempool-      liftIO $ storeMempoolSize m `Metrics.Gauge.set` fromIntegral s+      liftIO $ m.mempoolSize `Metrics.Gauge.set` fromIntegral s  -- | Configuration for a block store. data BlockStoreConfig = BlockStoreConfig-  { -- | peer manager from running node-    blockConfManager :: !PeerManager,+  { ctx :: !Ctx,+    -- | peer manager from running node+    peerMgr :: !PeerMgr,     -- | chain from a running node-    blockConfChain :: !Chain,+    chain :: !Chain,     -- | listener for store events-    blockConfListener :: !(Publisher StoreEvent),+    pub :: !(Publisher StoreEvent),     -- | RocksDB database handle-    blockConfDB :: !DatabaseReader,+    db :: !DatabaseReader,     -- | network constants-    blockConfNet :: !Network,+    net :: !Network,     -- | do not index new mempool transactions-    blockConfNoMempool :: !Bool,+    noMempool :: !Bool,     -- | wipe mempool at start-    blockConfWipeMempool :: !Bool,+    wipeMempool :: !Bool,     -- | sync mempool from peers-    blockConfSyncMempool :: !Bool,+    syncMempool :: !Bool,     -- | disconnect syncing peer if inactive for this long-    blockConfPeerTimeout :: !NominalDiffTime,-    blockConfStats :: !(Maybe Metrics.Store)+    peerTimeout :: !NominalDiffTime,+    statsStore :: !(Maybe Metrics.Store)   }  type BlockT m = ReaderT BlockStore m  runImport ::-  MonadLoggerIO m =>+  (MonadLoggerIO m) =>+  Network ->+  Ctx ->   WriterT (ExceptT ImportException m) a ->   BlockT m (Either ImportException a)-runImport f =-  ReaderT $ \r -> runExceptT $ runWriter (blockConfDB (myConfig r)) f+runImport net ctx f =+  ReaderT $ \r -> runExceptT $ runWriter net ctx r.config.db f  runRocksDB :: ReaderT DatabaseReader m a -> BlockT m a runRocksDB f =-  ReaderT $ runReaderT f . blockConfDB . myConfig+  ReaderT $ runReaderT f . (.config.db) -instance MonadIO m => StoreReadBase (BlockT m) where+instance (MonadIO m) => StoreReadBase (BlockT m) where+  getCtx =+    asks (.config.ctx)   getNetwork =     runRocksDB getNetwork   getBestBlock =@@ -341,7 +354,7 @@   getMempool =     runRocksDB getMempool -instance MonadUnliftIO m => StoreReadExtra (BlockT m) where+instance (MonadUnliftIO m) => StoreReadExtra (BlockT m) where   getMaxGap =     runRocksDB getMaxGap   getInitialGap =@@ -378,15 +391,15 @@   ts <- newTVarIO HashMap.empty   rq <- newTVarIO HashSet.empty   inbox <- newInbox-  metrics <- mapM newStoreMetrics (blockConfStats cfg)+  metrics <- mapM newStoreMetrics cfg.statsStore   let r =         BlockStore-          { myMailbox = inboxToMailbox inbox,-            myConfig = cfg,-            myPeer = pb,-            myTxs = ts,+          { mailbox = inboxToMailbox inbox,+            config = cfg,+            peer = pb,+            txs = ts,             requested = rq,-            myMetrics = metrics+            metrics = metrics           }   withAsync (runReaderT (go inbox) r) $ \a -> do     link a@@ -401,53 +414,60 @@         "Deleting " <> cs (show (length txs)) <> " transactions"       forM_ txs $ \(_, th) -> deleteUnconfirmedTx False th     wipe_it txs = do+      net <- getNetwork+      ctx <- getCtx       let (txs1, txs2) = splitAt 1000 txs       unless (null txs1) $-        runImport (del txs1) >>= \case+        runImport net ctx (del txs1) >>= \case           Left e -> do             $(logErrorS) "BlockStore" $               "Could not wipe mempool: " <> cs (show e)             throwIO e           Right () -> wipe_it txs2     wipe-      | blockConfWipeMempool cfg =+      | cfg.wipeMempool =           getMempool >>= wipe_it       | otherwise =           return ()-    ini =-      runImport initBest >>= \case+    ini = do+      net <- getNetwork+      ctx <- getCtx+      runImport net ctx initBest >>= \case         Left e -> do           $(logErrorS) "BlockStore" $             "Could not initialize: " <> cs (show e)           throwIO e         Right () -> return ()     run inbox =-      withAsync (pingMe (inboxToMailbox inbox)) $-        const $-          forever $-            receive inbox-              >>= ReaderT . runReaderT . processBlockStoreMessage+      withAsync (pingMe (inboxToMailbox inbox)) $ \a ->+        link a >> runBlockStoreLoop inbox -isInSync :: MonadLoggerIO m => BlockT m Bool+runBlockStoreLoop inbox =+  forever $ do+    $(logDebugS) "BlockStore" "Waiting for new event..."+    msg <- receive inbox+    ReaderT $ runReaderT $ processBlockStoreMessage msg++isInSync :: (MonadLoggerIO m) => BlockT m Bool isInSync =   getBestBlock >>= \case     Nothing -> do       $(logErrorS) "BlockStore" "Block database uninitialized"       throwIO Uninitialized     Just bb -> do-      cb <- asks (blockConfChain . myConfig) >>= chainGetBest-      if headerHash (nodeHeader cb) == bb+      cb <- asks (.config.chain) >>= chainGetBest+      if headerHash cb.header == bb         then clearSyncingState >> return True         else return False -guardMempool :: Monad m => BlockT m () -> BlockT m ()+guardMempool :: (Monad m) => BlockT m () -> BlockT m () guardMempool f = do-  n <- asks (blockConfNoMempool . myConfig)+  n <- asks (.config.noMempool)   unless n f -syncMempool :: Monad m => BlockT m () -> BlockT m ()+syncMempool :: (Monad m) => BlockT m () -> BlockT m () syncMempool f = do-  s <- asks (blockConfSyncMempool . myConfig)+  s <- asks (.config.syncMempool)   when s f  mempool :: (MonadUnliftIO m, MonadLoggerIO m) => Peer -> BlockT m ()@@ -455,7 +475,7 @@   guardMempool . syncMempool $     isInSync >>= \s -> when s $ do       $(logDebugS) "BlockStore" $-        "Requesting mempool from peer: " <> peerText p+        "Requesting mempool from peer: " <> p.label       MMempool `sendMessage` p  processBlock ::@@ -469,7 +489,7 @@     False -> do       $(logErrorS) "BlockStore" $         "Non-syncing peer "-          <> peerText peer+          <> peer.label           <> " sent me a block: "           <> blockHashToHex blockhash       PeerMisbehaving "Sent unexpected block" `killPeer` peer@@ -480,7 +500,7 @@       Nothing -> do         $(logErrorS) "BlockStore" $           "Peer "-            <> peerText peer+            <> peer.label             <> " sent unknown block: "             <> blockHashToHex blockhash         PeerMisbehaving "Sent unknown block" `killPeer` peer@@ -489,19 +509,21 @@     "Processing block: "       <> blockText node Nothing       <> " from peer: "-      <> peerText peer+      <> peer.label+  net <- lift getNetwork+  ctx <- lift getCtx   lift . notify (Just block) $-    runImport (importBlock block node) >>= \case+    runImport net ctx (importBlock block node) >>= \case       Left e -> failure e       Right _ -> success node   where-    header = blockHeader block+    header = block.header     blockhash = headerHash header     hexhash = blockHashToHex blockhash     success node = do       $(logInfoS) "BlockStore" $         "Best block: " <> blockText node (Just block)-      removeSyncingBlock $ headerHash $ nodeHeader node+      removeSyncingBlock $ headerHash node.header       touchPeer       isInSync >>= \case         False -> syncMe@@ -513,7 +535,7 @@         "Error importing block "           <> hexhash           <> " from peer: "-          <> peerText peer+          <> peer.label           <> ": "           <> cs (show e)       killPeer (PeerMisbehaving (show e)) peer@@ -523,55 +545,54 @@   [BlockHash] ->   m () setSyncingBlocks hs =-  asks myPeer >>= \box ->+  asks (.peer) >>= \box ->     atomically $       modifyTVar box $ \case         Nothing -> Nothing-        Just x -> Just x {syncingBlocks = hs}+        Just x -> Just (x :: Syncing) {blocks = hs}  getSyncingBlocks :: (MonadReader BlockStore m, MonadIO m) => m [BlockHash] getSyncingBlocks =-  asks myPeer >>= readTVarIO >>= \case+  asks (.peer) >>= readTVarIO >>= \case     Nothing -> return []-    Just x -> return $ syncingBlocks x+    Just x -> return x.blocks  addSyncingBlocks ::   (MonadReader BlockStore m, MonadIO m) =>   [BlockHash] ->   m () addSyncingBlocks hs =-  asks myPeer >>= \box ->+  asks (.peer) >>= \box ->     atomically $       modifyTVar box $ \case         Nothing -> Nothing-        Just x -> Just x {syncingBlocks = syncingBlocks x <> hs}+        Just x -> Just (x :: Syncing) {blocks = x.blocks <> hs}  removeSyncingBlock ::   (MonadReader BlockStore m, MonadIO m) =>   BlockHash ->   m () removeSyncingBlock h = do-  box <- asks myPeer+  box <- asks (.peer)   atomically $     modifyTVar box $ \case       Nothing -> Nothing-      Just x -> Just x {syncingBlocks = delete h (syncingBlocks x)}+      Just x -> Just (x :: Syncing) {blocks = delete h x.blocks}  checkPeer :: (MonadLoggerIO m, MonadReader BlockStore m) => Peer -> m Bool-checkPeer p =-  fmap syncingPeer <$> getSyncingState >>= \case-    Nothing -> return False-    Just p' -> return $ p == p'+checkPeer p = fmap (fromMaybe False) <$> runMaybeT $ do+  Syncing {peer} <- MaybeT getSyncingState+  return $ peer == p  getBlockNode ::   (MonadLoggerIO m, MonadReader BlockStore m) =>   BlockHash ->   m (Maybe BlockNode) getBlockNode blockhash =-  chainGetBlock blockhash =<< asks (blockConfChain . myConfig)+  chainGetBlock blockhash =<< asks (.config.chain)  processNoBlocks ::-  MonadLoggerIO m =>+  (MonadLoggerIO m) =>   Peer ->   [BlockHash] ->   BlockT m ()@@ -585,50 +606,50 @@         <> " "         <> blockHashToHex h         <> " not found by peer: "-        <> peerText p+        <> p.label   killPeer (PeerMisbehaving "Did not find requested block(s)") p -processTx :: MonadLoggerIO m => Peer -> Tx -> BlockT m ()+processTx :: (MonadLoggerIO m) => Peer -> Tx -> BlockT m () processTx p tx = guardMempool $ do   t <- liftIO getCurrentTime   $(logDebugS) "BlockManager" $     "Received tx "       <> txHashToHex (txHash tx)       <> " by peer: "-      <> peerText p+      <> p.label   addPendingTx $ PendingTx t tx HashSet.empty -pruneOrphans :: MonadIO m => BlockT m ()+pruneOrphans :: (MonadIO m) => BlockT m () pruneOrphans = guardMempool $ do-  ts <- asks myTxs+  ts <- asks (.txs)   now <- liftIO getCurrentTime   atomically . modifyTVar ts . HashMap.filter $ \p ->-    now `diffUTCTime` pendingTxTime p > 600+    now `diffUTCTime` p.time > 600 -addPendingTx :: MonadIO m => PendingTx -> BlockT m ()+addPendingTx :: (MonadIO m) => PendingTx -> BlockT m () addPendingTx p = do-  ts <- asks myTxs-  rq <- asks requested+  ts <- asks (.txs)+  rq <- asks (.requested)   atomically $ do     modifyTVar ts $ HashMap.insert th p     modifyTVar rq $ HashSet.delete th     HashMap.size <$> readTVar ts   setPendingTxs   where-    th = txHash (pendingTx p)+    th = txHash p.tx -addRequestedTx :: MonadIO m => TxHash -> BlockT m ()+addRequestedTx :: (MonadIO m) => TxHash -> BlockT m () addRequestedTx th = do-  qbox <- asks requested+  qbox <- asks (.requested)   atomically $ modifyTVar qbox $ HashSet.insert th   liftIO . void . async $ do     threadDelay 20000000     atomically $ modifyTVar qbox $ HashSet.delete th -isPending :: MonadIO m => TxHash -> BlockT m Bool+isPending :: (MonadIO m) => TxHash -> BlockT m Bool isPending th = do-  tbox <- asks myTxs-  qbox <- asks requested+  tbox <- asks (.txs)+  qbox <- asks (.requested)   atomically $ do     ts <- readTVar tbox     rs <- readTVar qbox@@ -636,34 +657,34 @@       th `HashMap.member` ts         || th `HashSet.member` rs -pendingTxs :: MonadIO m => Int -> BlockT m [PendingTx]+pendingTxs :: (MonadIO m) => Int -> BlockT m [PendingTx] pendingTxs i = do   selected <--    asks myTxs >>= \box -> atomically $ do+    asks (.txs) >>= \box -> atomically $ do       pending <- readTVar box       let (selected, rest) = select pending       writeTVar box rest-      return (selected)+      return selected   setPendingTxs   return selected   where     select pend =-      let eligible = HashMap.filter (null . pendingDeps) pend+      let eligible = HashMap.filter (null . (.deps)) pend           orphans = HashMap.difference pend eligible           selected = take i $ sortit eligible           remaining = HashMap.filter (`notElem` selected) eligible        in (selected, remaining <> orphans)     sortit m =-      let sorted = sortTxs $ map pendingTx $ HashMap.elems m+      let sorted = sortTxs $ map (.tx) $ HashMap.elems m           txids = map (txHash . snd) sorted        in mapMaybe (`HashMap.lookup` m) txids -fulfillOrphans :: MonadIO m => BlockStore -> TxHash -> m ()+fulfillOrphans :: (MonadIO m) => BlockStore -> TxHash -> m () fulfillOrphans block_read th =   atomically $ modifyTVar box (HashMap.map fulfill)   where-    box = myTxs block_read-    fulfill p = p {pendingDeps = HashSet.delete th (pendingDeps p)}+    box = block_read.txs+    fulfill p = p {deps = HashSet.delete th p.deps}  updateOrphans ::   ( StoreReadBase m,@@ -672,11 +693,11 @@   ) =>   m () updateOrphans = do-  box <- asks myTxs+  box <- asks (.txs)   pending <- readTVarIO box-  let orphans = HashMap.filter (not . null . pendingDeps) pending+  let orphans = HashMap.filter (not . null . (.deps)) pending   updated <- forM orphans $ \p -> do-    let tx = pendingTx p+    let tx = p.tx     exists (txHash tx) >>= \case       True -> return Nothing       False -> Just <$> fill_deps p@@ -686,20 +707,20 @@     exists th =       getTxData th >>= \case         Nothing -> return False-        Just TxData {txDataDeleted = True} -> return False-        Just TxData {txDataDeleted = False} -> return True-    prev_utxos tx = catMaybes <$> mapM (getUnspent . prevOutput) (txIn tx)+        Just TxData {deleted = True} -> return False+        Just TxData {deleted = False} -> return True+    prev_utxos tx = catMaybes <$> mapM (getUnspent . (.outpoint)) tx.inputs     fulfill p unspent =-      let unspent_hash = outPointHash (unspentPoint unspent)-          new_deps = HashSet.delete unspent_hash (pendingDeps p)-       in p {pendingDeps = new_deps}+      let unspent_hash = unspent.outpoint.hash+          new_deps = HashSet.delete unspent_hash p.deps+       in p {deps = new_deps}     fill_deps p = do-      let tx = pendingTx p+      let tx = p.tx       unspents <- prev_utxos tx       return $ foldl fulfill p unspents  newOrphanTx ::-  MonadLoggerIO m =>+  (MonadLoggerIO m) =>   BlockStore ->   UTCTime ->   Tx ->@@ -709,22 +730,22 @@     "Import tx "       <> txHashToHex (txHash tx)       <> ": Orphan"-  let box = myTxs block_read+  let box = block_read.txs   unspents <- catMaybes <$> mapM getUnspent prevs-  let unspent_set = HashSet.fromList (map unspentPoint unspents)+  let unspent_set = HashSet.fromList (map (.outpoint) unspents)       missing_set = HashSet.difference prev_set unspent_set-      missing_txs = HashSet.map outPointHash missing_set+      missing_txs = HashSet.map (.hash) missing_set   atomically . modifyTVar box $     HashMap.insert       (txHash tx)       PendingTx-        { pendingTxTime = time,-          pendingTx = tx,-          pendingDeps = missing_txs+        { time = time,+          tx = tx,+          deps = missing_txs         }   where     prev_set = HashSet.fromList prevs-    prevs = map prevOutput (txIn tx)+    prevs = map (.outpoint) tx.inputs  importMempoolTx ::   (MonadLoggerIO m, MonadError ImportException m) =>@@ -741,63 +762,59 @@       return False     handle_error _ = return False     seconds = floor (utcTimeToPOSIXSeconds time)-    new_mempool_tx =-      newMempoolTx tx seconds >>= \case-        True -> do-          $(logInfoS) "BlockStore" $-            "Import tx "-              <> txHashToHex (txHash tx)-              <> ": OK"-          fulfillOrphans block_read tx_hash-          return True-        False -> do-          $(logDebugS) "BlockStore" $-            "Import tx "-              <> txHashToHex (txHash tx)-              <> ": Already imported"-          return False+    new_mempool_tx = do+      t <- newMempoolTx tx seconds+      $(logInfoS) "BlockStore" $+        "Tx "+          <> txHashToHex (txHash tx)+          <> bool " was already imported" " imported" t+      when t $ fulfillOrphans block_read tx_hash+      return t -notify :: MonadIO m => Maybe Block -> BlockT m a -> BlockT m a+notify :: (MonadIO m) => Maybe Block -> BlockT m a -> BlockT m a notify block go = do   old <- HashSet.union e . HashSet.fromList . map snd <$> getMempool   x <- go   new <- HashSet.union e . HashSet.fromList . map snd <$> getMempool-  l <- asks (blockConfListener . myConfig)+  l <- asks (.config.pub)   forM_ (old `HashSet.difference` new) $ \h ->     publish (StoreMempoolDelete h) l   forM_ (new `HashSet.difference` old) $ \h ->     publish (StoreMempoolNew h) l   case block of-    Just b -> publish (StoreBestBlock (headerHash (blockHeader b))) l+    Just b -> publish (StoreBestBlock (headerHash b.header)) l     Nothing -> return ()   return x   where     e = case block of-      Just b -> HashSet.fromList (map txHash (blockTxns b))+      Just b -> HashSet.fromList (map txHash b.txs)       Nothing -> HashSet.empty -processMempool :: MonadLoggerIO m => BlockT m ()+processMempool :: (MonadLoggerIO m) => BlockT m () processMempool = guardMempool . notify Nothing $ do   txs <- pendingTxs 2000   block_read <- ask   unless (null txs) (import_txs block_read txs)   where     run_import block_read p =-      let t = pendingTx p+      let t = p.tx           h = txHash t-       in importMempoolTx block_read (pendingTxTime p) (pendingTx p)+       in importMempoolTx block_read p.time p.tx     import_txs block_read txs =       let r = mapM (run_import block_read) txs-       in runImport r >>= \case-            Left e -> report_error e-            Right _ -> return ()+       in do+            net <- getNetwork+            ctx <- getCtx+            runImport net ctx r >>= \case+              Left e -> report_error e+              Right _ -> return ()     report_error e = do       $(logErrorS) "BlockImport" $         "Error processing mempool: " <> cs (show e)       throwIO e  processTxs ::-  MonadLoggerIO m =>+  (MonadLoggerIO m) =>   Peer ->   [TxHash] ->   BlockT m ()@@ -808,7 +825,7 @@       "Received inventory with "         <> cs (show (length hs))         <> " transactions from peer: "-        <> peerText p+        <> p.label     xs <- catMaybes <$> zip_counter process_tx     unless (null xs) $ go xs   where@@ -853,9 +870,9 @@               return (Just h)     go xs = do       mapM_ addRequestedTx xs-      net <- asks (blockConfNet . myConfig)-      let inv = if getSegWit net then InvWitnessTx else InvTx-          vec = map (InvVector inv . getTxHash) xs+      net <- asks (.config.net)+      let inv = if net.segWit then InvWitnessTx else InvTx+          vec = map (InvVector inv . (.get)) xs           msg = MGetData (GetData vec)       msg `sendMessage` p @@ -868,38 +885,40 @@   getSyncingState >>= \case     Nothing -> return ()     Just _ -> do-      box <- asks myPeer+      box <- asks (.peer)       now <- liftIO getCurrentTime       atomically $         modifyTVar box $           fmap $-            \x -> x {syncingTime = now}+            \x -> (x :: Syncing) {time = now} -checkTime :: MonadLoggerIO m => BlockT m ()+checkTime :: (MonadLoggerIO m) => BlockT m () checkTime =-  asks myPeer >>= readTVarIO >>= \case+  asks (.peer) >>= readTVarIO >>= \case     Nothing -> return ()     Just       Syncing-        { syncingTime = t,-          syncingPeer = p+        { time = t,+          peer = p         } -> do         now <- liftIO getCurrentTime-        peer_time_out <- asks (blockConfPeerTimeout . myConfig)+        peer_time_out <- asks (.config.peerTimeout)         when (now `diffUTCTime` t > peer_time_out) $ do           $(logErrorS) "BlockStore" $-            "Syncing peer timeout: " <> peerText p+            "Syncing peer timeout: " <> p.label           killPeer PeerTimeout p -revertToMainChain :: MonadLoggerIO m => BlockT m ()+revertToMainChain :: (MonadLoggerIO m) => BlockT m () revertToMainChain = do-  h <- headerHash . nodeHeader <$> getBest-  ch <- asks (blockConfChain . myConfig)+  h <- headerHash . (.header) <$> getBest+  ch <- asks (.config.chain)+  net <- getNetwork+  ctx <- getCtx   chainBlockMain h ch >>= \x -> unless x $ do     $(logWarnS) "BlockStore" $       "Reverting best block: "         <> blockHashToHex h-    runImport (revertBlock h) >>= \case+    runImport net ctx (revertBlock h) >>= \case       Left e -> do         $(logErrorS) "BlockStore" $           "Could not revert block "@@ -910,7 +929,7 @@       Right () -> setSyncingBlocks []     revertToMainChain -getBest :: MonadLoggerIO m => BlockT m BlockNode+getBest :: (MonadLoggerIO m) => BlockT m BlockNode getBest = do   bb <-     getBestBlock >>= \case@@ -918,7 +937,7 @@       Nothing -> do         $(logErrorS) "BlockStore" "No best block set"         throwIO Uninitialized-  ch <- asks (blockConfChain . myConfig)+  ch <- asks (.config.chain)   chainGetBlock bb ch >>= \case     Just x -> return x     Nothing -> do@@ -927,7 +946,7 @@           <> blockHashToHex bb       throwIO (BlockNotInChain bb) -getSyncBest :: MonadLoggerIO m => BlockT m BlockNode+getSyncBest :: (MonadLoggerIO m) => BlockT m BlockNode getSyncBest = do   bb <-     getSyncingBlocks >>= \case@@ -938,7 +957,7 @@             $(logErrorS) "BlockStore" "No best block set"             throwIO Uninitialized       hs -> return $ last hs-  ch <- asks (blockConfChain . myConfig)+  ch <- asks (.config.chain)   chainGetBlock bb ch >>= \case     Just x -> return x     Nothing -> do@@ -947,18 +966,18 @@           <> blockHashToHex bb       throwIO (BlockNotInChain bb) -shouldSync :: MonadLoggerIO m => BlockT m (Maybe Peer)+shouldSync :: (MonadLoggerIO m) => BlockT m (Maybe Peer) shouldSync =   isInSync >>= \case     True -> return Nothing     False ->       getSyncingState >>= \case         Nothing -> return Nothing-        Just Syncing {syncingPeer = p, syncingBlocks = bs}+        Just Syncing {peer = p, blocks = bs}           | 100 > length bs -> return (Just p)           | otherwise -> return Nothing -syncMe :: MonadLoggerIO m => BlockT m ()+syncMe :: (MonadLoggerIO m) => BlockT m () syncMe = do   revertToMainChain   shouldSync >>= \case@@ -973,32 +992,30 @@           "Requesting "             <> fromString (show (length iv))             <> " blocks from peer: "-            <> peerText p-        addSyncingBlocks $ map (headerHash . nodeHeader) bns+            <> p.label+        addSyncingBlocks $ map (headerHash . (.header)) bns         MGetData (GetData iv) `sendMessage` p   where     getiv bns = do-      w <- getSegWit <$> asks (blockConfNet . myConfig)+      w <- asks (.config.net.segWit)       let i = if w then InvWitnessBlock else InvBlock-          f = InvVector i . getBlockHash . headerHash . nodeHeader+          f = InvVector i . (.get) . headerHash . (.header)       return $ map f bns     getbh =-      chainGetBest =<< asks (blockConfChain . myConfig)+      chainGetBest =<< asks (.config.chain)     sel bb bh = do       let sh = geth bb bh       t <- top sh bh-      ch <- asks (blockConfChain . myConfig)-      ps <- chainGetParents (nodeHeight bb + 1) t ch+      ch <- asks (.config.chain)+      ps <- chainGetParents (bb.height + 1) t ch       return $         if 500 > length ps           then ps <> [bh]           else ps     geth bb bh =-      min-        (nodeHeight bb + 501)-        (nodeHeight bh)+      min (bb.height + 501) bh.height     top sh bh =-      if sh == nodeHeight bh+      if sh == bh.height         then return bh         else findAncestor sh bh @@ -1008,16 +1025,16 @@   BlockNode ->   m BlockNode findAncestor height target = do-  ch <- asks (blockConfChain . myConfig)+  ch <- asks (.config.chain)   chainGetAncestor height target ch >>= \case     Just ancestor -> return ancestor     Nothing -> do-      let h = headerHash $ nodeHeader target+      let h = headerHash target.header       $(logErrorS) "BlockStore" $         "Could not find header for ancestor of block "           <> blockHashToHex h           <> " at height "-          <> cs (show (nodeHeight target))+          <> cs (show target.height)       throwIO $ AncestorNotInChain height h  finishPeer ::@@ -1025,17 +1042,17 @@   Peer ->   m () finishPeer p = do-  box <- asks myPeer+  box <- asks (.peer)   readTVarIO box >>= \case-    Just Syncing {syncingPeer = p'} | p == p' -> reset_it box+    Just Syncing {peer = p'} | p == p' -> reset_it box     _ -> return ()   where     reset_it box = do       atomically $ writeTVar box Nothing-      $(logDebugS) "BlockStore" $ "Releasing peer: " <> peerText p+      $(logDebugS) "BlockStore" $ "Releasing peer: " <> p.label       setFree p -trySetPeer :: MonadLoggerIO m => Peer -> BlockT m Bool+trySetPeer :: (MonadLoggerIO m) => Peer -> BlockT m Bool trySetPeer p =   getSyncingState >>= \case     Just _ -> return False@@ -1046,19 +1063,19 @@         False -> return False         True -> do           $(logDebugS) "BlockStore" $-            "Locked peer: " <> peerText p-          box <- asks myPeer+            "Locked peer: " <> p.label+          box <- asks (.peer)           now <- liftIO getCurrentTime           atomically . writeTVar box $             Just               Syncing-                { syncingPeer = p,-                  syncingTime = now,-                  syncingBlocks = []+                { peer = p,+                  time = now,+                  blocks = []                 }           return True -trySyncing :: MonadLoggerIO m => BlockT m ()+trySyncing :: (MonadLoggerIO m) => BlockT m () trySyncing =   isInSync >>= \case     True -> return ()@@ -1073,8 +1090,8 @@         False -> recurse ps         True -> syncMe     online_peer = do-      ops <- getPeers =<< asks (blockConfManager . myConfig)-      let ps = map onlinePeerMailbox ops+      ops <- getPeers =<< asks (.config.peerMgr)+      let ps = map (.mailbox) ops       recurse ps  trySyncingPeer :: (MonadUnliftIO m, MonadLoggerIO m) => Peer -> BlockT m ()@@ -1089,34 +1106,62 @@ getSyncingState ::   (MonadIO m, MonadReader BlockStore m) => m (Maybe Syncing) getSyncingState =-  readTVarIO =<< asks myPeer+  readTVarIO =<< asks (.peer)  clearSyncingState ::   (MonadLoggerIO m, MonadReader BlockStore m) => m () clearSyncingState =-  asks myPeer >>= readTVarIO >>= \case+  asks (.peer) >>= readTVarIO >>= \case     Nothing -> return ()-    Just Syncing {syncingPeer = p} -> finishPeer p+    Just Syncing {peer = p} -> finishPeer p  processBlockStoreMessage ::   (MonadUnliftIO m, MonadLoggerIO m) =>   BlockStoreMessage ->   BlockT m ()-processBlockStoreMessage (BlockNewBest _) =+processBlockStoreMessage (BlockNewBest node) = do+  $(logDebugS) "BlockStore" $+    "New best block mined at height "+      <> cs (show node.height)+      <> ": "+      <> blockHashToHex (headerHash node.header)   trySyncing-processBlockStoreMessage (BlockPeerConnect p) =+processBlockStoreMessage (BlockPeerConnect p) = do+  $(logDebugS) "BlockStore" $+    "New peer connected: " <> p.label   trySyncingPeer p-processBlockStoreMessage (BlockPeerDisconnect p) =+processBlockStoreMessage (BlockPeerDisconnect p) = do+  $(logDebugS) "BlockStore" $+    "Peer disconnected: " <> p.label   finishPeer p-processBlockStoreMessage (BlockReceived p b) =+processBlockStoreMessage (BlockReceived p b) = do+  $(logDebugS) "BlockStore" $+    "Received block: "+      <> blockHashToHex (headerHash b.header)   processBlock p b-processBlockStoreMessage (BlockNotFound p bs) =+processBlockStoreMessage (BlockNotFound p bs) = do+  $(logDebugS) "BlockStore" $+    "Blocks not found by peer "+      <> p.label+      <> ": "+      <> T.unwords (map blockHashToHex bs)   processNoBlocks p bs-processBlockStoreMessage (TxRefReceived p tx) =+processBlockStoreMessage (TxRefReceived p tx) = do+  $(logDebugS) "BlockStore" $+    "Transaction received from peer "+      <> p.label+      <> ": "+      <> txHashToHex (txHash tx)   processTx p tx-processBlockStoreMessage (TxRefAvailable p ts) =+processBlockStoreMessage (TxRefAvailable p ts) = do+  $(logDebugS) "BlockStore" $+    "Transactions available from peer "+      <> p.label+      <> ": "+      <> T.unwords (map txHashToHex ts)   processTxs p ts processBlockStoreMessage (BlockPing r) = do+  $(logDebugS) "BlockStore" "Internal clock event"   setStoreHeight   setHeadersHeight   setPendingTxs@@ -1128,7 +1173,7 @@   checkTime   atomically (r ()) -pingMe :: MonadLoggerIO m => Mailbox BlockStoreMessage -> m ()+pingMe :: (MonadLoggerIO m) => Mailbox BlockStoreMessage -> m () pingMe mbox =   forever $ do     BlockPing `query` mbox@@ -1140,84 +1185,84 @@           )     threadDelay delay -blockStorePeerConnect :: MonadIO m => Peer -> BlockStore -> m ()+blockStorePeerConnect :: (MonadIO m) => Peer -> BlockStore -> m () blockStorePeerConnect peer store =-  BlockPeerConnect peer `send` myMailbox store+  BlockPeerConnect peer `send` store.mailbox  blockStorePeerDisconnect ::-  MonadIO m => Peer -> BlockStore -> m ()+  (MonadIO m) => Peer -> BlockStore -> m () blockStorePeerDisconnect peer store =-  BlockPeerDisconnect peer `send` myMailbox store+  BlockPeerDisconnect peer `send` store.mailbox  blockStoreHead ::-  MonadIO m => BlockNode -> BlockStore -> m ()+  (MonadIO m) => BlockNode -> BlockStore -> m () blockStoreHead node store =-  BlockNewBest node `send` myMailbox store+  BlockNewBest node `send` store.mailbox  blockStoreBlock ::-  MonadIO m => Peer -> Block -> BlockStore -> m ()+  (MonadIO m) => Peer -> Block -> BlockStore -> m () blockStoreBlock peer block store =-  BlockReceived peer block `send` myMailbox store+  BlockReceived peer block `send` store.mailbox  blockStoreNotFound ::-  MonadIO m => Peer -> [BlockHash] -> BlockStore -> m ()+  (MonadIO m) => Peer -> [BlockHash] -> BlockStore -> m () blockStoreNotFound peer blocks store =-  BlockNotFound peer blocks `send` myMailbox store+  BlockNotFound peer blocks `send` store.mailbox  blockStoreTx ::-  MonadIO m => Peer -> Tx -> BlockStore -> m ()+  (MonadIO m) => Peer -> Tx -> BlockStore -> m () blockStoreTx peer tx store =-  TxRefReceived peer tx `send` myMailbox store+  TxRefReceived peer tx `send` store.mailbox  blockStoreTxHash ::-  MonadIO m => Peer -> [TxHash] -> BlockStore -> m ()+  (MonadIO m) => Peer -> [TxHash] -> BlockStore -> m () blockStoreTxHash peer txhashes store =-  TxRefAvailable peer txhashes `send` myMailbox store+  TxRefAvailable peer txhashes `send` store.mailbox  blockStorePeerConnectSTM ::   Peer -> BlockStore -> STM () blockStorePeerConnectSTM peer store =-  BlockPeerConnect peer `sendSTM` myMailbox store+  BlockPeerConnect peer `sendSTM` store.mailbox  blockStorePeerDisconnectSTM ::   Peer -> BlockStore -> STM () blockStorePeerDisconnectSTM peer store =-  BlockPeerDisconnect peer `sendSTM` myMailbox store+  BlockPeerDisconnect peer `sendSTM` store.mailbox  blockStoreHeadSTM ::   BlockNode -> BlockStore -> STM () blockStoreHeadSTM node store =-  BlockNewBest node `sendSTM` myMailbox store+  BlockNewBest node `sendSTM` store.mailbox  blockStoreBlockSTM ::   Peer -> Block -> BlockStore -> STM () blockStoreBlockSTM peer block store =-  BlockReceived peer block `sendSTM` myMailbox store+  BlockReceived peer block `sendSTM` store.mailbox  blockStoreNotFoundSTM ::   Peer -> [BlockHash] -> BlockStore -> STM () blockStoreNotFoundSTM peer blocks store =-  BlockNotFound peer blocks `sendSTM` myMailbox store+  BlockNotFound peer blocks `sendSTM` store.mailbox  blockStoreTxSTM ::   Peer -> Tx -> BlockStore -> STM () blockStoreTxSTM peer tx store =-  TxRefReceived peer tx `sendSTM` myMailbox store+  TxRefReceived peer tx `sendSTM` store.mailbox  blockStoreTxHashSTM ::   Peer -> [TxHash] -> BlockStore -> STM () blockStoreTxHashSTM peer txhashes store =-  TxRefAvailable peer txhashes `sendSTM` myMailbox store+  TxRefAvailable peer txhashes `sendSTM` store.mailbox  blockStorePendingTxs ::-  MonadIO m => BlockStore -> m Int+  (MonadIO m) => BlockStore -> m Int blockStorePendingTxs =   atomically . blockStorePendingTxsSTM  blockStorePendingTxsSTM ::   BlockStore -> STM Int blockStorePendingTxsSTM BlockStore {..} = do-  x <- HashMap.keysSet <$> readTVar myTxs+  x <- HashMap.keysSet <$> readTVar txs   y <- readTVar requested   return $ HashSet.size $ x `HashSet.union` y @@ -1228,9 +1273,9 @@   Just block ->     height <> sep <> t <> sep <> hash <> sep <> size block   where-    height = cs $ show (nodeHeight bn)-    b = posixSecondsToUTCTime . fromIntegral . blockTimestamp $ nodeHeader bn+    height = cs $ show bn.height+    b = posixSecondsToUTCTime $ fromIntegral bn.header.timestamp     t = cs $ formatTime defaultTimeLocale "%FT%T" b-    hash = blockHashToHex (headerHash (nodeHeader bn))+    hash = blockHashToHex (headerHash bn.header)     sep = " | "     size = (<> " bytes") . cs . show . B.length . encode
src/Haskoin/Store/Cache.hs view
@@ -2,15 +2,18 @@ {-# LANGUAGE BlockArguments #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-}+{-# LANGUAGE NoFieldSelectors #-}  module Haskoin.Store.Cache   ( CacheConfig (..),@@ -97,6 +100,7 @@     BlockHash,     BlockHeader (..),     BlockNode (..),+    Ctx,     DerivPathI (..),     KeyIndex,     OutPoint (..),@@ -124,6 +128,8 @@     chainGetAncestor,     chainGetBest,     chainGetBlock,+    chainGetParents,+    chainGetSplitBlock,   ) import Haskoin.Store.Common import Haskoin.Store.Data@@ -167,60 +173,60 @@   ) import UnliftIO.Concurrent (threadDelay) -runRedis :: MonadLoggerIO m => Redis (Either Reply a) -> CacheX m a-runRedis action =-  asks cacheConn >>= \conn ->-    liftIO (Redis.runRedis conn action) >>= \case-      Right x -> return x-      Left e -> do-        $(logErrorS) "Cache" $ "Got error from Redis: " <> cs (show e)-        throwIO (RedisError e)+runRedis :: (MonadLoggerIO m) => Redis (Either Reply a) -> CacheX m a+runRedis action = do+  conn <- asks (.redis)+  liftIO (Redis.runRedis conn action) >>= \case+    Right x -> return x+    Left e -> do+      $(logErrorS) "Cache" $ "Got error from Redis: " <> cs (show e)+      throwIO (RedisError e)  data CacheConfig = CacheConfig-  { cacheConn :: !Connection,-    cacheMin :: !Int,-    cacheMax :: !Integer,-    cacheChain :: !Chain,-    cacheMetrics :: !(Maybe CacheMetrics)+  { redis :: !Connection,+    minAddrs :: !Int,+    maxKeys :: !Integer,+    chain :: !Chain,+    metrics :: !(Maybe CacheMetrics)   }  data CacheMetrics = CacheMetrics   { cacheHits :: !Metrics.Counter,     cacheMisses :: !Metrics.Counter,-    cacheLockAcquired :: !Metrics.Counter,-    cacheLockReleased :: !Metrics.Counter,-    cacheLockFailed :: !Metrics.Counter,-    cacheXPubBals :: !Metrics.Counter,-    cacheXPubUnspents :: !Metrics.Counter,-    cacheXPubTxs :: !Metrics.Counter,-    cacheXPubTxCount :: !Metrics.Counter,-    cacheIndexTime :: !StatDist+    lockAcquired :: !Metrics.Counter,+    lockReleased :: !Metrics.Counter,+    lockFailed :: !Metrics.Counter,+    xPubBals :: !Metrics.Counter,+    xPubUnspents :: !Metrics.Counter,+    xPubTx :: !Metrics.Counter,+    xPubTxCount :: !Metrics.Counter,+    indexTime :: !StatDist   } -newCacheMetrics :: MonadIO m => Metrics.Store -> m CacheMetrics+newCacheMetrics :: (MonadIO m) => Metrics.Store -> m CacheMetrics newCacheMetrics s = liftIO $ do   cacheHits <- c "cache.hits"   cacheMisses <- c "cache.misses"-  cacheLockAcquired <- c "cache.lock_acquired"-  cacheLockReleased <- c "cache.lock_released"-  cacheLockFailed <- c "cache.lock_failed"-  cacheIndexTime <- d "cache.index"-  cacheXPubBals <- c "cache.xpub_balances_cached"-  cacheXPubUnspents <- c "cache.xpub_unspents_cached"-  cacheXPubTxs <- c "cache.xpub_txs_cached"-  cacheXPubTxCount <- c "cache.xpub_tx_count_cached"+  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"   return CacheMetrics {..}   where     c x = Metrics.createCounter x s     d x = createStatDist x s  withMetrics ::-  MonadUnliftIO m =>+  (MonadUnliftIO m) =>   (CacheMetrics -> StatDist) ->   CacheX m a ->   CacheX m a withMetrics df go =-  asks cacheMetrics >>= \case+  asks (.metrics) >>= \case     Nothing -> go     Just m ->       bracket@@ -235,12 +241,12 @@       addStatQuery (df metrics)  incrementCounter ::-  MonadIO m =>+  (MonadIO m) =>   (CacheMetrics -> Metrics.Counter) ->   Int ->   CacheX m () incrementCounter f i =-  asks cacheMetrics >>= \case+  asks (.metrics) >>= \case     Just s -> liftIO $ Metrics.Counter.add (f s) (fromIntegral i)     Nothing -> return () @@ -254,7 +260,7 @@   | LogicError !String   deriving (Show, Eq, Generic, NFData, Exception) -connectRedis :: MonadIO m => String -> m Connection+connectRedis :: (MonadIO m) => String -> m Connection connectRedis redisurl = do   conninfo <-     if null redisurl@@ -268,6 +274,7 @@   (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>   StoreReadBase (CacheT m)   where+  getCtx = lift getCtx   getNetwork = lift getNetwork   getBestBlock = lift getBestBlock   getBlocksAtHeight = lift . getBlocksAtHeight@@ -323,7 +330,7 @@         lift $           runReaderT (getXPubTxCount xpub xbals) cfg -withCache :: StoreReadBase m => Maybe CacheConfig -> CacheT m a -> m a+withCache :: (StoreReadBase m) => Maybe CacheConfig -> CacheT m a -> m a withCache s f = runReaderT f s  balancesPfx :: ByteString@@ -347,17 +354,18 @@ getXPubTxs xpub xbals limits = go False   where     go m =-      isXPubCached xpub >>= \case-        True -> do-          txs <- cacheGetXPubTxs xpub limits-          incrementCounter cacheXPubTxs (length txs)-          return txs-        False ->-          if m-            then lift $ xPubTxs xpub xbals limits-            else do-              newXPubC xpub xbals-              go True+      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  getXPubTxCount ::   (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m) =>@@ -368,16 +376,17 @@   go False   where     go t =-      isXPubCached xpub >>= \case-        True -> do-          incrementCounter cacheXPubTxCount 1-          cacheGetXPubTxCount xpub-        False ->-          if t-            then lift $ xPubTxCount xpub xbals-            else do-              newXPubC xpub xbals-              go True+      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  getXPubUnspents ::   (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m) =>@@ -389,36 +398,38 @@   go False   where     xm =-      let f x = (balanceAddress (xPubBal x), x)-          g = (> 0) . balanceUnspentCount . xPubBal+      let f x = (x.balance.address, x)+          g = (> 0) . (.balance.utxo)        in HashMap.fromList $ map f $ filter g xbals     go m =-      isXPubCached xpub >>= \case-        True -> do-          process-        False ->-          if m-            then lift $ xPubUnspents xpub xbals limits-            else do-              newXPubC xpub xbals-              go True+      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 (unspentScript u))+              (scriptToAddressBS ctx u.script)           g a = HashMap.lookup a xm           h u x =             XPubUnspent-              { xPubUnspent = u,-                xPubUnspentPath = xPubBalPath x+              { unspent = u,+                path = x.path               }           us = mapMaybe f uns           i a u = h u <$> g a-      incrementCounter cacheXPubUnspents (length us)+      incrementCounter (.xPubUnspents) (length us)       return $ mapMaybe (uncurry i) us  getXPubBalances ::@@ -426,34 +437,35 @@   XPubSpec ->   CacheX m [XPubBal] getXPubBalances xpub =-  isXPubCached xpub >>= \case-    True -> do-      xbals <- cacheGetXPubBalances xpub-      incrementCounter cacheXPubBals (length xbals)-      return xbals-    False -> do-      bals <- lift $ xPubBals xpub-      newXPubC xpub bals-      return bals+  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 -isInCache :: MonadLoggerIO m => XPubSpec -> CacheT m Bool+isInCache :: (MonadLoggerIO m) => XPubSpec -> CacheT m Bool isInCache xpub =   ask >>= \case     Nothing -> return False     Just cfg -> runReaderT (isXPubCached xpub) cfg -isXPubCached :: MonadLoggerIO m => XPubSpec -> CacheX m Bool-isXPubCached xpub = do-  cached <- runRedis (redisIsXPubCached xpub)-  if cached-    then incrementCounter cacheHits 1-    else incrementCounter cacheMisses 1-  return cached+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 -redisIsXPubCached :: RedisCtx m f => XPubSpec -> m (f Bool)+redisIsXPubCached :: (RedisCtx m f) => XPubSpec -> m (f Bool) redisIsXPubCached xpub = Redis.exists (balancesPfx <> encode xpub) -cacheGetXPubBalances :: MonadLoggerIO m => XPubSpec -> CacheX m [XPubBal]+cacheGetXPubBalances :: (MonadLoggerIO m) => XPubSpec -> CacheX m [XPubBal] cacheGetXPubBalances xpub = do   bals <- runRedis $ redisGetXPubBalances xpub   touchKeys [xpub]@@ -468,7 +480,7 @@   touchKeys [xpub]   return count -redisGetXPubTxCount :: RedisCtx m f => XPubSpec -> m (f Integer)+redisGetXPubTxCount :: (RedisCtx m f) => XPubSpec -> m (f Integer) redisGetXPubTxCount xpub = Redis.zcard (txSetPfx <> encode xpub)  cacheGetXPubTxs ::@@ -477,12 +489,12 @@   Limits ->   CacheX m [TxRef] cacheGetXPubTxs xpub limits =-  case start limits of+  case limits.start of     Nothing ->       go1 Nothing     Just (AtTx th) ->       lift (getTxData th) >>= \case-        Just TxData {txDataBlock = b@BlockRef {}} ->+        Just TxData {block = b@BlockRef {}} ->           go1 $ Just (blockRefScore b)         _ ->           go2 th@@ -495,8 +507,8 @@           getFromSortedSet             (txSetPfx <> encode xpub)             score-            (offset limits)-            (limit limits)+            limits.offset+            limits.limit       touchKeys [xpub]       return $ map (uncurry f) xs     go2 hash = do@@ -515,12 +527,12 @@       return $         map (uncurry f) $           l $-            drop (fromIntegral (offset limits)) xs'+            drop (fromIntegral limits.offset) xs'     l =-      if limit limits > 0-        then take (fromIntegral (limit limits))+      if limits.limit > 0+        then take (fromIntegral limits.limit)         else id-    f t s = TxRef {txRefHash = t, txRefBlock = scoreBlockRef s}+    f t s = TxRef {txid = t, block = scoreBlockRef s}  cacheGetXPubUnspents ::   (StoreReadBase m, MonadLoggerIO m) =>@@ -528,12 +540,12 @@   Limits ->   CacheX m [(BlockRef, OutPoint)] cacheGetXPubUnspents xpub limits =-  case start limits of+  case limits.start of     Nothing ->       go1 Nothing     Just (AtTx th) ->       lift (getTxData th) >>= \case-        Just TxData {txDataBlock = b@BlockRef {}} ->+        Just TxData {block = b@BlockRef {}} ->           go1 (Just (blockRefScore b))         _ ->           go2 th@@ -546,8 +558,8 @@           getFromSortedSet             (utxoPfx <> encode xpub)             score-            (offset limits)-            (limit limits)+            limits.offset+            limits.limit       touchKeys [xpub]       return $ map (uncurry f) xs     go2 hash = do@@ -560,16 +572,16 @@             0       touchKeys [xpub]       let xs' =-            if any ((== hash) . outPointHash . fst) xs-              then dropWhile ((/= hash) . outPointHash . fst) xs+            if any ((== hash) . (.hash) . fst) xs+              then dropWhile ((/= hash) . (.hash) . fst) xs               else []       return $         map (uncurry f) $           l $-            drop (fromIntegral (offset limits)) xs'+            drop (fromIntegral limits.offset) xs'     l =-      if limit limits > 0-        then take (fromIntegral (limit limits))+      if limits.limit > 0+        then take (fromIntegral limits.limit)         else id     f o s = (scoreBlockRef s, o) @@ -577,22 +589,22 @@ redisGetXPubBalances xpub =   fmap (sort . map (uncurry f)) <$> getAllFromMap (balancesPfx <> encode xpub)   where-    f p b = XPubBal {xPubBalPath = p, xPubBal = b}+    f p b = XPubBal {path = p, balance = b}  blockRefScore :: BlockRef -> Double-blockRefScore BlockRef {blockRefHeight = h, blockRefPos = p} =+blockRefScore BlockRef {height = h, position = p} =   fromIntegral (0x001fffffffffffff - (h' .|. p'))   where     h' = (fromIntegral h .&. 0x07ffffff) `shift` 26 :: Word64     p' = (fromIntegral p .&. 0x03ffffff) :: Word64-blockRefScore MemRef {memRefTime = t} = negate t'+blockRefScore MemRef {timestamp = t} = negate t'   where     t' = fromIntegral (t .&. 0x001fffffffffffff)  scoreBlockRef :: Double -> BlockRef scoreBlockRef s-  | s < 0 = MemRef {memRefTime = n}-  | otherwise = BlockRef {blockRefHeight = h, blockRefPos = p}+  | s < 0 = MemRef {timestamp = n}+  | otherwise = BlockRef {height = h, position = p}   where     n = truncate (abs s) :: Word64     m = 0x001fffffffffffff - n@@ -668,8 +680,8 @@ type CacheWriter = Mailbox CacheWriterMessage  data AddressXPub = AddressXPub-  { addressXPubSpec :: !XPubSpec,-    addressXPubPath :: ![KeyIndex]+  { spec :: !XPubSpec,+    path :: ![KeyIndex]   }   deriving (Show, Eq, Generic, NFData, Serialize) @@ -685,10 +697,10 @@ maxKey :: ByteString maxKey = "max" -xPubAddrFunction :: DeriveType -> XPubKey -> Address-xPubAddrFunction DeriveNormal = xPubAddr-xPubAddrFunction DeriveP2SH = xPubCompatWitnessAddr-xPubAddrFunction DeriveP2WPKH = xPubWitnessAddr+xPubAddrFunction :: Ctx -> DeriveType -> XPubKey -> Address+xPubAddrFunction ctx DeriveNormal = xPubAddr ctx+xPubAddrFunction ctx DeriveP2SH = xPubCompatWitnessAddr ctx+xPubAddrFunction ctx DeriveP2WPKH = xPubWitnessAddr ctx  cacheWriter ::   (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m) =>@@ -702,40 +714,41 @@       newBlockC       syncMempoolC       forever $ do+        $(logDebugS) "Cache" "Awaiting event..."         x <- receive inbox         cacheWriterReact x -lockIt :: MonadLoggerIO m => CacheX m Bool+lockIt :: (MonadLoggerIO m) => CacheX m Bool lockIt = do   go >>= \case     Right Redis.Ok -> do       $(logDebugS) "Cache" "Acquired lock"-      incrementCounter cacheLockAcquired 1+      incrementCounter (.lockAcquired) 1       return True     Right Redis.Pong -> do       $(logErrorS)         "Cache"         "Unexpected pong when acquiring lock"-      incrementCounter cacheLockFailed 1+      incrementCounter (.lockFailed) 1       return False     Right (Redis.Status s) -> do       $(logErrorS) "Cache" $         "Unexpected status acquiring lock: " <> cs s-      incrementCounter cacheLockFailed 1+      incrementCounter (.lockFailed) 1       return False     Left (Redis.Bulk Nothing) -> do       $(logDebugS) "Cache" "Lock already taken"-      incrementCounter cacheLockFailed 1+      incrementCounter (.lockFailed) 1       return False     Left e -> do       $(logErrorS)         "Cache"         "Error when trying to acquire lock"-      incrementCounter cacheLockFailed 1+      incrementCounter (.lockFailed) 1       throwIO (RedisError e)   where     go = do-      conn <- asks cacheConn+      conn <- asks (.redis)       liftIO . Redis.runRedis conn $ do         let opts =               Redis.SetOpts@@ -745,7 +758,7 @@                 }         Redis.setOpts "lock" "locked" opts -refreshLock :: MonadLoggerIO m => CacheX m ()+refreshLock :: (MonadLoggerIO m) => CacheX m () refreshLock = void . runRedis $ do   let opts =         Redis.SetOpts@@ -755,7 +768,7 @@           }   Redis.setOpts "lock" "locked" opts -unlockIt :: MonadLoggerIO m => Bool -> CacheX m ()+unlockIt :: (MonadLoggerIO m) => Bool -> CacheX m () unlockIt False = return () unlockIt True = void $ runRedis (Redis.del ["lock"]) @@ -777,7 +790,7 @@   (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>   CacheX m Bool isFull = do-  x <- asks cacheMax+  x <- asks (.maxKeys)   s <- runRedis Redis.dbsize   return $ s > x @@ -785,7 +798,8 @@   (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>   CacheX m Integer pruneDB = do-  x <- asks (((`div` 10) . (* 8)) . cacheMax) -- Prune to 80% of max+  x <- asks (((`div` 10) . (* 8)) . (.maxKeys))+  -- Prune to 80% of max   s <- runRedis Redis.dbsize   if s > x then flush (s - x) else return 0   where@@ -800,7 +814,7 @@             "Pruning " <> cs (show (length ks)) <> " old xpubs"           delXPubKeys ks -touchKeys :: MonadLoggerIO m => [XPubSpec] -> CacheX m ()+touchKeys :: (MonadLoggerIO m) => [XPubSpec] -> CacheX m () touchKeys xpubs = do   now <- systemSeconds <$> liftIO getSystemTime   runRedis $ redisTouchKeys now xpubs@@ -815,16 +829,21 @@   CacheWriterMessage ->   CacheX m () cacheWriterReact CacheNewBlock = do+  $(logDebugS) "Cache" "Received new block event"   newBlockC   syncMempoolC-cacheWriterReact (CacheNewTx txid) =+cacheWriterReact (CacheNewTx txid) = do+  $(logDebugS) "Cache" $+    "Received new transaction event: " <> txHashToHex txid   syncNewTxC [txid] cacheWriterReact (CacheSyncMempool l) = do+  $(logDebugS) "Cache" "Received sync mempool event"+  newBlockC   syncMempoolC   atomically $ l ()  lenNotNull :: [XPubBal] -> Int-lenNotNull = length . filter (not . nullBalance . xPubBal)+lenNotNull = length . filter (not . nullBalance . (.balance))  newXPubC ::   (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m) =>@@ -834,7 +853,7 @@ newXPubC xpub xbals =   should_index >>= \i -> when i $     bracket set_index unset_index $ \j -> when j $-      withMetrics cacheIndexTime $ do+      withMetrics (.indexTime) $ do         xpubtxt <- xpubText xpub         $(logDebugS) "Cache" $           "Caching "@@ -867,9 +886,9 @@           return $ b >> c >> d >> e >> return ()         $(logDebugS) "Cache" $ "Cached " <> xpubtxt   where-    op XPubUnspent {xPubUnspent = u} = (unspentPoint u, unspentBlock u)+    op XPubUnspent {unspent = u} = (u.outpoint, u.block)     should_index =-      asks cacheMin >>= \x ->+      asks (.minAddrs) >>= \x ->         if x <= lenNotNull xbals           then             inSync >>= \s ->@@ -886,9 +905,9 @@         }     red = Redis.setOpts key "1" opts     unset_index y = when y . void . runRedis $ Redis.del [key]-    set_index =-      asks cacheConn >>= \conn ->-        liftIO (Redis.runRedis conn red) <&> isRight+    set_index = do+      conn <- asks (.redis)+      liftIO (Redis.runRedis conn red) <&> isRight  inSync ::   (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m) =>@@ -897,72 +916,52 @@   lift getBestBlock >>= \case     Nothing -> return False     Just bb -> do-      ch <- asks cacheChain+      ch <- asks (.chain)       cb <- chainGetBest ch-      return $ nodeHeight cb > 0 && headerHash (nodeHeader cb) == bb+      return $ cb.height > 0 && headerHash cb.header == bb  newBlockC ::   (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m) =>   CacheX m () newBlockC =   inSync >>= \s -> when s . void . withLock $ do-    ch <- asks cacheChain-    bn <- chainGetBest ch-    cn <- cacheGetHead-    case cn of-      Nothing -> do-        $(logInfoS) "Cache" "Initializing best cache block"-        do_import bn-      Just hb ->-        if hb == headerHash (nodeHeader bn)-          then $(logDebugS) "Cache" "Cache in sync"-          else do-            sync ch hb bn-            void pruneDB+    get_best_block_node >>= \case+      Nothing -> $(logErrorS) "Cache" "No best block available"+      Just best_block_node ->+        cacheGetHead >>= \case+          Nothing -> do+            $(logInfoS) "Cache" "Initializing best cache block"+            importBlockC $ headerHash best_block_node.header+          Just cache_head_hash ->+            get_block_node cache_head_hash >>= \case+              Nothing -> do+                $(logErrorS) "Cache" $+                  "Could not get best cache block: "+                    <> blockHashToHex cache_head_hash+              Just cache_head_node -> do+                blocks <- get_blocks cache_head_node best_block_node+                mapM_ importBlockC blocks   where-    sync ch hb bn =-      chainGetBlock hb ch >>= \case-        Nothing -> do-          $(logErrorS) "Cache" $-            "Cache head block node not found: "-              <> blockHashToHex hb-        Just hn ->-          chainBlockMain hb ch >>= \m ->-            if m-              then next ch bn hn-              else do-                $(logDebugS) "Cache" $-                  "Reverting cache head not in main chain: "-                    <> blockHashToHex hb-                removeHeadC hb-                cacheGetHead >>= \case-                  Nothing -> do_import bn-                  Just hb' -> sync ch hb' bn-    next ch bn hn =-      if-          | prevBlock (nodeHeader bn) == headerHash (nodeHeader hn) ->-              do_import bn-          | nodeHeight bn > nodeHeight hn ->-              chainGetAncestor (nodeHeight hn + 1) bn ch >>= \case-                Nothing -> do-                  $(logErrorS) "Cache" $-                    "Ancestor not found at height "-                      <> cs (show (nodeHeight hn + 1))-                      <> " for block: "-                      <> blockHashToHex (headerHash (nodeHeader bn))-                  throwIO $-                    LogicError $-                      "Ancestor not found at height "-                        <> show (nodeHeight hn + 1)-                        <> " for block: "-                        <> cs (blockHashToHex (headerHash (nodeHeader bn)))-                Just hn' -> do-                  do_import hn'-                  next ch bn hn'-          | otherwise ->-              $(logInfoS) "Cache" "Cache best block higher than this node's"-    do_import bn = do-      importBlockC . headerHash $ nodeHeader bn+    get_best_block_node =+      lift getBestBlock >>= \case+        Nothing -> return Nothing+        Just best_block_hash -> get_block_node best_block_hash+    get_block_node block_hash = do+      ch <- asks (.chain)+      chainGetBlock block_hash ch+    get_blocks left_node right_node = do+      ch <- asks (.chain)+      split_node <- chainGetSplitBlock left_node right_node ch+      let split_node_hash = headerHash split_node.header+          right_node_hash = headerHash right_node.header+      if split_node_hash == right_node_hash+        then return []+        else do+          let fork_height = split_node.height + 1+          left_parents <- chainGetParents fork_height left_node ch+          right_parents <- chainGetParents fork_height right_node ch+          let blocks = reverse left_parents <> right_parents <> pure right_node+          return $ map (headerHash . (.header)) blocks  importBlockC ::   (MonadUnliftIO m, StoreReadExtra m, MonadLoggerIO m) =>@@ -971,7 +970,7 @@ importBlockC bh =   lift (getBlock bh) >>= \case     Just bd -> do-      let ths = blockDataTxs bd+      let ths = bd.txs       tds <- sortTxData . catMaybes <$> mapM (lift . getTxData) ths       $(logDebugS) "Cache" $         "Importing "@@ -993,39 +992,18 @@         "Could not get block: "           <> blockHashToHex bh -removeHeadC ::-  (StoreReadExtra m, MonadUnliftIO m, MonadLoggerIO m) =>-  BlockHash ->-  CacheX m ()-removeHeadC cb =-  void . runMaybeT $ do-    bh <- MaybeT cacheGetHead-    guard (cb == bh)-    bd <- MaybeT (lift (getBlock bh))-    lift $ do-      tds <--        sortTxData . catMaybes-          <$> mapM (lift . getTxData) (blockDataTxs bd)-      $(logDebugS) "Cache" $ "Reverting head: " <> blockHashToHex bh-      importMultiTxC tds-      $(logWarnS) "Cache" $-        "Reverted block head "-          <> blockHashToHex bh-          <> " to parent "-          <> blockHashToHex (prevBlock (blockDataHeader bd))-      cacheSetHead (prevBlock (blockDataHeader bd))- importMultiTxC ::   (MonadUnliftIO m, StoreReadExtra m, MonadLoggerIO m) =>   [TxData] ->   CacheX m () importMultiTxC txs = do+  ctx <- lift getCtx   $(logDebugS) "Cache" $ "Processing " <> cs (show (length txs)) <> " txs"   $(logDebugS) "Cache" $     "Getting address information for "-      <> cs (show (length alladdrs))+      <> cs (show (length (alladdrs ctx)))       <> " addresses"-  addrmap <- getaddrmap+  addrmap <- getaddrmap ctx   let addrs = HashMap.keys addrmap   $(logDebugS) "Cache" $     "Getting balances for "@@ -1034,9 +1012,9 @@   balmap <- getbalances addrs   $(logDebugS) "Cache" $     "Getting unspent data for "-      <> cs (show (length allops))+      <> cs (show (length (allops ctx)))       <> " outputs"-  unspentmap <- getunspents+  unspentmap <- getunspents ctx   gap <- lift getMaxGap   now <- systemSeconds <$> liftIO getSystemTime   let xpubs = allxpubsls addrmap@@ -1058,22 +1036,26 @@     let addrmap' = faddrmap (HashMap.keysSet xmap) addrmap     $(logDebugS) "Cache" "Starting Redis import pipeline"     runRedis $ do-      x <- redisImportMultiTx addrmap' unspentmap txs+      x <- redisImportMultiTx ctx addrmap' unspentmap txs       y <- redisUpdateBalances addrmap' balmap       z <- redisTouchKeys now (HashMap.keys xmap)       return $ x >> y >> z >> return ()     $(logDebugS) "Cache" "Completed Redis pipeline"-    return $ getNewAddrs gap xmap (HashMap.elems addrmap')+    return $ getNewAddrs ctx gap xmap (HashMap.elems addrmap')   cacheAddAddresses addrs'   where-    alladdrsls = HashSet.toList alladdrs-    faddrmap xmap = HashMap.filter (\a -> addressXPubSpec a `elem` xmap)-    getaddrmap =-      HashMap.fromList . catMaybes . zipWith (\a -> fmap (a,)) alladdrsls-        <$> cacheGetAddrsInfo alladdrsls-    getunspents =-      HashMap.fromList . catMaybes . zipWith (\p -> fmap (p,)) allops-        <$> lift (mapM getUnspent allops)+    alladdrsls ctx = HashSet.toList (alladdrs ctx)+    faddrmap xmap = HashMap.filter (\a -> a.spec `elem` xmap)+    getaddrmap ctx =+      HashMap.fromList+        . catMaybes+        . zipWith (\a -> fmap (a,)) (alladdrsls ctx)+        <$> cacheGetAddrsInfo (alladdrsls ctx)+    getunspents ctx =+      HashMap.fromList+        . catMaybes+        . zipWith (\p -> fmap (p,)) (allops ctx)+        <$> lift (mapM getUnspent (allops ctx))     getbalances addrs =       HashMap.fromList . zip addrs <$> mapM (lift . getDefaultBalance) addrs     getxbals xpubs = do@@ -1081,68 +1063,81 @@         bs <- redisGetXPubBalances xpub         return $ (,) xpub <$> bs       return $ HashMap.filter (not . null) (HashMap.fromList bals)-    allops = map snd $ concatMap txInputs txs <> concatMap txOutputs txs-    alladdrs =-      HashSet.fromList . map fst $-        concatMap txInputs txs <> concatMap txOutputs txs-    allxpubsls addrmap = HashSet.toList (allxpubs addrmap)-    allxpubs addrmap =-      HashSet.fromList . map addressXPubSpec $ HashMap.elems addrmap+    allops ctx =+      map snd $+        concatMap (txInputs ctx) txs+          <> concatMap (txOutputs ctx) txs+    alladdrs ctx =+      HashSet.fromList $+        map fst $+          concatMap (txInputs ctx) txs+            <> concatMap (txOutputs ctx) txs+    allxpubsls = HashSet.toList . allxpubs+    allxpubs =+      HashSet.fromList . map (.spec) . HashMap.elems  redisImportMultiTx ::   (Monad f, RedisCtx m f) =>+  Ctx ->   HashMap Address AddressXPub ->   HashMap OutPoint Unspent ->   [TxData] ->   m (f ())-redisImportMultiTx addrmap unspentmap txs = do-  xs <- mapM importtxentries txs+redisImportMultiTx ctx addrmap unspentmap tds = do+  xs <- mapM importtxentries tds   return $ sequence_ xs   where     uns p i =       case HashMap.lookup p unspentmap of         Just u ->-          redisAddXPubUnspents (addressXPubSpec i) [(p, unspentBlock u)]-        Nothing -> redisRemXPubUnspents (addressXPubSpec i) [p]+          redisAddXPubUnspents i.spec [(p, u.block)]+        Nothing -> redisRemXPubUnspents i.spec [p]     addtx tx a p =       case HashMap.lookup a addrmap of         Just i -> do           let tr =                 TxRef-                  { txRefHash = txHash (txData tx),-                    txRefBlock = txDataBlock tx+                  { txid = txHash tx.tx,+                    block = tx.block                   }-          x <- redisAddXPubTxs (addressXPubSpec i) [tr]+          x <- redisAddXPubTxs i.spec [tr]           y <- uns p i           return $ x >> y >> return ()         Nothing -> return (pure ())     remtx tx a p =       case HashMap.lookup a addrmap of         Just i -> do-          x <- redisRemXPubTxs (addressXPubSpec i) [txHash (txData tx)]+          x <- redisRemXPubTxs i.spec [txHash tx.tx]           y <- uns p i           return $ x >> y >> return ()         Nothing -> return (pure ())-    importtxentries tx =-      if txDataDeleted tx+    importtxentries td =+      if td.deleted         then do-          x <- mapM (uncurry (remtx tx)) (txaddrops tx)-          y <- redisRemFromMempool [txHash (txData tx)]+          x <-+            mapM+              (uncurry (remtx td))+              (txaddrops td)+          y <- redisRemFromMempool [txHash td.tx]           return $ sequence_ x >> void y         else do-          a <- sequence <$> mapM (uncurry (addtx tx)) (txaddrops tx)+          a <-+            sequence+              <$> mapM+                (uncurry (addtx td))+                (txaddrops td)           b <--            case txDataBlock tx of+            case td.block of               b@MemRef {} ->                 let tr =                       TxRef-                        { txRefHash = txHash (txData tx),-                          txRefBlock = b+                        { txid = txHash td.tx,+                          block = b                         }                  in redisAddToMempool [tr]-              _ -> redisRemFromMempool [txHash (txData tx)]+              _ -> redisRemFromMempool [txHash td.tx]           return $ a >> b >> return ()-    txaddrops td = txInputs td <> txOutputs td+    txaddrops td = txInputs ctx td <> txOutputs ctx td  redisUpdateBalances ::   (Monad f, RedisCtx m f) =>@@ -1153,11 +1148,11 @@   fmap (fmap mconcat . sequence) . forM (HashMap.keys addrmap) $ \a ->     case (HashMap.lookup a addrmap, HashMap.lookup a balmap) of       (Just ainfo, Just bal) ->-        redisAddXPubBalances (addressXPubSpec ainfo) [xpubbal ainfo bal]+        redisAddXPubBalances ainfo.spec [xpubbal ainfo bal]       _ -> return (pure ())   where     xpubbal ainfo bal =-      XPubBal {xPubBalPath = addressXPubPath ainfo, xPubBal = bal}+      XPubBal {path = ainfo.path, balance = bal}  cacheAddAddresses ::   (StoreReadExtra m, MonadUnliftIO m, MonadLoggerIO m) =>@@ -1165,6 +1160,7 @@   CacheX m () cacheAddAddresses [] = $(logDebugS) "Cache" "No further addresses to add" cacheAddAddresses addrs = do+  ctx <- lift getCtx   $(logDebugS) "Cache" $     "Adding " <> cs (show (length addrs)) <> " new generated addresses"   $(logDebugS) "Cache" "Getting balances"@@ -1183,55 +1179,59 @@   let xpubs =         HashSet.toList           . HashSet.fromList-          . map addressXPubSpec+          . map (.spec)           $ Map.elems amap   $(logDebugS) "Cache" "Getting xpub balances"   xmap <- getbals xpubs   gap <- lift getMaxGap   let notnulls = getnotnull balmap-      addrs' = getNewAddrs gap xmap notnulls+      addrs' = getNewAddrs ctx gap xmap notnulls   cacheAddAddresses addrs'   where     getbals xpubs = runRedis $ do       bs <- sequence <$> forM xpubs redisGetXPubBalances       return $-        HashMap.filter (not . null) . HashMap.fromList . zip xpubs <$> bs+        HashMap.filter (not . null)+          . HashMap.fromList+          . zip xpubs+          <$> bs     amap = Map.fromList addrs     getnotnull =       let f xpub =             map $ \bal ->               AddressXPub-                { addressXPubSpec = xpub,-                  addressXPubPath = xPubBalPath bal+                { spec = xpub,+                  path = bal.path                 }-          g = filter (not . nullBalance . xPubBal)+          g = filter (not . nullBalance . (.balance))        in concatMap (uncurry f) . HashMap.toList . HashMap.map g     getbal a i =       let f b =-            ( addressXPubSpec i,-              [XPubBal {xPubBal = b, xPubBalPath = addressXPubPath i}]+            ( i.spec,+              [XPubBal {balance = b, path = i.path}]             )        in f <$> lift (getDefaultBalance a)     getutxo a i =       let f us =-            ( addressXPubSpec i,-              map (\u -> (unspentPoint u, unspentBlock u)) us+            ( i.spec,+              map (\u -> (u.outpoint, u.block)) us             )        in f <$> lift (getAddressUnspents a def)     gettxmap a i =-      let f ts = (addressXPubSpec i, ts)+      let f ts = (i.spec, ts)        in f <$> lift (getAddressTxs a def)  getNewAddrs ::+  Ctx ->   KeyIndex ->   HashMap XPubSpec [XPubBal] ->   [AddressXPub] ->   [(Address, AddressXPub)]-getNewAddrs gap xpubs =+getNewAddrs ctx gap xpubs =   concatMap $ \a ->-    case HashMap.lookup (addressXPubSpec a) xpubs of+    case HashMap.lookup a.spec xpubs of       Nothing -> []-      Just bals -> addrsToAdd gap bals a+      Just bals -> addrsToAdd ctx gap bals a  syncNewTxC ::   (MonadUnliftIO m, MonadLoggerIO m, StoreReadExtra m) =>@@ -1243,7 +1243,7 @@     unless (null txs) $ do       forM_ txs $ \tx ->         $(logDebugS) "Cache" $-          "Synchronizing transaction: " <> txHashToHex (txHash (txData tx))+          "Synchronizing transaction: " <> txHashToHex (txHash tx.tx)       importMultiTxC txs  syncMempoolC ::@@ -1263,13 +1263,13 @@         "Synchronizing " <> cs (show (length txs)) <> " mempool transactions"       importMultiTxC txs -cacheGetMempool :: MonadLoggerIO m => CacheX m [(UnixTime, TxHash)]+cacheGetMempool :: (MonadLoggerIO m) => CacheX m [(UnixTime, TxHash)] cacheGetMempool = runRedis redisGetMempool -cacheIsInMempool :: MonadLoggerIO m => TxHash -> CacheX m Bool+cacheIsInMempool :: (MonadLoggerIO m) => TxHash -> CacheX m Bool cacheIsInMempool = runRedis . redisIsInMempool -cacheGetHead :: MonadLoggerIO m => CacheX m (Maybe BlockHash)+cacheGetHead :: (MonadLoggerIO m) => CacheX m (Maybe BlockHash) cacheGetHead = runRedis redisGetHead  cacheSetHead :: (MonadLoggerIO m, StoreReadBase m) => BlockHash -> CacheX m ()@@ -1278,7 +1278,7 @@   void $ runRedis (redisSetHead bh)  cacheGetAddrsInfo ::-  MonadLoggerIO m => [Address] -> CacheX m [Maybe AddressXPub]+  (MonadLoggerIO m) => [Address] -> CacheX m [Maybe AddressXPub] cacheGetAddrsInfo as = runRedis (redisGetAddrsInfo as)  redisAddToMempool :: (Applicative f, RedisCtx m f) => [TxRef] -> m (f Integer)@@ -1286,7 +1286,7 @@ redisAddToMempool btxs =   zadd mempoolSetKey $     map-      (\btx -> (blockRefScore (txRefBlock btx), encode (txRefHash btx)))+      (\btx -> (blockRefScore btx.block, encode btx.txid))       btxs  redisIsInMempool :: (Applicative f, RedisCtx m f) => TxHash -> m (f Bool)@@ -1327,7 +1327,7 @@  redisDelXPubKeys ::   (Monad f, RedisCtx m f) => XPubSpec -> [XPubBal] -> m (f Integer)-redisDelXPubKeys xpub bals = go (map (balanceAddress . xPubBal) bals)+redisDelXPubKeys xpub bals = go $ map (.balance.address) bals   where     go addrs = do       addrcount <-@@ -1351,7 +1351,7 @@ redisAddXPubTxs _ [] = return (pure 0) redisAddXPubTxs xpub btxs =   zadd (txSetPfx <> encode xpub) $-    map (\t -> (blockRefScore (txRefBlock t), encode (txRefHash t))) btxs+    map (\t -> (blockRefScore t.block, encode t.txid)) btxs  redisRemXPubTxs ::   (Applicative f, RedisCtx m f) => XPubSpec -> [TxHash] -> m (f Integer)@@ -1383,16 +1383,16 @@   xs <- mapM (uncurry (Redis.hset (balancesPfx <> encode xpub))) entries   ys <- forM bals $ \b ->     redisSetAddrInfo-      (balanceAddress (xPubBal b))+      b.balance.address       AddressXPub-        { addressXPubSpec = xpub,-          addressXPubPath = xPubBalPath b+        { spec = xpub,+          path = b.path         }   return $ sequence_ xs >> sequence_ ys   where-    entries = map (\b -> (encode (xPubBalPath b), encode (xPubBal b))) bals+    entries = map (\b -> (encode b.path, encode b.balance)) bals -redisSetHead :: RedisCtx m f => BlockHash -> m (f Redis.Status)+redisSetHead :: (RedisCtx m f) => BlockHash -> m (f Redis.Status) redisSetHead bh = Redis.set bestBlockKey (encode bh)  redisGetAddrsInfo ::@@ -1404,60 +1404,69 @@     is' <- sequence is     return $ map (eitherToMaybe . decode =<<) is' -addrsToAdd :: KeyIndex -> [XPubBal] -> AddressXPub -> [(Address, AddressXPub)]-addrsToAdd gap xbals addrinfo-  | null fbals = []-  | not haschange = zipWith f addrs list <> zipWith f changeaddrs changelist-  | otherwise = zipWith f addrs list+addrsToAdd ::+  Ctx ->+  KeyIndex ->+  [XPubBal] ->+  AddressXPub ->+  [(Address, AddressXPub)]+addrsToAdd ctx gap xbals addrinfo+  | null fbals =+      []+  | not haschange =+      zipWith f (addrs ctx) list+        <> zipWith f (changeaddrs ctx) changelist+  | otherwise =+      zipWith f (addrs ctx) list   where-    haschange = any ((== 1) . head . xPubBalPath) xbals-    f a p = (a, AddressXPub {addressXPubSpec = xpub, addressXPubPath = p})-    dchain = head (addressXPubPath addrinfo)-    fbals = filter ((== dchain) . head . xPubBalPath) xbals-    maxidx = maximum (map (head . tail . xPubBalPath) fbals)-    xpub = addressXPubSpec addrinfo-    aidx = (head . tail) (addressXPubPath addrinfo)+    haschange = any ((== 1) . head . (.path)) xbals+    f a p = (a, AddressXPub {spec = xpub, path = p})+    dchain = head addrinfo.path+    fbals = filter ((== dchain) . head . (.path)) xbals+    maxidx = maximum (map (head . tail . (.path)) fbals)+    xpub = addrinfo.spec+    aidx = (head . tail) addrinfo.path     ixs =       if gap > maxidx - aidx         then [maxidx + 1 .. aidx + gap]         else []     paths = map (Deriv :/ dchain :/) ixs-    keys = map (\p -> derivePubPath p (xPubSpecKey xpub))+    keys ctx = map (\p -> derivePubPath ctx p xpub.key)     list = map pathToList paths-    xpubf = xPubAddrFunction (xPubDeriveType xpub)-    addrs = map xpubf (keys paths)+    xpubf ctx = xPubAddrFunction ctx xpub.deriv+    addrs ctx = map (xpubf ctx) (keys ctx paths)     changepaths = map (Deriv :/ 1 :/) [0 .. gap - 1]-    changeaddrs = map xpubf (keys changepaths)+    changeaddrs ctx = map (xpubf ctx) (keys ctx changepaths)     changelist = map pathToList changepaths  sortTxData :: [TxData] -> [TxData] sortTxData tds =-  let txm = Map.fromList (map (\d -> (txHash (txData d), d)) tds)-      ths = map (txHash . snd) (sortTxs (map txData tds))+  let txm = Map.fromList (map (\d -> (txHash d.tx, d)) tds)+      ths = map (txHash . snd) (sortTxs (map (.tx) tds))    in mapMaybe (`Map.lookup` txm) ths -txInputs :: TxData -> [(Address, OutPoint)]-txInputs td =-  let is = txIn (txData td)-      ps = I.toAscList (txDataPrevs td)-      as = map (scriptToAddressBS . prevScript . snd) ps-      f (Right a) i = Just (a, prevOutput i)+txInputs :: Ctx -> TxData -> [(Address, OutPoint)]+txInputs ctx td =+  let is = td.tx.inputs+      ps = I.toAscList td.prevs+      as = map (scriptToAddressBS ctx . (.script) . snd) ps+      f (Right a) i = Just (a, i.outpoint)       f (Left _) _ = Nothing    in catMaybes (zipWith f as is) -txOutputs :: TxData -> [(Address, OutPoint)]-txOutputs td =+txOutputs :: Ctx -> TxData -> [(Address, OutPoint)]+txOutputs ctx td =   let ps =         zipWith           ( \i _ ->               OutPoint-                { outPointHash = txHash (txData td),-                  outPointIndex = i+                { hash = txHash td.tx,+                  index = i                 }           )           [0 ..]-          (txOut (txData td))-      as = map (scriptToAddressBS . scriptOutput) (txOut (txData td))+          td.tx.outputs+      as = map (scriptToAddressBS ctx . (.script)) td.tx.outputs       f (Right a) p = Just (a, p)       f (Left _) _ = Nothing    in catMaybes (zipWith f as ps)@@ -1472,25 +1481,29 @@   xs <- getFromSortedSet mempoolSetKey Nothing 0 0   return $ map (uncurry f) <$> xs   where-    f t s = (memRefTime (scoreBlockRef s), t)+    f t s = ((scoreBlockRef s).timestamp, t)  xpubText ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>+  ( MonadUnliftIO m,+    MonadLoggerIO m,+    StoreReadBase m+  ) =>   XPubSpec ->   CacheX m Text xpubText xpub = do   net <- lift getNetwork-  let suffix = case xPubDeriveType xpub of+  let suffix = case xpub.deriv of         DeriveNormal -> ""         DeriveP2SH -> "/p2sh"         DeriveP2WPKH -> "/p2wpkh"-  return . cs $ suffix <> xPubExport net (xPubSpecKey xpub)+  ctx <- lift getCtx+  return . cs $ suffix <> xPubExport net ctx xpub.key -cacheNewBlock :: MonadIO m => CacheWriter -> m ()+cacheNewBlock :: (MonadIO m) => CacheWriter -> m () cacheNewBlock = send CacheNewBlock -cacheNewTx :: MonadIO m => TxHash -> CacheWriter -> m ()+cacheNewTx :: (MonadIO m) => TxHash -> CacheWriter -> m () cacheNewTx = send . CacheNewTx -cacheSyncMempool :: MonadIO m => CacheWriter -> m ()+cacheSyncMempool :: (MonadIO m) => CacheWriter -> m () cacheSyncMempool = query CacheSyncMempool
src/Haskoin/Store/Common.hs view
@@ -1,11 +1,15 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TupleSections #-}+{-# LANGUAGE NoFieldSelectors #-}  module Haskoin.Store.Common   ( Limits (..),@@ -59,14 +63,12 @@ import Control.Monad.Trans.Reader (runReaderT) import Data.ByteString (ByteString) import Data.Default (Default (..))-import qualified Data.Graph as G-import qualified Data.HashSet as H-import qualified Data.HashMap.Strict as M+import Data.HashSet qualified as H import Data.Hashable (Hashable) import Data.IntMap.Strict (IntMap)-import qualified Data.IntMap.Strict as I+import Data.IntMap.Strict qualified as I import Data.List (sortOn)-import qualified Data.Map.Strict as Map+import Data.Map.Strict qualified as Map import Data.Maybe (catMaybes, mapMaybe) import Data.Ord (Down (..)) import Data.Serialize (Serialize (..))@@ -78,29 +80,6 @@ import Data.Word (Word32, Word64) import GHC.Generics (Generic) import Haskoin-  ( Address,-    BlockHash,-    BlockHeader (..),-    BlockHeight,-    BlockNode (..),-    KeyIndex,-    Network (..),-    OutPoint (..),-    RejectCode (..),-    Tx (..),-    TxHash (..),-    TxIn (..),-    XPubKey (..),-    deriveAddr,-    deriveCompatWitnessAddr,-    deriveWitnessAddr,-    firstGreaterOrEqual,-    headerHash,-    lastSmallerOrEqual,-    mtp,-    pubSubKey,-    txHash,-  ) import Haskoin.Node (Chain, Peer) import Haskoin.Store.Data   ( Balance (..),@@ -120,9 +99,9 @@     toTransaction,     zeroBalance,   )-import qualified System.Metrics as Metrics+import System.Metrics qualified as Metrics import System.Metrics.Counter (Counter)-import qualified System.Metrics.Counter as Counter+import System.Metrics.Counter qualified as Counter import UnliftIO (MonadIO, liftIO)  type DeriveAddr = XPubKey -> KeyIndex -> Address@@ -149,8 +128,9 @@ instance Default Limits where   def = defaultLimits -class Monad m => StoreReadBase m where+class (Monad m) => StoreReadBase m where   getNetwork :: m Network+  getCtx :: m Ctx   getBestBlock :: m (Maybe BlockHash)   getBlocksAtHeight :: BlockHeight -> m [BlockHash]   getBlock :: BlockHash -> m (Maybe BlockData)@@ -160,7 +140,7 @@   getUnspent :: OutPoint -> m (Maybe Unspent)   getMempool :: m [(UnixTime, TxHash)] -class StoreReadBase m => StoreReadExtra m where+class (StoreReadBase m) => StoreReadExtra m where   getAddressesTxs :: [Address] -> Limits -> m [TxRef]   getAddressesUnspents :: [Address] -> Limits -> m [Unspent]   getInitialGap :: m Word32@@ -189,19 +169,19 @@   insertUnspent :: Unspent -> m ()   deleteUnspent :: OutPoint -> m () -getActiveBlock :: StoreReadExtra m => BlockHash -> m (Maybe BlockData)+getActiveBlock :: (StoreReadExtra m) => BlockHash -> m (Maybe BlockData) getActiveBlock bh =   getBlock bh >>= \case-    Just b | blockDataMainChain b -> return (Just b)+    Just b | b.main -> return (Just b)     _ -> return Nothing -getActiveTxData :: StoreReadBase m => TxHash -> m (Maybe TxData)+getActiveTxData :: (StoreReadBase m) => TxHash -> m (Maybe TxData) getActiveTxData th =   getTxData th >>= \case-    Just td | not (txDataDeleted td) -> return (Just td)+    Just td | not (td.deleted) -> return (Just td)     _ -> return Nothing -getDefaultBalance :: StoreReadBase m => Address -> m Balance+getDefaultBalance :: (StoreReadBase m) => Address -> m Balance getDefaultBalance a =   getBalance a >>= \case     Nothing -> return $ zeroBalance a@@ -210,36 +190,40 @@ deriveAddresses :: DeriveAddr -> XPubKey -> Word32 -> [(Word32, Address)] deriveAddresses derive xpub start = map (\i -> (i, derive xpub i)) [start ..] -deriveFunction :: DeriveType -> DeriveAddr-deriveFunction DeriveNormal i = fst . deriveAddr i-deriveFunction DeriveP2SH i = fst . deriveCompatWitnessAddr i-deriveFunction DeriveP2WPKH i = fst . deriveWitnessAddr i+deriveFunction :: Ctx -> DeriveType -> DeriveAddr+deriveFunction ctx DeriveNormal i = fst . deriveAddr ctx i+deriveFunction ctx DeriveP2SH i = fst . deriveCompatWitnessAddr ctx i+deriveFunction ctx DeriveP2WPKH i = fst . deriveWitnessAddr ctx i  xPubSummary :: XPubSpec -> [XPubBal] -> XPubSummary xPubSummary _xspec xbals =   XPubSummary-    { xPubSummaryConfirmed = sum (map (balanceAmount . xPubBal) bs),-      xPubSummaryZero = sum (map (balanceZero . xPubBal) bs),-      xPubSummaryReceived = rx,-      xPubUnspentCount = uc,-      xPubChangeIndex = ch,-      xPubExternalIndex = ex+    { confirmed = sum (map (.balance.confirmed) bs),+      unconfirmed = sum (map (.balance.unconfirmed) bs),+      received = rx,+      utxo = uc,+      change = ch,+      external = ex     }   where-    bs = filter (not . nullBalance . xPubBal) xbals-    ex = foldl max 0 [i | XPubBal {xPubBalPath = [0, i]} <- bs]-    ch = foldl max 0 [i | XPubBal {xPubBalPath = [1, i]} <- bs]-    uc = sum [balanceUnspentCount (xPubBal b) | b <- bs]-    xt = [b | b@XPubBal {xPubBalPath = [0, _]} <- bs]-    rx = sum [balanceTotalReceived (xPubBal b) | b <- xt]+    bs = filter (not . nullBalance . (.balance)) xbals+    ex = foldl max 0 [i | XPubBal {path = [0, i]} <- bs]+    ch = foldl max 0 [i | XPubBal {path = [1, i]} <- bs]+    uc = sum [b.balance.utxo | b <- bs]+    xt = [b | b@XPubBal {path = [0, _]} <- bs]+    rx = sum [b.balance.received | b <- xt]  getTransaction ::   (Monad m, StoreReadBase m) => TxHash -> m (Maybe Transaction)-getTransaction h = fmap toTransaction <$> getTxData h+getTransaction h = do+  ctx <- getCtx+  fmap (toTransaction ctx) <$> getTxData h  getNumTransaction ::   (Monad m, StoreReadExtra m) => Word64 -> m [Transaction]-getNumTransaction i = map toTransaction <$> getNumTxData i+getNumTransaction i =+  getCtx >>= \ctx ->+    map (toTransaction ctx) <$> getNumTxData i  blockAtOrAfter ::   (MonadIO m, StoreReadExtra m) =>@@ -249,10 +233,10 @@ blockAtOrAfter ch q = runMaybeT $ do   net <- lift getNetwork   x <- MaybeT $ liftIO $ runReaderT (firstGreaterOrEqual net f) ch-  MaybeT $ getBlock (headerHash (nodeHeader x))+  MaybeT $ getBlock $ headerHash x.header   where     f x =-      let t = fromIntegral (blockTimestamp (nodeHeader x))+      let t = fromIntegral x.header.timestamp        in return $ t `compare` q  blockAtOrBefore ::@@ -263,10 +247,10 @@ blockAtOrBefore ch q = runMaybeT $ do   net <- lift getNetwork   x <- MaybeT $ liftIO $ runReaderT (lastSmallerOrEqual net f) ch-  MaybeT $ getBlock (headerHash (nodeHeader x))+  MaybeT $ getBlock $ headerHash x.header   where     f x =-      let t = fromIntegral (blockTimestamp (nodeHeader x))+      let t = fromIntegral x.header.timestamp        in return $ t `compare` q  blockAtOrAfterMTP ::@@ -277,7 +261,7 @@ blockAtOrAfterMTP ch q = runMaybeT $ do   net <- lift getNetwork   x <- MaybeT $ liftIO $ runReaderT (firstGreaterOrEqual net f) ch-  MaybeT $ getBlock (headerHash (nodeHeader x))+  MaybeT $ getBlock $ headerHash x.header   where     f x = do       t <- fromIntegral <$> mtp x@@ -330,41 +314,37 @@ applyLimit l = take (fromIntegral l)  deOffset :: Limits -> Limits-deOffset l = case limit l of+deOffset l = case l.limit of   0 -> l {offset = 0}-  _ -> l {limit = limit l + offset l, offset = 0}+  _ -> l {limit = l.limit + l.offset, offset = 0} -applyLimitsC :: Monad m => Limits -> ConduitT i i m ()+applyLimitsC :: (Monad m) => Limits -> ConduitT i i m () applyLimitsC Limits {..} = applyOffsetC offset >> applyLimitC limit -applyOffsetC :: Monad m => Offset -> ConduitT i i m ()+applyOffsetC :: (Monad m) => Offset -> ConduitT i i m () applyOffsetC = dropC . fromIntegral -applyLimitC :: Monad m => Limit -> ConduitT i i m ()+applyLimitC :: (Monad m) => Limit -> ConduitT i i m () applyLimitC 0 = mapC id applyLimitC l = takeC (fromIntegral l)  sortTxs :: [Tx] -> [(Word32, Tx)]-sortTxs txs = ts+sortTxs txs = go [] thset $ zip [0 ..] txs   where-    hm :: M.HashMap TxHash Int-    hm = M.fromList $ zip (map txHash txs) [0 ..]-    ns :: [(Tx, Int, [Int])]-    ns =-      let is tx = nub' $ map (outPointHash . prevOutput) (txIn tx)-          hs tx = mapMaybe (`M.lookup` hm) (is tx)-          tp i tx = (tx, i, hs tx)-       in zipWith tp [0 ..] txs-    (g, n, _) = G.graphFromEdges ns-    vs = G.reverseTopSort g-    ts =-      let f (tx, i, _) = (fromIntegral i, tx)-       in map (f . n) vs+    thset = H.fromList (map txHash txs)+    go [] _ [] = []+    go orphans ths [] = go [] ths orphans+    go orphans ths ((i, tx) : xs) =+      let ops = map (.outpoint.hash) tx.inputs+          orp = any (`H.member` ths) ops+       in if orp+            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' = H.toList . H.fromList -microseconds :: MonadIO m => m Integer+microseconds :: (MonadIO m) => m Integer microseconds =   let f t =         toInteger (systemSeconds t) * 1000000@@ -372,7 +352,7 @@    in liftIO $ f <$> getSystemTime  streamThings ::-  Monad m =>+  (Monad m) =>   (Limits -> m [a]) ->   Maybe (a -> TxHash) ->   Limits ->@@ -384,9 +364,9 @@   where     h l x = case gettx of       Just g -> Just l {offset = 1, start = Just (AtTx (g x))}-      Nothing -> case limit l of+      Nothing -> case l.limit of         0 -> Nothing-        _ -> Just l {offset = offset l + limit l}+        _ -> Just l {offset = l.offset + l.limit}     go l x = case h l x of       Nothing -> return ()       Just l' ->@@ -433,7 +413,7 @@     dataXPubTxCount :: !Counter   } -createDataMetrics :: MonadIO m => Metrics.Store -> m DataMetrics+createDataMetrics :: (MonadIO m) => Metrics.Store -> m DataMetrics createDataMetrics s = liftIO $ do   dataBestCount <- Metrics.createCounter "data.best_block" s   dataBlockCount <- Metrics.createCounter "data.blocks" s
src/Haskoin/Store/Database/Reader.hs view
@@ -1,8 +1,11 @@+{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE NoFieldSelectors #-}  module Haskoin.Store.Database.Reader   ( -- * RocksDB Database Access@@ -34,10 +37,10 @@ import Control.Monad.Reader (ReaderT, ask, asks, runReaderT) import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT) import Data.Bits ((.&.))-import qualified Data.ByteString as BS+import Data.ByteString qualified as BS import Data.Default (def) import Data.Function (on)-import qualified Data.IntMap.Strict as IntMap+import Data.IntMap.Strict qualified as IntMap import Data.List (sortOn) import Data.Maybe (fromMaybe) import Data.Ord (Down (..))@@ -63,6 +66,7 @@   ( Address,     BlockHash,     BlockHeight,+    Ctx,     Network,     OutPoint (..),     TxHash,@@ -72,28 +76,29 @@ import Haskoin.Store.Common import Haskoin.Store.Data import Haskoin.Store.Database.Types-import qualified System.Metrics as Metrics+import System.Metrics qualified as Metrics import System.Metrics.Counter (Counter)-import qualified System.Metrics.Counter as Counter+import System.Metrics.Counter qualified as Counter import UnliftIO (MonadIO, MonadUnliftIO, liftIO)  type DatabaseReaderT = ReaderT DatabaseReader  data DatabaseReader = DatabaseReader-  { databaseHandle :: !DB,-    databaseMaxGap :: !Word32,-    databaseInitialGap :: !Word32,-    databaseNetwork :: !Network,-    databaseMetrics :: !(Maybe DataMetrics)+  { db :: !DB,+    maxGap :: !Word32,+    initGap :: !Word32,+    net :: !Network,+    metrics :: !(Maybe DataMetrics),+    ctx :: !Ctx   }  incrementCounter ::-  MonadIO m =>+  (MonadIO m) =>   (DataMetrics -> Counter) ->   Int ->   ReaderT DatabaseReader m () incrementCounter f i =-  asks databaseMetrics >>= \case+  asks (.metrics) >>= \case     Just s -> liftIO $ Counter.add (f s) (fromIntegral i)     Nothing -> return () @@ -101,23 +106,25 @@ dataVersion = 18  withDatabaseReader ::-  MonadUnliftIO m =>+  (MonadUnliftIO m) =>   Network ->+  Ctx ->   Word32 ->   Word32 ->   FilePath ->   Maybe DataMetrics ->   DatabaseReaderT m a ->   m a-withDatabaseReader net igap gap dir stats f =+withDatabaseReader net ctx igap gap dir stats f =   withDBCF dir cfg columnFamilyConfig $ \db -> do     let bdb =           DatabaseReader-            { databaseHandle = db,-              databaseMaxGap = gap,-              databaseNetwork = net,-              databaseInitialGap = igap,-              databaseMetrics = stats+            { db = db,+              maxGap = gap,+              net = net,+              initGap = igap,+              metrics = stats,+              ctx = ctx             }     initRocksDB bdb     runReaderT f bdb@@ -157,8 +164,8 @@ balanceCF :: DB -> ColumnFamily balanceCF db = columnFamilies db !! 7 -initRocksDB :: MonadIO m => DatabaseReader -> m ()-initRocksDB DatabaseReader {databaseHandle = db} = do+initRocksDB :: (MonadIO m) => DatabaseReader -> m ()+initRocksDB DatabaseReader {db = db} = do   e <-     runExceptT $       retrieve db VersionKey >>= \case@@ -170,11 +177,11 @@     Left s -> error s     Right () -> return () -setInitRocksDB :: MonadIO m => DB -> m ()+setInitRocksDB :: (MonadIO m) => DB -> m () setInitRocksDB db = insert db VersionKey dataVersion  addressConduit ::-  MonadUnliftIO m =>+  (MonadUnliftIO m) =>   Address ->   Maybe Start ->   Iterator ->@@ -194,9 +201,9 @@           (AddrTxKeyB a (BlockRef bh maxBound))       Just (AtTx txh) ->         lift (getTxData txh) >>= \case-          Just TxData {txDataBlock = b@BlockRef {}} ->+          Just TxData {block = b@BlockRef {}} ->             matchingSkip it (AddrTxKeyA a) (AddrTxKeyB a b)-          Just TxData {txDataBlock = MemRef {}} ->+          Just TxData {block = MemRef {}} ->             let cond (AddrTxKey _a (TxRef MemRef {} th)) =                   th /= txh                 cond (AddrTxKey _a (TxRef BlockRef {} _th)) =@@ -206,13 +213,14 @@           Nothing -> return ()  unspentConduit ::-  MonadUnliftIO m =>+  (MonadUnliftIO m) =>+  Ctx ->   Address ->   Maybe Start ->   Iterator ->   ConduitT i Unspent (DatabaseReaderT m) ()-unspentConduit a s it =-  x .| mapC (uncurry toUnspent)+unspentConduit ctx a s it =+  x .| mapC (uncurry (toUnspent ctx))   where     x = case s of       Nothing ->@@ -224,11 +232,11 @@           (AddrOutKeyB a (BlockRef h maxBound))       Just (AtTx txh) ->         lift (getTxData txh) >>= \case-          Just TxData {txDataBlock = b@BlockRef {}} ->+          Just TxData {block = b@BlockRef {}} ->             matchingSkip it (AddrOutKeyA a) (AddrOutKeyB a b)-          Just TxData {txDataBlock = MemRef {}} ->+          Just TxData {block = MemRef {}} ->             let cond (AddrOutKey _a MemRef {} p) =-                  outPointHash p /= txh+                  p.hash /= txh                 cond (AddrOutKey _a BlockRef {} _p) =                   False              in matching it (AddrOutKeyA a)@@ -236,7 +244,7 @@           Nothing -> return ()  withManyIters ::-  MonadUnliftIO m =>+  (MonadUnliftIO m) =>   DB ->   ColumnFamily ->   Int ->@@ -255,117 +263,121 @@ joinConduits cs l =   runConduit $ joinDescStreams cs .| applyLimitsC l .| sinkList -instance MonadIO m => StoreReadBase (DatabaseReaderT m) where-  getNetwork = asks databaseNetwork+instance (MonadIO m) => StoreReadBase (DatabaseReaderT m) where+  getCtx = asks (.ctx)+  getNetwork = asks (.net)    getTxData th = do-    db <- asks databaseHandle+    db <- asks (.db)     retrieveCF db (txCF db) (TxKey th) >>= \case       Nothing -> return Nothing       Just t -> do-        incrementCounter dataTxCount 1+        incrementCounter (.dataTxCount) 1         return (Just t)    getSpender op = runMaybeT $ do-    td <- MaybeT $ getTxData (outPointHash op)-    let i = fromIntegral (outPointIndex op)-    MaybeT . return $ i `IntMap.lookup` txDataSpenders td+    td <- MaybeT $ getTxData op.hash+    let i = fromIntegral op.index+    MaybeT . return $ i `IntMap.lookup` td.spenders    getUnspent p = do-    db <- asks databaseHandle+    db <- asks (.db)+    ctx <- asks (.ctx)     val <- retrieveCF db (unspentCF db) (UnspentKey p)-    case fmap (valToUnspent p) val of+    case fmap (valToUnspent ctx p) val of       Nothing -> return Nothing       Just u -> do-        incrementCounter dataUnspentCount 1+        incrementCounter (.dataUnspentCount) 1         return (Just u)    getBalance a = do-    db <- asks databaseHandle-    incrementCounter dataBalanceCount 1+    db <- asks (.db)+    incrementCounter (.dataBalanceCount) 1     fmap (valToBalance a) <$> retrieveCF db (balanceCF db) (BalKey a)    getMempool = do-    db <- asks databaseHandle-    incrementCounter dataMempoolCount 1+    db <- asks (.db)+    incrementCounter (.dataMempoolCount) 1     fromMaybe [] <$> retrieve db MemKey    getBestBlock = do-    incrementCounter dataBestCount 1-    asks databaseHandle >>= (`retrieve` BestKey)+    incrementCounter (.dataBestCount) 1+    asks (.db) >>= (`retrieve` BestKey)    getBlocksAtHeight h = do-    db <- asks databaseHandle+    db <- asks (.db)     retrieveCF db (heightCF db) (HeightKey h) >>= \case       Nothing -> return []       Just ls -> do-        incrementCounter dataBlockCount (length ls)+        incrementCounter (.dataBlockCount) (length ls)         return ls    getBlock h = do-    db <- asks databaseHandle+    db <- asks (.db)     retrieveCF db (blockCF db) (BlockKey h) >>= \case       Nothing -> return Nothing       Just b -> do-        incrementCounter dataBlockCount 1+        incrementCounter (.dataBlockCount) 1         return (Just b) -instance MonadUnliftIO m => StoreReadExtra (DatabaseReaderT m) where+instance (MonadUnliftIO m) => StoreReadExtra (DatabaseReaderT m) where   getAddressesTxs addrs limits = do-    db <- asks databaseHandle+    db <- asks (.db)     withManyIters db (addrTxCF db) (length addrs) $ \its -> do       txs <- joinConduits (cs its) limits-      incrementCounter dataAddrTxCount (length txs)+      incrementCounter (.dataAddrTxCount) (length txs)       return txs     where-      cs = map (uncurry c) . zip addrs-      c a = addressConduit a (start limits)+      cs = zipWith c addrs+      c a = addressConduit a limits.start    getAddressesUnspents addrs limits = do-    db <- asks databaseHandle+    db <- asks (.db)+    ctx <- asks (.ctx)     withManyIters db (addrOutCF db) (length addrs) $ \its -> do-      uns <- joinConduits (cs its) limits-      incrementCounter dataUnspentCount (length uns)+      uns <- joinConduits (cs ctx its) limits+      incrementCounter (.dataUnspentCount) (length uns)       return uns     where-      cs = map (uncurry c) . zip addrs-      c a = unspentConduit a (start limits)+      cs ctx = zipWith (c ctx) addrs+      c ctx a = unspentConduit ctx a limits.start    getAddressUnspents a limits = do-    db <- asks databaseHandle+    db <- asks (.db)+    ctx <- asks (.ctx)     us <- withIterCF db (addrOutCF db) $ \it ->       runConduit $-        unspentConduit a (start limits) it+        unspentConduit ctx a limits.start it           .| applyLimitsC limits           .| sinkList-    incrementCounter dataUnspentCount (length us)+    incrementCounter (.dataUnspentCount) (length us)     return us    getAddressTxs a limits = do-    db <- asks databaseHandle+    db <- asks (.db)     txs <- withIterCF db (addrTxCF db) $ \it ->       runConduit $-        addressConduit a (start limits) it+        addressConduit a limits.start it           .| applyLimitsC limits           .| sinkList-    incrementCounter dataAddrTxCount (length txs)+    incrementCounter (.dataAddrTxCount) (length txs)     return txs -  getMaxGap = asks databaseMaxGap+  getMaxGap = asks (.maxGap) -  getInitialGap = asks databaseInitialGap+  getInitialGap = asks (.initGap)    getNumTxData i = do-    db <- asks databaseHandle+    db <- asks (.db)     let (sk, w) = decodeTxKey i     ls <- liftIO $ matchingAsListCF db (txCF db) (TxKeyS sk)     let f t =-          let bs = encode $ txHash (txData t)+          let bs = encode $ txHash t.tx               b = BS.head (BS.drop 6 bs)               w' = b .&. 0xf8            in w == w'         txs = filter f $ map snd ls-    incrementCounter dataTxCount (length txs)+    incrementCounter (.dataTxCount) (length txs)     return txs    getBalances as = do@@ -375,25 +387,26 @@       f _ (Just b) = b    xPubBals xpub = do+    ctx <- asks (.ctx)     igap <- getInitialGap     gap <- getMaxGap-    ext1 <- derive_until_gap gap 0 (take (fromIntegral igap) (aderiv 0 0))-    if all (nullBalance . xPubBal) ext1+    ext1 <- derive_until_gap gap 0 (take (fromIntegral igap) (aderiv ctx 0 0))+    if all (nullBalance . (.balance)) ext1       then do-        incrementCounter dataXPubBals (length ext1)+        incrementCounter (.dataXPubBals) (length ext1)         return ext1       else do-        ext2 <- derive_until_gap gap 0 (aderiv 0 igap)-        chg <- derive_until_gap gap 1 (aderiv 1 0)+        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)+        incrementCounter (.dataXPubBals) (length bals)         return bals     where-      aderiv m =+      aderiv ctx m =         deriveAddresses-          (deriveFunction (xPubDeriveType xpub))-          (pubSubKey (xPubSpecKey xpub) m)-      xbalance m b n = XPubBal {xPubBalPath = [m, n], xPubBal = b}+          (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@@ -405,26 +418,24 @@    xPubUnspents _xspec xbals limits = do     us <- concat <$> mapM h cs-    incrementCounter dataXPubUnspents (length us)+    incrementCounter (.dataXPubUnspents) (length us)     return . applyLimits limits $ sortOn Down us     where       l = deOffset limits-      cs = filter ((> 0) . balanceUnspentCount . xPubBal) xbals-      i b = do-        us <- getAddressUnspents (balanceAddress (xPubBal b)) l-        return us-      f b t = XPubUnspent {xPubUnspentPath = xPubBalPath b, xPubUnspent = t}+      cs = filter ((> 0) . (.balance.utxo)) xbals+      i b = getAddressUnspents b.balance.address l+      f b t = XPubUnspent {path = b.path, unspent = t}       h b = map (f b) <$> i b    xPubTxs _xspec xbals limits = do     let as =-          map balanceAddress $+          map (.address) $             filter (not . nullBalance) $-              map xPubBal xbals+              map (.balance) xbals     txs <- getAddressesTxs as limits-    incrementCounter dataXPubTxs (length txs)+    incrementCounter (.dataXPubTxs) (length txs)     return txs    xPubTxCount xspec xbals = do-    incrementCounter dataXPubTxCount 1+    incrementCounter (.dataXPubTxCount) 1     fromIntegral . length <$> xPubTxs xspec xbals def
src/Haskoin/Store/Database/Types.hs view
@@ -1,7 +1,11 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE NoFieldSelectors #-}  module Haskoin.Store.Database.Types   ( AddrTxKey (..),@@ -37,7 +41,7 @@     (.|.),   ) import Data.ByteString (ByteString)-import qualified Data.ByteString as BS+import Data.ByteString qualified as BS import Data.Default (Default (..)) import Data.Either (fromRight) import Data.Hashable (Hashable)@@ -67,6 +71,7 @@     eitherToMaybe,     scriptToAddressBS,   )+import Haskoin.Crypto import Haskoin.Store.Data   ( Balance (..),     BlockData,@@ -82,14 +87,16 @@ data AddrTxKey   = -- | key for a transaction affecting an address     AddrTxKey-      { addrTxKeyA :: !Address,-        addrTxKeyT :: !TxRef+      { address :: !Address,+        tx :: !TxRef       }   | -- | short key that matches all entries-    AddrTxKeyA {addrTxKeyA :: !Address}+    AddrTxKeyA+      { address :: !Address+      }   | AddrTxKeyB-      { addrTxKeyA :: !Address,-        addrTxKeyB :: !BlockRef+      { address :: !Address,+        block :: !BlockRef       }   | AddrTxKeyS   deriving (Show, Eq, Ord, Generic, Hashable)@@ -99,18 +106,18 @@    put     AddrTxKey-      { addrTxKeyA = a,-        addrTxKeyT = TxRef {txRefBlock = b, txRefHash = t}+      { address = a,+        tx = TxRef {block = b, txid = t}       } = do-      put AddrTxKeyB {addrTxKeyA = a, addrTxKeyB = b}+      put AddrTxKeyB {address = a, block = b}       put t   -- 0x05 · Address-  put AddrTxKeyA {addrTxKeyA = a} = do+  put AddrTxKeyA {address = a} = do     put AddrTxKeyS     put a   -- 0x05 · Address · BlockRef-  put AddrTxKeyB {addrTxKeyA = a, addrTxKeyB = b} = do-    put AddrTxKeyA {addrTxKeyA = a}+  put AddrTxKeyB {address = a, block = b} = do+    put AddrTxKeyA {address = a}     put b   -- 0x05   put AddrTxKeyS = putWord8 0x05@@ -121,8 +128,8 @@     t <- get     return       AddrTxKey-        { addrTxKeyA = a,-          addrTxKeyT = TxRef {txRefBlock = b, txRefHash = t}+        { address = a,+          tx = TxRef {block = b, txid = t}         }  instance Key AddrTxKey@@ -133,15 +140,17 @@ data AddrOutKey   = -- | full key     AddrOutKey-      { addrOutKeyA :: !Address,-        addrOutKeyB :: !BlockRef,-        addrOutKeyP :: !OutPoint+      { address :: !Address,+        block :: !BlockRef,+        outpoint :: !OutPoint       }   | -- | short key for all spent or unspent outputs-    AddrOutKeyA {addrOutKeyA :: !Address}+    AddrOutKeyA+      { address :: !Address+      }   | AddrOutKeyB-      { addrOutKeyA :: !Address,-        addrOutKeyB :: !BlockRef+      { address :: !Address,+        block :: !BlockRef       }   | AddrOutKeyS   deriving (Show, Read, Eq, Ord, Generic, Hashable)@@ -149,15 +158,15 @@ instance Serialize AddrOutKey where   -- 0x06 · StoreAddr · BlockRef · OutPoint -  put AddrOutKey {addrOutKeyA = a, addrOutKeyB = b, addrOutKeyP = p} = do-    put AddrOutKeyB {addrOutKeyA = a, addrOutKeyB = b}+  put AddrOutKey {address = a, block = b, outpoint = p} = do+    put AddrOutKeyB {address = a, block = b}     put p   -- 0x06 · StoreAddr · BlockRef-  put AddrOutKeyB {addrOutKeyA = a, addrOutKeyB = b} = do-    put AddrOutKeyA {addrOutKeyA = a}+  put AddrOutKeyB {address = a, block = b} = do+    put AddrOutKeyA {address = a}     put b   -- 0x06 · StoreAddr-  put AddrOutKeyA {addrOutKeyA = a} = do+  put AddrOutKeyA {address = a} = do     put AddrOutKeyS     put a   -- 0x06@@ -169,8 +178,8 @@ instance Key AddrOutKey  data OutVal = OutVal-  { outValAmount :: !Word64,-    outValScript :: !ByteString+  { value :: !Word64,+    script :: !ByteString   }   deriving (Show, Read, Eq, Ord, Generic, Hashable, Serialize) @@ -178,8 +187,8 @@  -- | Transaction database key. data TxKey-  = TxKey {txKey :: TxHash}-  | TxKeyS {txKeyShort :: (Word32, Word16)}+  = TxKey {txid :: TxHash}+  | TxKeyS {short :: (Word32, Word16)}   deriving (Show, Read, Eq, Ord, Generic, Hashable)  instance Serialize TxKey where@@ -213,19 +222,19 @@  -- | Unspent output database key. data UnspentKey-  = UnspentKey {unspentKey :: !OutPoint}-  | UnspentKeyS {unspentKeyS :: !TxHash}+  = UnspentKey {outpoint :: !OutPoint}+  | UnspentKeyS {txid :: !TxHash}   | UnspentKeyB   deriving (Show, Read, Eq, Ord, Generic, Hashable)  instance Serialize UnspentKey where   -- 0x09 · TxHash · Index-  put UnspentKey {unspentKey = OutPoint {outPointHash = h, outPointIndex = i}} = do+  put UnspentKey {outpoint = OutPoint {hash = h, index = i}} = do     putWord8 0x09     put h     put i   -- 0x09 · TxHash-  put UnspentKeyS {unspentKeyS = t} = do+  put UnspentKeyS {txid = t} = do     putWord8 0x09     put t   -- 0x09@@ -234,20 +243,20 @@     guard . (== 0x09) =<< getWord8     h <- get     i <- get-    return $ UnspentKey OutPoint {outPointHash = h, outPointIndex = i}+    return $ UnspentKey OutPoint {hash = h, index = i}  instance Key UnspentKey  instance KeyValue UnspentKey UnspentVal -toUnspent :: AddrOutKey -> OutVal -> Unspent-toUnspent b v =+toUnspent :: Ctx -> AddrOutKey -> OutVal -> Unspent+toUnspent ctx AddrOutKey {..} OutVal {..} =   Unspent-    { unspentBlock = addrOutKeyB b,-      unspentAmount = outValAmount v,-      unspentScript = outValScript v,-      unspentPoint = addrOutKeyP b,-      unspentAddress = eitherToMaybe (scriptToAddressBS (outValScript v))+    { block = block,+      value = value,+      script = script,+      outpoint = outpoint,+      address = eitherToMaybe (scriptToAddressBS ctx script)     }  -- | Mempool transaction database key.@@ -268,7 +277,7 @@  -- | Block entry database key. newtype BlockKey = BlockKey-  { blockKey :: BlockHash+  { hash :: BlockHash   }   deriving (Show, Read, Eq, Ord, Generic, Hashable) @@ -287,15 +296,15 @@  -- | Block height database key. newtype HeightKey = HeightKey-  { heightKey :: BlockHeight+  { height :: BlockHeight   }   deriving (Show, Read, Eq, Ord, Generic, Hashable)  instance Serialize HeightKey where   -- 0x03 · BlockHeight-  put (HeightKey height) = do+  put (HeightKey h) = do     putWord8 0x03-    put height+    put h   get = do     guard . (== 0x03) =<< getWord8     HeightKey <$> get@@ -307,14 +316,14 @@ -- | Address balance database key. data BalKey   = BalKey-      { balanceKey :: !Address+      { address :: !Address       }   | BalKeyS   deriving (Show, Read, Eq, Ord, Generic, Hashable)  instance Serialize BalKey where   -- 0x04 · Address-  put BalKey {balanceKey = a} = do+  put (BalKey a) = do     putWord8 0x04     put a   -- 0x04@@ -360,96 +369,45 @@ instance KeyValue VersionKey Word32  data BalVal = BalVal-  { balValAmount :: !Word64,-    balValZero :: !Word64,-    balValUnspentCount :: !Word64,-    balValTxCount :: !Word64,-    balValTotalReceived :: !Word64+  { confirmed :: !Word64,+    unconfirmed :: !Word64,+    utxo :: !Word64,+    txs :: !Word64,+    received :: !Word64   }   deriving (Show, Read, Eq, Ord, Generic, Hashable, Serialize, NFData)  valToBalance :: Address -> BalVal -> Balance-valToBalance-  a-  BalVal-    { balValAmount = v,-      balValZero = z,-      balValUnspentCount = u,-      balValTxCount = t,-      balValTotalReceived = r-    } =-    Balance-      { balanceAddress = a,-        balanceAmount = v,-        balanceZero = z,-        balanceUnspentCount = u,-        balanceTxCount = t,-        balanceTotalReceived = r-      }+valToBalance address BalVal {..} =+  Balance {..}  balanceToVal :: Balance -> BalVal-balanceToVal-  Balance-    { balanceAmount = v,-      balanceZero = z,-      balanceUnspentCount = u,-      balanceTxCount = t,-      balanceTotalReceived = r-    } =-    BalVal-      { balValAmount = v,-        balValZero = z,-        balValUnspentCount = u,-        balValTxCount = t,-        balValTotalReceived = r-      }+balanceToVal Balance {..} =+  BalVal {..}  -- | Default balance for an address. instance Default BalVal where   def =     BalVal-      { balValAmount = 0,-        balValZero = 0,-        balValUnspentCount = 0,-        balValTxCount = 0,-        balValTotalReceived = 0+      { confirmed = 0,+        unconfirmed = 0,+        utxo = 0,+        txs = 0,+        received = 0       }  data UnspentVal = UnspentVal-  { unspentValBlock :: !BlockRef,-    unspentValAmount :: !Word64,-    unspentValScript :: !ByteString+  { block :: !BlockRef,+    value :: !Word64,+    script :: !ByteString   }   deriving (Show, Read, Eq, Ord, Generic, Hashable, Serialize, NFData)  unspentToVal :: Unspent -> (OutPoint, UnspentVal)-unspentToVal-  Unspent-    { unspentBlock = b,-      unspentPoint = p,-      unspentAmount = v,-      unspentScript = s-    } =-    ( p,-      UnspentVal-        { unspentValBlock = b,-          unspentValAmount = v,-          unspentValScript = s-        }-    )+unspentToVal Unspent {..} = (outpoint, UnspentVal {..}) -valToUnspent :: OutPoint -> UnspentVal -> Unspent-valToUnspent-  p-  UnspentVal-    { unspentValBlock = b,-      unspentValAmount = v,-      unspentValScript = s-    } =-    Unspent-      { unspentBlock = b,-        unspentPoint = p,-        unspentAmount = v,-        unspentScript = s,-        unspentAddress = eitherToMaybe (scriptToAddressBS s)-      }+valToUnspent :: Ctx -> OutPoint -> UnspentVal -> Unspent+valToUnspent ctx outpoint UnspentVal {..} =+  Unspent {..}+  where+    address = eitherToMaybe (scriptToAddressBS ctx script)
src/Haskoin/Store/Database/Writer.hs view
@@ -1,18 +1,22 @@+{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE NoFieldSelectors #-}  module Haskoin.Store.Database.Writer (WriterT, runWriter) where  import Control.Monad (join) import Control.Monad.Reader (ReaderT (..))-import qualified Control.Monad.Reader as R+import Control.Monad.Reader qualified as R import Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as M-import qualified Data.HashTable.IO as H-import qualified Data.IntMap.Strict as IntMap+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) import Data.Ord (Down (..)) import Data.Tuple (swap)@@ -28,6 +32,7 @@   ( Address,     BlockHash,     BlockHeight,+    Ctx,     Network,     OutPoint (..),     TxHash,@@ -54,14 +59,15 @@   )  data Writer = Writer-  { getReader :: !DatabaseReader,-    getState :: !Memory+  { reader :: !DatabaseReader,+    memory :: !Memory   }  type WriterT = ReaderT Writer -instance MonadIO m => StoreReadBase (WriterT m) where-  getNetwork = getNetworkI+instance (MonadIO m) => StoreReadBase (WriterT m) where+  getNetwork = R.asks (.reader.net)+  getCtx = R.asks (.reader.ctx)   getBestBlock = getBestBlockI   getBlocksAtHeight = getBlocksAtHeightI   getBlock = getBlockI@@ -92,149 +98,152 @@ type MempoolTable = H.BasicHashTable TxHash UnixTime  data Memory = Memory-  { hNet :: !NetRef,-    hBest :: !BestRef,-    hBlock :: !BlockTable,-    hHeight :: !HeightTable,-    hTx :: !TxTable,-    hUnspent :: !UnspentTable,-    hBalance :: !BalanceTable,-    hAddrTx :: !AddrTxTable,-    hAddrOut :: !AddrOutTable,-    hMempool :: !MempoolTable+  { net :: !Network,+    ctx :: !Ctx,+    best :: !BestRef,+    blockTable :: !BlockTable,+    heightTable :: !HeightTable,+    txTable :: !TxTable,+    unspentTable :: !UnspentTable,+    balanceTable :: !BalanceTable,+    addressTable :: !AddrTxTable,+    outputTable :: !AddrOutTable,+    mempoolTable :: !MempoolTable   } -instance MonadIO m => StoreWrite (WriterT m) where+instance (MonadIO m) => StoreWrite (WriterT m) where   setBest h =-    ReaderT $ \Writer {getState = s} ->-      liftIO $ writeIORef (hBest s) (Just (Just h))+    ReaderT $ \Writer {memory = Memory {..}} ->+      liftIO $ writeIORef best (Just (Just h))   insertBlock b =-    ReaderT $ \Writer {getState = s} ->-      liftIO $ H.insert (hBlock s) (headerHash (blockDataHeader b)) (Just b)+    ReaderT $ \Writer {memory = Memory {..}} ->+      liftIO $ H.insert blockTable (headerHash b.header) (Just b)   setBlocksAtHeight h g =-    ReaderT $ \Writer {getState = s} ->-      liftIO $ H.insert (hHeight s) g h+    ReaderT $ \Writer {memory = Memory {..}} ->+      liftIO $ H.insert heightTable g h   insertTx t =-    ReaderT $ \Writer {getState = s} ->-      liftIO $ H.insert (hTx s) (txHash (txData t)) (Just t)+    ReaderT $ \Writer {memory = Memory {..}} ->+      liftIO $ H.insert txTable (txHash t.tx) (Just t)   insertAddrTx a t =-    ReaderT $ \Writer {getState = s} ->-      liftIO $ H.insert (hAddrTx s) (a, t) (Just ())+    ReaderT $ \Writer {memory = Memory {..}} ->+      liftIO $ H.insert addressTable (a, t) (Just ())   deleteAddrTx a t =-    ReaderT $ \Writer {getState = s} ->-      liftIO $ H.insert (hAddrTx s) (a, t) Nothing+    ReaderT $ \Writer {memory = Memory {..}} ->+      liftIO $ H.insert addressTable (a, t) Nothing   insertAddrUnspent a u =-    ReaderT $ \Writer {getState = s} ->-      liftIO $ H.insert (hAddrOut s) k (Just v)+    ReaderT $ \Writer {memory = Memory {..}} ->+      liftIO $ H.insert outputTable k (Just v)     where-      k = (a, unspentBlock u, unspentPoint u)-      v = OutVal {outValAmount = unspentAmount u, outValScript = unspentScript u}+      k = (a, u.block, u.outpoint)+      v = OutVal {value = u.value, script = u.script}   deleteAddrUnspent a u =-    ReaderT $ \Writer {getState = s} ->-      liftIO $ H.insert (hAddrOut s) k Nothing+    ReaderT $ \Writer {memory = Memory {..}} ->+      liftIO $ H.insert outputTable k Nothing     where-      k = (a, unspentBlock u, unspentPoint u)+      k = (a, u.block, u.outpoint)   addToMempool x t =-    ReaderT $ \Writer {getState = s} ->-      liftIO $ H.insert (hMempool s) x t+    ReaderT $ \Writer {memory = Memory {..}} ->+      liftIO $ H.insert mempoolTable x t   deleteFromMempool x =-    ReaderT $ \Writer {getState = s} ->-      liftIO $ H.delete (hMempool s) x+    ReaderT $ \Writer {memory = Memory {..}} ->+      liftIO $ H.delete mempoolTable x   setBalance b =-    ReaderT $ \Writer {getState = s} ->-      liftIO $ H.insert (hBalance s) (balanceAddress b) (Just b)+    ReaderT $ \Writer {memory = Memory {..}} ->+      liftIO $ H.insert balanceTable b.address (Just b)   insertUnspent u =-    ReaderT $ \Writer {getState = s} ->-      liftIO $ H.insert (hUnspent s) (fst (unspentToVal u)) (Just u)+    ReaderT $ \Writer {memory = Memory {..}} ->+      liftIO $ H.insert unspentTable (fst (unspentToVal u)) (Just u)   deleteUnspent p =-    ReaderT $ \Writer {getState = s} ->-      liftIO $ H.insert (hUnspent s) p Nothing+    ReaderT $ \Writer {memory = Memory {..}} ->+      liftIO $ H.insert unspentTable p Nothing  getLayered ::-  MonadIO m =>+  (MonadIO m) =>   (Memory -> IO (Maybe a)) ->   DatabaseReaderT m a ->   WriterT m a getLayered f g =-  ReaderT $ \Writer {getReader = db, getState = s} ->+  ReaderT $ \Writer {reader = db, memory = s} ->     liftIO (f s) >>= \case       Just x -> return x       Nothing -> runReaderT g db  runWriter ::-  MonadIO m =>+  (MonadIO m) =>+  Network ->+  Ctx ->   DatabaseReader ->   WriterT m a ->   m a-runWriter bdb@DatabaseReader {databaseHandle = db} f = do+runWriter net ctx bdb@DatabaseReader {db} f = do   mempool <- runReaderT getMempool bdb-  hm <- newMemory mempool-  x <- R.runReaderT f Writer {getReader = bdb, getState = hm}+  hm <- newMemory net ctx mempool+  x <- R.runReaderT f Writer {reader = bdb, memory = hm}   ops <- hashMapOps db hm   writeBatch db ops   return x -hashMapOps :: MonadIO m => DB -> Memory -> m [BatchOp]-hashMapOps db mem =+hashMapOps :: (MonadIO m) => DB -> Memory -> m [BatchOp]+hashMapOps db Memory {..} =   mconcat     <$> sequence-      [ bestBlockOp (hBest mem),-        blockHashOps db (hBlock mem),-        blockHeightOps db (hHeight mem),-        txOps db (hTx mem),-        balOps db (hBalance mem),-        addrTxOps db (hAddrTx mem),-        addrOutOps db (hAddrOut mem),-        mempoolOp (hMempool mem),-        unspentOps db (hUnspent mem)+      [ bestBlockOp best,+        blockHashOps db blockTable,+        blockHeightOps db heightTable,+        txOps db txTable,+        balOps db balanceTable,+        addrTxOps db addressTable,+        addrOutOps db outputTable,+        mempoolOp mempoolTable,+        unspentOps db unspentTable       ] -bestBlockOp :: MonadIO m => BestRef -> m [BatchOp]+bestBlockOp :: (MonadIO m) => BestRef -> m [BatchOp] bestBlockOp r =   readIORef r >>= \case     Nothing -> return []     Just Nothing -> return [deleteOp BestKey]     Just (Just b) -> return [insertOp BestKey b] -blockHashOps :: MonadIO m => DB -> BlockTable -> m [BatchOp]+blockHashOps :: (MonadIO m) => DB -> BlockTable -> m [BatchOp] blockHashOps db t = map (uncurry f) <$> liftIO (H.toList t)   where     f k (Just d) = insertOpCF (blockCF db) (BlockKey k) d     f k Nothing = deleteOpCF (blockCF db) (BlockKey k) -blockHeightOps :: MonadIO m => DB -> HeightTable -> m [BatchOp]+blockHeightOps :: (MonadIO m) => DB -> HeightTable -> m [BatchOp] blockHeightOps db t = map (uncurry f) <$> liftIO (H.toList t)   where     f = insertOpCF (heightCF db) . HeightKey -txOps :: MonadIO m => DB -> TxTable -> m [BatchOp]+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 Nothing = deleteOpCF (txCF db) (TxKey k) -balOps :: MonadIO m => DB -> BalanceTable -> m [BatchOp]+balOps :: (MonadIO m) => DB -> BalanceTable -> m [BatchOp] balOps db t = map (uncurry f) <$> liftIO (H.toList t)   where     f a (Just b) = insertOpCF (balanceCF db) (BalKey a) (balanceToVal b)     f a Nothing = deleteOpCF (balanceCF db) (BalKey a) -addrTxOps :: MonadIO m => DB -> AddrTxTable -> m [BatchOp]+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) -addrOutOps :: MonadIO m => DB -> AddrOutTable -> m [BatchOp]+addrOutOps :: (MonadIO m) => DB -> AddrOutTable -> m [BatchOp] addrOutOps db t = map (uncurry f) <$> liftIO (H.toList t)   where     f (a, b, p) (Just l) =       insertOpCF         (addrOutCF db)         ( AddrOutKey-            { addrOutKeyA = a,-              addrOutKeyB = b,-              addrOutKeyP = p+            { address = a,+              block = b,+              outpoint = p             }         )         l@@ -242,16 +251,16 @@       deleteOpCF         (addrOutCF db)         AddrOutKey-          { addrOutKeyA = a,-            addrOutKeyB = b,-            addrOutKeyP = p+          { address = a,+            block = b,+            outpoint = p           } -mempoolOp :: MonadIO m => MempoolTable -> m [BatchOp]+mempoolOp :: (MonadIO m) => MempoolTable -> m [BatchOp] mempoolOp t =   return . insertOp MemKey . sortOn Down . map swap <$> liftIO (H.toList t) -unspentOps :: MonadIO m => DB -> UnspentTable -> m [BatchOp]+unspentOps :: (MonadIO m) => DB -> UnspentTable -> m [BatchOp] unspentOps db t = map (uncurry f) <$> liftIO (H.toList t)   where     f p (Just u) =@@ -259,73 +268,80 @@     f p Nothing =       deleteOpCF (unspentCF db) (UnspentKey p) -getNetworkI :: MonadIO m => WriterT m Network-getNetworkI = getLayered (liftIO . readIORef . hNet) getNetwork--getBestBlockI :: MonadIO m => WriterT m (Maybe BlockHash)+getBestBlockI :: (MonadIO m) => WriterT m (Maybe BlockHash) getBestBlockI = getLayered getBestBlockH getBestBlock -getBlocksAtHeightI :: MonadIO m => BlockHeight -> WriterT m [BlockHash]+getBlocksAtHeightI :: (MonadIO m) => BlockHeight -> WriterT m [BlockHash] getBlocksAtHeightI bh =   getLayered (getBlocksAtHeightH bh) (getBlocksAtHeight bh) -getBlockI :: MonadIO m => BlockHash -> WriterT m (Maybe BlockData)+getBlockI :: (MonadIO m) => BlockHash -> WriterT m (Maybe BlockData) getBlockI bh = getLayered (getBlockH bh) (getBlock bh) -getTxDataI :: MonadIO m => TxHash -> WriterT m (Maybe TxData)+getTxDataI :: (MonadIO m) => TxHash -> WriterT m (Maybe TxData) getTxDataI th = getLayered (getTxDataH th) (getTxData th) -getSpenderI :: MonadIO m => OutPoint -> WriterT m (Maybe Spender)+getSpenderI :: (MonadIO m) => OutPoint -> WriterT m (Maybe Spender) getSpenderI op = getLayered (getSpenderH op) (getSpender op) -getBalanceI :: MonadIO m => Address -> WriterT m (Maybe Balance)+getBalanceI :: (MonadIO m) => Address -> WriterT m (Maybe Balance) getBalanceI a = getLayered (getBalanceH a) (getBalance a) -getUnspentI :: MonadIO m => OutPoint -> WriterT m (Maybe Unspent)+getUnspentI :: (MonadIO m) => OutPoint -> WriterT m (Maybe Unspent) getUnspentI op = getLayered (getUnspentH op) (getUnspent op) -getMempoolI :: MonadIO m => WriterT m [(UnixTime, TxHash)]+getMempoolI :: (MonadIO m) => WriterT m [(UnixTime, TxHash)] getMempoolI =-  ReaderT $ \Writer {getState = s} ->-    liftIO $ map swap <$> H.toList (hMempool s)+  ReaderT $ \Writer {memory = Memory {..}} ->+    liftIO $ map swap <$> H.toList mempoolTable -newMemory :: MonadIO m => [(UnixTime, TxHash)] -> m Memory-newMemory mempool = do-  hNet <- newIORef Nothing-  hBest <- newIORef Nothing-  hBlock <- liftIO H.new-  hHeight <- liftIO H.new-  hTx <- liftIO H.new-  hUnspent <- liftIO H.new-  hBalance <- liftIO H.new-  hAddrTx <- liftIO H.new-  hAddrOut <- liftIO H.new-  hMempool <- liftIO $ H.fromList (map swap mempool)+newMemory ::+  (MonadIO m) =>+  Network ->+  Ctx ->+  [(UnixTime, TxHash)] ->+  m Memory+newMemory net ctx mempool = do+  best <- newIORef Nothing+  blockTable <- liftIO H.new+  heightTable <- liftIO H.new+  txTable <- liftIO H.new+  unspentTable <- liftIO H.new+  balanceTable <- liftIO H.new+  addressTable <- liftIO H.new+  outputTable <- liftIO H.new+  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 . hBest+getBestBlockH = readIORef . (.best)  getBlocksAtHeightH :: BlockHeight -> Memory -> IO (Maybe [BlockHash])-getBlocksAtHeightH h s = H.lookup (hHeight s) h+getBlocksAtHeightH h s = H.lookup s.heightTable h  getBlockH :: BlockHash -> Memory -> IO (Maybe (Maybe BlockData))-getBlockH h s = H.lookup (hBlock s) h+getBlockH h s = H.lookup s.blockTable h  getTxDataH :: TxHash -> Memory -> IO (Maybe (Maybe TxData))-getTxDataH t s = H.lookup (hTx s) t+getTxDataH t s = H.lookup s.txTable t  getSpenderH :: OutPoint -> Memory -> IO (Maybe (Maybe Spender)) getSpenderH op s = do-  fmap (join . fmap f) <$> getTxDataH (outPointHash op) s+  fmap (f =<<) <$> getTxDataH op.hash s   where-    f = IntMap.lookup (fromIntegral (outPointIndex op)) . txDataSpenders+    f = IntMap.lookup (fromIntegral op.index) . (.spenders)  getBalanceH :: Address -> Memory -> IO (Maybe (Maybe Balance))-getBalanceH a s = H.lookup (hBalance s) a+getBalanceH a s = H.lookup s.balanceTable a  getMempoolH :: Memory -> IO [(UnixTime, TxHash)]-getMempoolH s = sortOn Down . map swap <$> H.toList (hMempool s)+getMempoolH Memory {..} = sortOn Down . map swap <$> H.toList mempoolTable  getUnspentH :: OutPoint -> Memory -> IO (Maybe (Maybe Unspent))-getUnspentH p s = H.lookup (hUnspent s) p+getUnspentH p Memory {..} = H.lookup unspentTable p
src/Haskoin/Store/Logic.hs view
@@ -1,10 +1,15 @@ {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-}+{-# LANGUAGE NoFieldSelectors #-}+{-# OPTIONS_GHC -Wno-ambiguous-fields #-}  module Haskoin.Store.Logic   ( ImportException (..),@@ -54,6 +59,7 @@     BlockHash,     BlockHeader (..),     BlockNode (..),+    Ctx,     Network (..),     OutPoint (..),     Tx (..),@@ -74,19 +80,6 @@   ) import Haskoin.Store.Common import Haskoin.Store.Data-  ( Balance (..),-    BlockData (..),-    BlockRef (..),-    Prev (..),-    Spender (..),-    TxData (..),-    TxRef (..),-    UnixTime,-    Unspent (..),-    confirmed,-    isCoinbaseTx,-    txDataFee,-  ) import UnliftIO (Exception)  type MonadImport m =@@ -123,16 +116,17 @@   show DuplicatePrevOutput = "Duplicate previous output"   show TxSpent = "Transaction is spent" -initBest :: MonadImport m => m ()+initBest :: (MonadImport m) => m () initBest = do+  ctx <- getCtx   $(logDebugS) "BlockStore" "Initializing best block"   net <- getNetwork   m <- getBestBlock   when (isNothing m) . void $ do     $(logDebugS) "BlockStore" "Importing Genesis block"-    importBlock (genesisBlock net) (genesisNode net)+    importBlock (genesisBlock net ctx) (genesisNode net) -newMempoolTx :: MonadImport m => Tx -> UnixTime -> m Bool+newMempoolTx :: (MonadImport m) => Tx -> UnixTime -> m Bool newMempoolTx tx w =   getActiveTxData (txHash tx) >>= \case     Just _ ->@@ -144,7 +138,7 @@       importTx (MemRef w) w rbf tx       return True -bestBlockData :: MonadImport m => m BlockData+bestBlockData :: (MonadImport m) => m BlockData bestBlockData = do   h <-     getBestBlock >>= \case@@ -158,11 +152,11 @@       throwError BestBlockNotFound     Just b -> return b -revertBlock :: MonadImport m => BlockHash -> m ()+revertBlock :: (MonadImport m) => BlockHash -> m () revertBlock bh = do   bd <-     bestBlockData >>= \b ->-      if headerHash (blockDataHeader b) == bh+      if headerHash b.header == bh         then return b         else do           $(logErrorS) "BlockStore" $@@ -170,49 +164,49 @@           throwError BlockNotBest   $(logDebugS) "BlockStore" $     "Obtained block data for " <> blockHashToHex bh-  tds <- mapM getImportTxData (blockDataTxs bd)+  tds <- mapM getImportTxData bd.txs   $(logDebugS) "BlockStore" $     "Obtained import tx data for block " <> blockHashToHex bh-  setBest (prevBlock (blockDataHeader bd))+  setBest bd.header.prev   $(logDebugS) "BlockStore" $     "Set parent as best block "-      <> blockHashToHex (prevBlock (blockDataHeader bd))-  insertBlock bd {blockDataMainChain = False}+      <> blockHashToHex bd.header.prev+  insertBlock bd {main = False}   $(logDebugS) "BlockStore" $     "Updated as not in main chain: " <> blockHashToHex bh   forM_ (tail tds) unConfirmTx   $(logDebugS) "BlockStore" $     "Unconfirmed " <> cs (show (length tds)) <> " transactions"-  deleteConfirmedTx (txHash (txData (head tds)))+  deleteConfirmedTx (txHash (head tds).tx)   $(logDebugS) "BlockStore" $-    "Deleted coinbase: " <> txHashToHex (txHash (txData (head tds)))+    "Deleted coinbase: " <> txHashToHex (txHash (head tds).tx) -checkNewBlock :: MonadImport m => Block -> BlockNode -> m ()+checkNewBlock :: (MonadImport m) => Block -> BlockNode -> m () checkNewBlock b n =   getBestBlock >>= \case     Nothing       | isGenesis n -> return ()       | otherwise -> do-        $(logErrorS) "BlockStore" $-          "Cannot import non-genesis block: "-            <> blockHashToHex (headerHash (blockHeader b))-        throwError BestBlockNotFound+          $(logErrorS) "BlockStore" $+            "Cannot import non-genesis block: "+              <> blockHashToHex (headerHash b.header)+          throwError BestBlockNotFound     Just h-      | prevBlock (blockHeader b) == h -> return ()+      | b.header.prev == h -> return ()       | otherwise -> do-        $(logErrorS) "BlockStore" $-          "Block does not build on head: "-            <> blockHashToHex (headerHash (blockHeader b))-        throwError PrevBlockNotBest+          $(logErrorS) "BlockStore" $+            "Block does not build on head: "+              <> blockHashToHex (headerHash b.header)+          throwError PrevBlockNotBest -importOrConfirm :: MonadImport m => BlockNode -> [Tx] -> m [TxData]+importOrConfirm :: (MonadImport m) => BlockNode -> [Tx] -> m [TxData] importOrConfirm bn txns = do   mapM_ (freeOutputs True False . snd) (reverse txs)   mapM (uncurry action) txs   where     txs = sortTxs txns-    br i = BlockRef {blockRefHeight = nodeHeight bn, blockRefPos = i}-    bn_time = fromIntegral . blockTimestamp $ nodeHeader bn+    br i = BlockRef {height = bn.height, position = i}+    bn_time = fromIntegral $ bn.header.timestamp     action i tx =       testPresent tx >>= \case         False -> import_it i tx@@ -234,57 +228,57 @@         "Importing tx: " <> txHashToHex (txHash tx)       importTx (br i) bn_time False tx -importBlock :: MonadImport m => Block -> BlockNode -> m (BlockData, [TxData])+importBlock :: (MonadImport m) => Block -> BlockNode -> m (BlockData, [TxData]) importBlock b n = do   $(logDebugS) "BlockStore" $     "Checking new block: "-      <> blockHashToHex (headerHash (nodeHeader n))+      <> blockHashToHex (headerHash n.header)   checkNewBlock b n   $(logDebugS) "BlockStore" "Passed check"   net <- getNetwork-  let subsidy = computeSubsidy net (nodeHeight n)-  bs <- getBlocksAtHeight (nodeHeight n)+  let subsidy = computeSubsidy net n.height+  bs <- getBlocksAtHeight n.height   $(logDebugS) "BlockStore" $     "Inserting block entries for: "-      <> blockHashToHex (headerHash (nodeHeader n))+      <> blockHashToHex (headerHash n.header)   setBlocksAtHeight-    (nub (headerHash (nodeHeader n) : bs))-    (nodeHeight n)-  setBest (headerHash (nodeHeader n))-  tds <- importOrConfirm n (blockTxns b)+    (nub (headerHash n.header : bs))+    n.height+  setBest (headerHash n.header)+  tds <- importOrConfirm n b.txs   let bd =         BlockData-          { blockDataHeight = nodeHeight n,-            blockDataMainChain = True,-            blockDataWork = nodeWork n,-            blockDataHeader = nodeHeader n,-            blockDataSize = fromIntegral (B.length (encode b)),-            blockDataTxs = map txHash (blockTxns b),-            blockDataWeight = if getSegWit net then w else 0,-            blockDataSubsidy = subsidy,-            blockDataFees = sum $ map txDataFee tds,-            blockDataOutputs = ts_out_val+          { height = n.height,+            main = True,+            work = n.work,+            header = n.header,+            size = fromIntegral (B.length (encode b)),+            txs = map txHash b.txs,+            weight = if net.segWit then w else 0,+            subsidy = subsidy,+            fee = sum $ map txDataFee tds,+            outputs = ts_out_val           }   insertBlock bd   $(logDebugS) "BlockStore" $     "Finished importing block: "-      <> blockHashToHex (headerHash (nodeHeader n))+      <> blockHashToHex (headerHash n.header)   return (bd, tds)   where     cb_out_val =-      sum $ map outValue $ txOut $ head $ blockTxns b+      sum $ map (.value) $ (head b.txs).outputs     ts_out_val =-      sum $ map (sum . map outValue . txOut) $ tail $ blockTxns b+      sum $ map (sum . map (.value) . (.outputs)) $ tail $ b.txs     w =-      let f t = t {txWitness = []}-          b' = b {blockTxns = map f (blockTxns b)}+      let f t = (t :: Tx) {witness = []}+          b' = (b :: Block) {txs = map f b.txs}           x = B.length (encode b)           s = B.length (encode b')        in fromIntegral $ s * 3 + x -checkNewTx :: MonadImport m => Tx -> m ()+checkNewTx :: (MonadImport m) => Tx -> m () checkNewTx tx = do-  when (unique_inputs < length (txIn tx)) $ do+  when (unique_inputs < length tx.inputs) $ do     $(logErrorS) "BlockStore" $       "Transaction spends same output twice: "         <> txHashToHex (txHash tx)@@ -308,30 +302,30 @@       "Insufficient funds for tx: " <> txHashToHex (txHash tx)     throwError InsufficientFunds   where-    unspents = sum . map unspentAmount . catMaybes-    outputs = sum (map outValue (txOut tx))-    unique_inputs = length (nub' (map prevOutput (txIn tx)))+    unspents = sum . map (.value) . catMaybes+    outputs = sum (map (.value) tx.outputs)+    unique_inputs = length (nub' (map (.outpoint) tx.inputs)) -getUnspentOutputs :: StoreReadBase m => Tx -> m [Maybe Unspent]+getUnspentOutputs :: (StoreReadBase m) => Tx -> m [Maybe Unspent] getUnspentOutputs tx = mapM getUnspent (prevOuts tx)  prepareTxData :: Bool -> BlockRef -> Word64 -> Tx -> [Unspent] -> TxData prepareTxData rbf br tt tx us =   TxData-    { txDataBlock = br,-      txData = tx,-      txDataPrevs = ps,-      txDataDeleted = False,-      txDataRBF = rbf,-      txDataTime = tt,-      txDataSpenders = I.empty+    { block = br,+      tx = tx,+      prevs = ps,+      deleted = False,+      rbf = rbf,+      timestamp = tt,+      spenders = I.empty     }   where-    mkprv u = Prev (unspentScript u) (unspentAmount u)+    mkprv Unspent {..} = Prev script value     ps = I.fromList $ zip [0 ..] $ map mkprv us  importTx ::-  MonadImport m =>+  (MonadImport m) =>   BlockRef ->   -- | unix time   Word64 ->@@ -352,94 +346,98 @@   commitAddTx td   return td -unConfirmTx :: MonadImport m => TxData -> m TxData+unConfirmTx :: (MonadImport m) => TxData -> m TxData unConfirmTx t = confTx t Nothing -confirmTx :: MonadImport m => TxData -> BlockRef -> m TxData+confirmTx :: (MonadImport m) => TxData -> BlockRef -> m TxData confirmTx t br = confTx t (Just br) -replaceAddressTx :: MonadImport m => TxData -> BlockRef -> m ()-replaceAddressTx t new = forM_ (txDataAddresses t) $ \a -> do-  deleteAddrTx-    a-    TxRef-      { txRefBlock = txDataBlock t,-        txRefHash = txHash (txData t)-      }-  insertAddrTx-    a-    TxRef-      { txRefBlock = new,-        txRefHash = txHash (txData t)-      }+replaceAddressTx :: (MonadImport m) => TxData -> BlockRef -> m ()+replaceAddressTx t new = do+  ctx <- getCtx+  forM_ (txDataAddresses ctx t) $ \a -> do+    deleteAddrTx+      a+      TxRef+        { block = t.block,+          txid = txHash t.tx+        }+    insertAddrTx+      a+      TxRef+        { block = new,+          txid = txHash t.tx+        }  adjustAddressOutput ::-  MonadImport m =>+  (MonadImport m) =>   OutPoint ->   TxOut ->   BlockRef ->   BlockRef ->   m () adjustAddressOutput op o old new = do-  let pk = scriptOutput o+  let pk = o.script   getUnspent op >>= \case     Nothing -> return ()     Just u -> do-      unless (unspentBlock u == old) $-        error $ "Existing unspent block bad for output: " <> show op+      unless (u.block == old) $+        error $+          "Existing unspent block bad for output: " <> show op       replace_unspent pk   where     replace_unspent pk = do-      let ma = eitherToMaybe (scriptToAddressBS pk)+      ctx <- getCtx+      let ma = eitherToMaybe (scriptToAddressBS ctx pk)       deleteUnspent op       insertUnspent         Unspent-          { unspentBlock = new,-            unspentPoint = op,-            unspentAmount = outValue o,-            unspentScript = pk,-            unspentAddress = ma+          { block = new,+            outpoint = op,+            value = o.value,+            script = pk,+            address = ma           }       forM_ ma $ replace_addr_unspent pk     replace_addr_unspent pk a = do       deleteAddrUnspent         a         Unspent-          { unspentBlock = old,-            unspentPoint = op,-            unspentAmount = outValue o,-            unspentScript = pk,-            unspentAddress = Just a+          { block = old,+            outpoint = op,+            value = o.value,+            script = pk,+            address = Just a           }       insertAddrUnspent         a         Unspent-          { unspentBlock = new,-            unspentPoint = op,-            unspentAmount = outValue o,-            unspentScript = pk,-            unspentAddress = Just a+          { block = new,+            outpoint = op,+            value = o.value,+            script = pk,+            address = Just a           }-      decreaseBalance (confirmed old) a (outValue o)-      increaseBalance (confirmed new) a (outValue o)+      decreaseBalance (confirmed old) a o.value+      increaseBalance (confirmed new) a o.value -confTx :: MonadImport m => TxData -> Maybe BlockRef -> m TxData+confTx :: (MonadImport m) => TxData -> Maybe BlockRef -> m TxData confTx t mbr = do   replaceAddressTx t new-  forM_ (zip [0 ..] (txOut (txData t))) $ \(n, o) -> do-    let op = OutPoint (txHash (txData t)) n+  forM_ (zip [0 ..] t.tx.outputs) $ \(n, o) -> do+    let op = OutPoint (txHash t.tx) n     adjustAddressOutput op o old new-  rbf <- isRBF new (txData t)-  let td = t {txDataBlock = new, txDataRBF = rbf}+  rbf <- isRBF new t.tx+  let td = (t :: TxData) {block = new, rbf = rbf}   insertTx td   updateMempool td   return td   where-    new = fromMaybe (MemRef (txDataTime t)) mbr-    old = txDataBlock t+    new = fromMaybe (MemRef t.timestamp) mbr+    old = t.block  freeOutputs ::-  MonadImport m =>+  (MonadImport m) =>   -- | only delete transaction if unconfirmed   Bool ->   -- | only delete RBF@@ -451,13 +449,13 @@   unspents <- mapM getUnspent prevs   let spents = [p | (p, Nothing) <- zip prevs unspents]   spndrs <- catMaybes <$> mapM getSpender spents-  let txids = HashSet.fromList $ filter (/= txHash tx) $ map spenderHash spndrs+  let txids = HashSet.fromList $ filter (/= txHash tx) $ map (.txid) spndrs   mapM_ (deleteTx memonly rbfcheck) $ HashSet.toList txids -deleteConfirmedTx :: MonadImport m => TxHash -> m ()+deleteConfirmedTx :: (MonadImport m) => TxHash -> m () deleteConfirmedTx = deleteTx False False -deleteUnconfirmedTx :: MonadImport m => Bool -> TxHash -> m ()+deleteUnconfirmedTx :: (MonadImport m) => Bool -> TxHash -> m () deleteUnconfirmedTx rbfcheck th =   getActiveTxData th >>= \case     Just _ -> deleteTx True rbfcheck th@@ -466,7 +464,7 @@         "Not found or already deleted: " <> txHashToHex th  deleteTx ::-  MonadImport m =>+  (MonadImport m) =>   -- | only delete transaction if unconfirmed   Bool ->   -- | only delete RBF@@ -476,7 +474,8 @@ deleteTx memonly rbfcheck th = do   chain <- getChain memonly rbfcheck th   $(logDebugS) "BlockStore" $-    "Deleting " <> cs (show (length chain))+    "Deleting "+      <> cs (show (length chain))       <> " txs from chain leading to "       <> txHashToHex th   mapM_ (\t -> let h = txHash t in deleteSingleTx h >> return h) chain@@ -502,34 +501,34 @@             "Transaction not found: " <> txHashToHex th           return Nothing         Just td-          | memonly && confirmed (txDataBlock td) -> do-            $(logErrorS) "BlockStore" $-              "Transaction already confirmed: "-                <> txHashToHex th-            throwError TxConfirmed+          | memonly && confirmed td.block -> do+              $(logErrorS) "BlockStore" $+                "Transaction already confirmed: "+                  <> txHashToHex th+              throwError TxConfirmed           | rbfcheck ->-            isRBF (txDataBlock td) (txData td) >>= \case-              True -> return $ Just td-              False -> do-                $(logErrorS) "BlockStore" $-                  "Double-spending transaction: "-                    <> txHashToHex th-                throwError DoubleSpend+              isRBF td.block td.tx >>= \case+                True -> return $ Just td+                False -> do+                  $(logErrorS) "BlockStore" $+                    "Double-spending transaction: "+                      <> txHashToHex th+                  throwError DoubleSpend           | otherwise -> return $ Just td     go txs pdg = do       tds <- catMaybes <$> mapM get_tx (HashSet.toList pdg)-      let txsn = HashSet.fromList $ fmap txData tds+      let txsn = HashSet.fromList $ fmap (.tx) tds           pdgn =             HashSet.fromList-              . concatMap (map spenderHash . I.elems)-              $ fmap txDataSpenders tds+              . concatMap (map (.txid) . I.elems)+              $ fmap (.spenders) tds           txs' = txsn <> txs           pdg' = pdgn `HashSet.difference` HashSet.map txHash txs'       if HashSet.null pdg'         then return $ HashSet.toList txs'         else go txs' pdg' -deleteSingleTx :: MonadImport m => TxHash -> m ()+deleteSingleTx :: (MonadImport m) => TxHash -> m () deleteSingleTx th =   getActiveTxData th >>= \case     Nothing -> do@@ -537,7 +536,7 @@         "Already deleted: " <> txHashToHex th       throwError TxNotFound     Just td ->-      if I.null (txDataSpenders td)+      if I.null td.spenders         then do           $(logDebugS) "BlockStore" $             "Deleting tx: " <> txHashToHex th@@ -548,31 +547,32 @@               <> txHashToHex th           throwError TxSpent -commitDelTx :: MonadImport m => TxData -> m ()+commitDelTx :: (MonadImport m) => TxData -> m () commitDelTx = commitModTx False -commitAddTx :: MonadImport m => TxData -> m ()+commitAddTx :: (MonadImport m) => TxData -> m () commitAddTx = commitModTx True -commitModTx :: MonadImport m => Bool -> TxData -> m ()+commitModTx :: (MonadImport m) => Bool -> TxData -> m () commitModTx add tx_data = do-  mapM_ mod_addr_tx (txDataAddresses td)+  ctx <- getCtx+  mapM_ mod_addr_tx (txDataAddresses ctx td)   mod_outputs   mod_unspent   insertTx td   updateMempool td   where-    tx = txData td-    br = txDataBlock td-    td = tx_data {txDataDeleted = not add}+    tx = td.tx+    br = td.block+    td = (tx_data :: TxData) {deleted = not add}     tx_ref = TxRef br (txHash tx)     mod_addr_tx a       | add = do-        insertAddrTx a tx_ref-        modAddressCount add a+          insertAddrTx a tx_ref+          modAddressCount add a       | otherwise = do-        deleteAddrTx a tx_ref-        modAddressCount add a+          deleteAddrTx a tx_ref+          modAddressCount add a     mod_unspent       | add = spendOutputs tx       | otherwise = unspendOutputs tx@@ -580,90 +580,97 @@       | add = addOutputs br tx       | otherwise = delOutputs br tx -updateMempool :: MonadImport m => TxData -> m ()-updateMempool td@TxData {txDataDeleted = True} =-  deleteFromMempool (txHash (txData td))-updateMempool td@TxData {txDataBlock = MemRef t} =-  addToMempool (txHash (txData td)) t-updateMempool td@TxData {txDataBlock = BlockRef {}} =-  deleteFromMempool (txHash (txData td))+updateMempool :: (MonadImport m) => TxData -> m ()+updateMempool td@TxData {deleted = True} =+  deleteFromMempool (txHash td.tx)+updateMempool td@TxData {block = MemRef t} =+  addToMempool (txHash td.tx) t+updateMempool td@TxData {block = BlockRef {}} =+  deleteFromMempool (txHash td.tx) -spendOutputs :: MonadImport m => Tx -> m ()+spendOutputs :: (MonadImport m) => Tx -> m () spendOutputs tx =   zipWithM_ (spendOutput (txHash tx)) [0 ..] (prevOuts tx) -addOutputs :: MonadImport m => BlockRef -> Tx -> m ()+addOutputs :: (MonadImport m) => BlockRef -> Tx -> m () addOutputs br tx =-  zipWithM_ (addOutput br . OutPoint (txHash tx)) [0 ..] (txOut tx)+  zipWithM_ (addOutput br . OutPoint (txHash tx)) [0 ..] tx.outputs  isRBF ::-  StoreReadBase m =>+  (StoreReadBase m) =>   BlockRef ->   Tx ->   m Bool isRBF br tx   | confirmed br = return False   | otherwise =-    getNetwork >>= \net ->-      if getReplaceByFee net-        then go-        else return False+      getNetwork >>= \net ->+        if net.replaceByFee+          then go+          else return False   where     go-      | any ((< 0xffffffff - 1) . txInSequence) (txIn tx) = return True+      | any ((< 0xffffffff - 1) . (.sequence)) tx.inputs = return True       | otherwise = carry_on     carry_on =-      let hs = nub' $ map (outPointHash . prevOutput) (txIn tx)+      let hs = nub' $ map (.outpoint.hash) tx.inputs           ck [] = return False           ck (h : hs') =             getActiveTxData h >>= \case               Nothing -> return False               Just t-                | confirmed (txDataBlock t) -> ck hs'-                | txDataRBF t -> return True+                | confirmed t.block -> ck hs'+                | t.rbf -> return True                 | otherwise -> ck hs'        in ck hs -addOutput :: MonadImport m => BlockRef -> OutPoint -> TxOut -> m ()+addOutput :: (MonadImport m) => BlockRef -> OutPoint -> TxOut -> m () addOutput = modOutput True -delOutput :: MonadImport m => BlockRef -> OutPoint -> TxOut -> m ()+delOutput :: (MonadImport m) => BlockRef -> OutPoint -> TxOut -> m () delOutput = modOutput False -modOutput :: MonadImport m => Bool -> BlockRef -> OutPoint -> TxOut -> m ()+modOutput ::+  (MonadImport m) =>+  Bool ->+  BlockRef ->+  OutPoint ->+  TxOut ->+  m () modOutput add br op o = do-  mod_unspent-  forM_ ma $ \a -> do-    mod_addr_unspent a u-    modBalance (confirmed br) add a (outValue o)+  ctx <- getCtx+  mod_unspent ctx+  forM_ (ma ctx) $ \a -> do+    mod_addr_unspent a (u ctx)+    modBalance (confirmed br) add a o.value     modifyReceived a v   where     v-      | add = (+ outValue o)-      | otherwise = subtract (outValue o)-    ma = eitherToMaybe (scriptToAddressBS (scriptOutput o))-    u =+      | add = (+ o.value)+      | otherwise = subtract o.value+    ma ctx = eitherToMaybe (scriptToAddressBS ctx o.script)+    u ctx =       Unspent-        { unspentScript = scriptOutput o,-          unspentBlock = br,-          unspentPoint = op,-          unspentAmount = outValue o,-          unspentAddress = ma+        { script = o.script,+          block = br,+          outpoint = op,+          value = o.value,+          address = ma ctx         }-    mod_unspent-      | add = insertUnspent u+    mod_unspent ctx+      | add = insertUnspent (u ctx)       | otherwise = deleteUnspent op     mod_addr_unspent       | add = insertAddrUnspent       | otherwise = deleteAddrUnspent -delOutputs :: MonadImport m => BlockRef -> Tx -> m ()+delOutputs :: (MonadImport m) => BlockRef -> Tx -> m () delOutputs br tx =-  forM_ (zip [0 ..] (txOut tx)) $ \(i, o) -> do+  forM_ (zip [0 ..] tx.outputs) $ \(i, o) -> do     let op = OutPoint (txHash tx) i     delOutput br op o -getImportTxData :: MonadImport m => TxHash -> m TxData+getImportTxData :: (MonadImport m) => TxHash -> m TxData getImportTxData th =   getActiveTxData th >>= \case     Nothing -> do@@ -673,26 +680,26 @@  getTxOut :: Word32 -> Tx -> Maybe TxOut getTxOut i tx = do-  guard (fromIntegral i < length (txOut tx))-  return $ txOut tx !! fromIntegral i+  guard (fromIntegral i < length tx.outputs)+  return $ tx.outputs !! fromIntegral i -insertSpender :: MonadImport m => OutPoint -> Spender -> m ()+insertSpender :: (MonadImport m) => OutPoint -> Spender -> m () insertSpender op s = do-  td <- getImportTxData (outPointHash op)-  let p = txDataSpenders td-      p' = I.insert (fromIntegral (outPointIndex op)) s p-      td' = td {txDataSpenders = p'}+  td <- getImportTxData op.hash+  let p = td.spenders+      p' = I.insert (fromIntegral op.index) s p+      td' = (td :: TxData) {spenders = p'}   insertTx td' -deleteSpender :: MonadImport m => OutPoint -> m ()+deleteSpender :: (MonadImport m) => OutPoint -> m () deleteSpender op = do-  td <- getImportTxData (outPointHash op)-  let p = txDataSpenders td-      p' = I.delete (fromIntegral (outPointIndex op)) p-      td' = td {txDataSpenders = p'}+  td <- getImportTxData op.hash+  let p = td.spenders+      p' = I.delete (fromIntegral op.index) p+      td' = (td :: TxData) {spenders = p'}   insertTx td' -spendOutput :: MonadImport m => TxHash -> Word32 -> OutPoint -> m ()+spendOutput :: (MonadImport m) => TxHash -> Word32 -> OutPoint -> m () spendOutput th ix op = do   u <-     getUnspent op >>= \case@@ -700,55 +707,54 @@       Nothing -> error $ "Could not find UTXO to spend: " <> show op   deleteUnspent op   insertSpender op (Spender th ix)-  let pk = unspentScript u-  forM_ (scriptToAddressBS pk) $ \a -> do-    decreaseBalance-      (confirmed (unspentBlock u))-      a-      (unspentAmount u)+  let pk = u.script+  ctx <- getCtx+  forM_ (scriptToAddressBS ctx pk) $ \a -> do+    decreaseBalance (confirmed u.block) a u.value     deleteAddrUnspent a u -unspendOutputs :: MonadImport m => Tx -> m ()+unspendOutputs :: (MonadImport m) => Tx -> m () unspendOutputs = mapM_ unspendOutput . prevOuts -unspendOutput :: MonadImport m => OutPoint -> m ()+unspendOutput :: (MonadImport m) => OutPoint -> m () unspendOutput op = do+  ctx <- getCtx   t <--    getActiveTxData (outPointHash op) >>= \case-      Nothing -> error $ "Could not find tx data: " <> show (outPointHash op)+    getActiveTxData op.hash >>= \case+      Nothing -> error $ "Could not find tx data: " <> show op.hash       Just t -> return t   let o =         fromMaybe           (error ("Could not find output: " <> show op))-          (getTxOut (outPointIndex op) (txData t))-      m = eitherToMaybe (scriptToAddressBS (scriptOutput o))+          (getTxOut op.index t.tx)+      m = eitherToMaybe (scriptToAddressBS ctx o.script)       u =         Unspent-          { unspentAmount = outValue o,-            unspentBlock = txDataBlock t,-            unspentScript = scriptOutput o,-            unspentPoint = op,-            unspentAddress = m+          { value = o.value,+            block = t.block,+            script = o.script,+            outpoint = op,+            address = m           }   deleteSpender op   insertUnspent u   forM_ m $ \a -> do     insertAddrUnspent a u-    increaseBalance (confirmed (unspentBlock u)) a (outValue o)+    increaseBalance (confirmed u.block) a o.value -modifyReceived :: MonadImport m => Address -> (Word64 -> Word64) -> m ()+modifyReceived :: (MonadImport m) => Address -> (Word64 -> Word64) -> m () modifyReceived a f = do   b <- getDefaultBalance a-  setBalance b {balanceTotalReceived = f (balanceTotalReceived b)}+  setBalance b {received = f b.received} -decreaseBalance :: MonadImport m => Bool -> Address -> Word64 -> m ()+decreaseBalance :: (MonadImport m) => Bool -> Address -> Word64 -> m () decreaseBalance conf = modBalance conf False -increaseBalance :: MonadImport m => Bool -> Address -> Word64 -> m ()+increaseBalance :: (MonadImport m) => Bool -> Address -> Word64 -> m () increaseBalance conf = modBalance conf True  modBalance ::-  MonadImport m =>+  (MonadImport m) =>   -- | confirmed   Bool ->   -- | add@@ -760,38 +766,38 @@   b <- getDefaultBalance a   setBalance $ (g . f) b   where-    g b = b {balanceUnspentCount = m 1 (balanceUnspentCount b)}+    g b = (b :: Balance) {utxo = m 1 b.utxo}     f b-      | conf = b {balanceAmount = m val (balanceAmount b)}-      | otherwise = b {balanceZero = m val (balanceZero b)}+      | conf = (b :: Balance) {confirmed = m val b.confirmed}+      | otherwise = (b :: Balance) {unconfirmed = m val b.unconfirmed}     m       | add = (+)       | otherwise = subtract -modAddressCount :: MonadImport m => Bool -> Address -> m ()+modAddressCount :: (MonadImport m) => Bool -> Address -> m () modAddressCount add a = do   b <- getDefaultBalance a-  setBalance b {balanceTxCount = f (balanceTxCount b)}+  setBalance b {txs = f b.txs}   where     f       | add = (+ 1)       | otherwise = subtract 1 -txOutAddrs :: [TxOut] -> [Address]-txOutAddrs = nub' . rights . map (scriptToAddressBS . scriptOutput)+txOutAddrs :: Ctx -> [TxOut] -> [Address]+txOutAddrs ctx = nub' . rights . map (scriptToAddressBS ctx . (.script)) -txInAddrs :: [Prev] -> [Address]-txInAddrs = nub' . rights . map (scriptToAddressBS . prevScript)+txInAddrs :: Ctx -> [Prev] -> [Address]+txInAddrs ctx = nub' . rights . map (scriptToAddressBS ctx . (.script)) -txDataAddresses :: TxData -> [Address]-txDataAddresses t =-  nub' $ txInAddrs prevs <> txOutAddrs outs+txDataAddresses :: Ctx -> TxData -> [Address]+txDataAddresses ctx t =+  nub' $ txInAddrs ctx prevs <> txOutAddrs ctx outs   where-    prevs = I.elems (txDataPrevs t)-    outs = txOut (txData t)+    prevs = I.elems t.prevs+    outs = t.tx.outputs  prevOuts :: Tx -> [OutPoint]-prevOuts tx = filter (/= nullOutPoint) (map prevOutput (txIn tx))+prevOuts tx = filter (/= nullOutPoint) (map (.outpoint) tx.inputs) -testPresent :: StoreReadBase m => Tx -> m Bool+testPresent :: (StoreReadBase m) => Tx -> m Bool testPresent tx = isJust <$> getActiveTxData (txHash tx)
src/Haskoin/Store/Manager.hs view
@@ -1,5 +1,9 @@+{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE ImportQualifiedPost #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE NoFieldSelectors #-}  module Haskoin.Store.Manager   ( StoreConfig (..),@@ -9,6 +13,14 @@ where  import Control.Monad (forever, unless, when)+import Control.Monad.Cont+  ( ContT (..),+    MonadCont (callCC),+    cont,+    lift,+    runCont,+    runContT,+  ) import Control.Monad.Logger (MonadLoggerIO) import Control.Monad.Reader (ReaderT (ReaderT), runReaderT) import Data.Serialize (decode)@@ -16,6 +28,7 @@ import Data.Word (Word32) import Haskoin   ( BlockHash (..),+    Ctx,     Inv (..),     InvType (..),     InvVector (..),@@ -37,7 +50,7 @@     NodeConfig (..),     NodeEvent (..),     PeerEvent (..),-    PeerManager,+    PeerMgr,     WithConnection,     withNode,   )@@ -96,53 +109,56 @@  -- | Store mailboxes. data Store = Store-  { storeManager :: !PeerManager,-    storeChain :: !Chain,-    storeBlock :: !BlockStore,-    storeDB :: !DatabaseReader,-    storeCache :: !(Maybe CacheConfig),-    storePublisher :: !(Publisher StoreEvent),-    storeNetwork :: !Network+  { peerMgr :: !PeerMgr,+    chain :: !Chain,+    block :: !BlockStore,+    db :: !DatabaseReader,+    cache :: !(Maybe CacheConfig),+    pub :: !(Publisher StoreEvent),+    net :: !Network,+    ctx :: !Ctx   }  -- | Configuration for a 'Store'. data StoreConfig = StoreConfig   { -- | max peers to connect to-    storeConfMaxPeers :: !Int,+    maxPeers :: !Int,     -- | static set of peers to connect to-    storeConfInitPeers :: ![String],+    initPeers :: ![String],     -- | discover new peers-    storeConfDiscover :: !Bool,+    discover :: !Bool,     -- | RocksDB database path-    storeConfDB :: !FilePath,+    db :: !FilePath,     -- | network constants-    storeConfNetwork :: !Network,+    net :: !Network,     -- | Redis cache configuration-    storeConfCache :: !(Maybe String),+    redis :: !(Maybe String),+    -- | Secp256k1 context+    ctx :: !Ctx,     -- | gap on extended public key with no transactions-    storeConfInitialGap :: !Word32,+    initGap :: !Word32,     -- | gap for extended public keys-    storeConfGap :: !Word32,+    gap :: !Word32,     -- | cache xpubs with more than this many used addresses-    storeConfCacheMin :: !Int,+    redisMinAddrs :: !Int,     -- | maximum number of keys in Redis cache-    storeConfMaxKeys :: !Integer,+    redisMaxKeys :: !Integer,     -- | do not index new mempool transactions-    storeConfNoMempool :: !Bool,+    noMempool :: !Bool,     -- | wipe mempool when starting-    storeConfWipeMempool :: !Bool,+    wipeMempool :: !Bool,     -- | sync mempool from peers-    storeConfSyncMempool :: !Bool,+    syncMempool :: !Bool,     -- | disconnect peer if message not received for this many seconds-    storeConfPeerTimeout :: !NominalDiffTime,+    peerTimeout :: !NominalDiffTime,     -- | disconnect peer if it has been connected this long-    storeConfPeerMaxLife :: !NominalDiffTime,+    maxPeerLife :: !NominalDiffTime,     -- | connect to peers using the function 'withConnection'-    storeConfConnect :: !(SockAddr -> WithConnection),+    connect :: !(SockAddr -> WithConnection),     -- | stats store-    storeConfStats :: !(Maybe Metrics.Store),+    statsStore :: !(Maybe Metrics.Store),     -- | sync mempool against cache every this many seconds-    storeConfCacheMempoolSync :: !Int+    redisSyncInterval :: !Int   }  withStore ::@@ -152,34 +168,41 @@   m a withStore cfg action =   connectDB cfg $-    ReaderT $ \db ->-      withPublisher $ \pub ->-        withPublisher $ \node_pub ->-          withSubscription node_pub $ \node_sub ->-            withNode (nodeCfg cfg db node_pub) $ \node ->-              withCache cfg (nodeChain node) db pub $ \mcache ->-                withBlockStore (blockStoreCfg cfg node pub db) $ \b ->-                  withAsync (nodeForwarder b pub node_sub) $ \a1 ->-                    link a1-                      >> action-                        Store-                          { storeManager = nodeManager node,-                            storeChain = nodeChain node,-                            storeBlock = b,-                            storeDB = db,-                            storeCache = mcache,-                            storePublisher = pub,-                            storeNetwork = storeConfNetwork cfg-                          }+    ReaderT $ \db -> flip runContT return $ do+      pub <- ContT withPublisher+      node_pub <- ContT withPublisher+      node_sub <- ContT $ withSubscription node_pub+      node <- ContT $ withNode $ nodeCfg cfg db node_pub+      cache_cfg <- ContT $ withCache cfg node.chain db pub+      block_store <- ContT $ withBlockStore $ blockStoreCfg cfg node pub db+      fwd <- ContT $ withAsync $ nodeForwarder block_store pub node_sub+      link fwd+      lift $+        action+          Store+            { peerMgr = node.peerMgr,+              chain = node.chain,+              block = block_store,+              db = db,+              cache = cache_cfg,+              pub = pub,+              net = cfg.net,+              ctx = cfg.ctx+            } -connectDB :: MonadUnliftIO m => StoreConfig -> DatabaseReaderT m a -> m a+connectDB ::+  (MonadUnliftIO m) =>+  StoreConfig ->+  DatabaseReaderT m a ->+  m a connectDB cfg f = do-  stats <- mapM createDataMetrics (storeConfStats cfg)+  stats <- mapM createDataMetrics cfg.statsStore   withDatabaseReader-    (storeConfNetwork cfg)-    (storeConfInitialGap cfg)-    (storeConfGap cfg)-    (storeConfDB cfg)+    cfg.net+    cfg.ctx+    cfg.initGap+    cfg.gap+    cfg.db     stats     f @@ -191,16 +214,17 @@   BlockStoreConfig blockStoreCfg cfg node pub db =   BlockStoreConfig-    { blockConfChain = nodeChain node,-      blockConfManager = nodeManager node,-      blockConfListener = pub,-      blockConfDB = db,-      blockConfNet = storeConfNetwork cfg,-      blockConfNoMempool = storeConfNoMempool cfg,-      blockConfWipeMempool = storeConfWipeMempool cfg,-      blockConfSyncMempool = storeConfSyncMempool cfg,-      blockConfPeerTimeout = storeConfPeerTimeout cfg,-      blockConfStats = storeConfStats cfg+    { chain = node.chain,+      peerMgr = node.peerMgr,+      pub = pub,+      db = db,+      net = cfg.net,+      ctx = cfg.ctx,+      noMempool = cfg.noMempool,+      wipeMempool = cfg.wipeMempool,+      syncMempool = cfg.syncMempool,+      peerTimeout = cfg.peerTimeout,+      statsStore = cfg.statsStore     }  nodeCfg ::@@ -210,20 +234,20 @@   NodeConfig nodeCfg cfg db pub =   NodeConfig-    { nodeConfMaxPeers = storeConfMaxPeers cfg,-      nodeConfDB = databaseHandle db,-      nodeConfColumnFamily = Nothing,-      nodeConfPeers = storeConfInitPeers cfg,-      nodeConfDiscover = storeConfDiscover cfg,-      nodeConfEvents = pub,-      nodeConfNetAddr =+    { maxPeers = cfg.maxPeers,+      db = db.db,+      cf = Nothing,+      peers = cfg.initPeers,+      discover = cfg.discover,+      pub = pub,+      address =         NetworkAddress           0           (sockToHostAddress (SockAddrInet 0 0)),-      nodeConfNet = storeConfNetwork cfg,-      nodeConfTimeout = storeConfPeerTimeout cfg,-      nodeConfPeerMaxLife = storeConfPeerMaxLife cfg,-      nodeConfConnect = storeConfConnect cfg+      net = cfg.net,+      timeout = cfg.peerTimeout,+      maxPeerLife = cfg.maxPeerLife,+      connect = cfg.connect     }  withCache ::@@ -235,31 +259,33 @@   (Maybe CacheConfig -> m a) ->   m a withCache cfg chain db pub action =-  case storeConfCache cfg of+  case cfg.redis of     Nothing ->       action Nothing-    Just redisurl ->-      mapM newCacheMetrics (storeConfStats cfg) >>= \metrics ->-        connectRedis redisurl >>= \conn ->-          withSubscription pub $ \evts ->-            let conf = c conn metrics-             in withProcess (f conf) $ \p ->-                  cacheWriterProcesses interval evts (getProcessMailbox p) $ do-                    action (Just conf)+    Just redisurl -> do+      metrics <- mapM newCacheMetrics cfg.statsStore+      conn <- connectRedis redisurl+      withSubscription pub $ \evts ->+        let conf = c conn metrics+         in withProcess (f conf) $ \p ->+              cacheWriterProcesses+                cfg.redisSyncInterval+                evts+                (getProcessMailbox p)+                $ action (Just conf)   where-    interval = storeConfCacheMempoolSync cfg     f conf cwinbox = runReaderT (cacheWriter conf cwinbox) db     c conn metrics =       CacheConfig-        { cacheConn = conn,-          cacheMin = storeConfCacheMin cfg,-          cacheChain = chain,-          cacheMax = storeConfMaxKeys cfg,-          cacheMetrics = metrics+        { redis = conn,+          minAddrs = cfg.redisMinAddrs,+          chain = chain,+          maxKeys = cfg.redisMaxKeys,+          metrics = metrics         }  cacheWriterProcesses ::-  MonadUnliftIO m =>+  (MonadUnliftIO m) =>   Int ->   Inbox StoreEvent ->   CacheWriter ->@@ -268,7 +294,7 @@ cacheWriterProcesses interval evts cwm action =   withAsync (cacheWriterEvents interval evts cwm) $ \a1 -> link a1 >> action -cacheWriterEvents :: MonadUnliftIO m => Int -> Inbox StoreEvent -> CacheWriter -> m ()+cacheWriterEvents :: (MonadUnliftIO m) => Int -> Inbox StoreEvent -> CacheWriter -> m () cacheWriterEvents interval evts cwm =   withAsync mempool . const $     forever $@@ -279,14 +305,14 @@       threadDelay (interval * 1000 * 1000)       cacheSyncMempool cwm -cacheWriterDispatch :: MonadIO m => StoreEvent -> CacheWriter -> m ()+cacheWriterDispatch :: (MonadIO m) => StoreEvent -> CacheWriter -> m () cacheWriterDispatch (StoreBestBlock _) = cacheNewBlock cacheWriterDispatch (StoreMempoolNew t) = cacheNewTx t cacheWriterDispatch (StoreMempoolDelete t) = cacheNewTx t cacheWriterDispatch _ = const (return ())  nodeForwarder ::-  MonadIO m =>+  (MonadIO m) =>   BlockStore ->   Publisher StoreEvent ->   Inbox NodeEvent ->@@ -300,44 +326,49 @@   Publisher StoreEvent ->   NodeEvent ->   STM ()-storeDispatch b pub (PeerEvent (PeerConnected p)) = do-  publishSTM (StorePeerConnected p) pub-  blockStorePeerConnectSTM p b-storeDispatch b pub (PeerEvent (PeerDisconnected p)) = do-  publishSTM (StorePeerDisconnected p) pub-  blockStorePeerDisconnectSTM p b+storeDispatch b pub (PeerEvent pe) =+  case pe of+    PeerConnected p -> do+      publishSTM (StorePeerConnected p) pub+      blockStorePeerConnectSTM p b+    PeerDisconnected p -> do+      publishSTM (StorePeerDisconnected p) pub+      blockStorePeerDisconnectSTM p b+    PeerMessage p msg ->+      case msg of+        MPong (Pong n) ->+          publishSTM (StorePeerPong p n) pub+        MBlock block ->+          blockStoreBlockSTM p block b+        MTx tx ->+          blockStoreTxSTM p tx b+        MNotFound (NotFound is) -> do+          let blocks =+                [ BlockHash h+                  | InvVector t h <- is,+                    t == InvBlock || t == InvWitnessBlock+                ]+          unless (null blocks) $ blockStoreNotFoundSTM p blocks b+        MInv (Inv is) -> do+          let txs =+                [ TxHash h+                  | InvVector t h <- is,+                    t == InvTx || t == InvWitnessTx+                ]+          publishSTM (StoreTxAnnounce p txs) pub+          unless (null txs) $ blockStoreTxHashSTM p txs b+        MReject Reject {message = MCTx, extra, code, reason} ->+          case decode extra of+            Left _ -> return ()+            Right th ->+              let reject =+                    StoreTxReject+                      p+                      th+                      code+                      reason.get+               in publishSTM reject pub+        _ -> return () storeDispatch b _ (ChainEvent (ChainBestBlock bn)) =   blockStoreHeadSTM bn b-storeDispatch _ _ (ChainEvent _) =-  return ()-storeDispatch _ pub (PeerMessage p (MPong (Pong n))) =-  publishSTM (StorePeerPong p n) pub-storeDispatch b _ (PeerMessage p (MBlock block)) =-  blockStoreBlockSTM p block b-storeDispatch b _ (PeerMessage p (MTx tx)) =-  blockStoreTxSTM p tx b-storeDispatch b _ (PeerMessage p (MNotFound (NotFound is))) = do-  let blocks =-        [ BlockHash h-          | InvVector t h <- is,-            t == InvBlock || t == InvWitnessBlock-        ]-  unless (null blocks) $ blockStoreNotFoundSTM p blocks b-storeDispatch b pub (PeerMessage p (MInv (Inv is))) = do-  let txs = [TxHash h | InvVector t h <- is, t == InvTx || t == InvWitnessTx]-  publishSTM (StoreTxAnnounce p txs) pub-  unless (null txs) $ blockStoreTxHashSTM p txs b-storeDispatch _ pub (PeerMessage p (MReject r)) =-  when (rejectMessage r == MCTx) $-    case decode (rejectData r) of-      Left _ -> return ()-      Right th ->-        let reject =-              StoreTxReject-                p-                th-                (rejectCode r)-                (getVarString (rejectReason r))-         in publishSTM reject pub-storeDispatch _ _ _ =-  return ()+storeDispatch _ _ (ChainEvent _) = return ()
src/Haskoin/Store/Stats.hs view
@@ -1,5 +1,9 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE ImportQualifiedPost #-}+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE NoFieldSelectors #-}  module Haskoin.Store.Stats   ( StatDist,@@ -18,13 +22,13 @@     flushTQueue,     writeTQueue,   )-import qualified Control.Foldl as L+import Control.Foldl qualified as L import Control.Monad (forever) import Data.Function (on) import Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HashMap+import Data.HashMap.Strict qualified as HashMap import Data.Int (Int64)-import Data.List (sort, sortBy)+import Data.List (sort, sortBy, sortOn) import Data.Maybe (fromMaybe) import Data.Ord (Down (..), comparing) import Data.String.Conversions (cs)@@ -58,7 +62,7 @@   ) import UnliftIO.Concurrent (threadDelay) -withStats :: MonadIO m => Text -> Int -> Text -> (Store -> m a) -> m a+withStats :: (MonadIO m) => Text -> Int -> Text -> (Store -> m a) -> m a withStats h p pfx go = do   store <- liftIO newStore   _statsd <-@@ -74,62 +78,62 @@   go store  data StatData = StatData-  { statTimes :: ![Int64],-    statQueries :: !Int64,-    statItems :: !Int64,-    statClientErrors :: !Int64,-    statServerErrors :: !Int64+  { times :: ![Int64],+    queries :: !Int64,+    items :: !Int64,+    clientErrors :: !Int64,+    serverErrors :: !Int64   }  data StatDist = StatDist-  { distQueue :: !(TQueue Int64),-    distQueries :: !(TVar Int64),-    distItems :: !(TVar Int64),-    distClientErrors :: !(TVar Int64),-    distServerErrors :: !(TVar Int64)+  { queue :: !(TQueue Int64),+    queries :: !(TVar Int64),+    items :: !(TVar Int64),+    clientErrors :: !(TVar Int64),+    serverErrors :: !(TVar Int64)   } -createStatDist :: MonadIO m => Text -> Store -> m StatDist+createStatDist :: (MonadIO m) => Text -> Store -> m StatDist createStatDist t store = liftIO $ do-  distQueue <- newTQueueIO-  distQueries <- newTVarIO 0-  distItems <- newTVarIO 0-  distClientErrors <- newTVarIO 0-  distServerErrors <- newTVarIO 0+  queue <- newTQueueIO+  queries <- newTVarIO 0+  items <- newTVarIO 0+  clientErrors <- newTVarIO 0+  serverErrors <- newTVarIO 0   let metrics =         HashMap.fromList           [ ( t <> ".request_count",-              Counter . statQueries+              Counter . (.queries)             ),             ( t <> ".item_count",-              Counter . statItems+              Counter . (.items)             ),             ( t <> ".client_errors",-              Counter . statClientErrors+              Counter . (.clientErrors)             ),             ( t <> ".server_errors",-              Counter . statServerErrors+              Counter . (.serverErrors)             ),             ( t <> ".mean_ms",-              Gauge . mean . statTimes+              Gauge . mean . (.times)             ),             ( t <> ".avg_ms",-              Gauge . avg . statTimes+              Gauge . avg . (.times)             ),             ( t <> ".max_ms",-              Gauge . maxi . statTimes+              Gauge . maxi . (.times)             ),             ( t <> ".min_ms",-              Gauge . mini . statTimes+              Gauge . mini . (.times)             ),             ( t <> ".p90max_ms",-              Gauge . p90max . statTimes+              Gauge . p90max . (.times)             ),             ( t <> ".p90avg_ms",-              Gauge . p90avg . statTimes+              Gauge . p90avg . (.times)             ),             ( t <> ".var_ms",-              Gauge . var . statTimes+              Gauge . var . (.times)             )           ]   let sd = StatDist {..}@@ -139,36 +143,36 @@ toDouble :: Int64 -> Double toDouble = fromIntegral -addStatTime :: MonadIO m => StatDist -> Int64 -> m ()+addStatTime :: (MonadIO m) => StatDist -> Int64 -> m () addStatTime q =-  liftIO . atomically . writeTQueue (distQueue q)+  liftIO . atomically . writeTQueue q.queue -addStatQuery :: MonadIO m => StatDist -> m ()+addStatQuery :: (MonadIO m) => StatDist -> m () addStatQuery q =-  liftIO . atomically $ modifyTVar (distQueries q) (+ 1)+  liftIO . atomically $ modifyTVar q.queries (+ 1) -addStatItems :: MonadIO m => StatDist -> Int64 -> m ()+addStatItems :: (MonadIO m) => StatDist -> Int64 -> m () addStatItems q =-  liftIO . atomically . modifyTVar (distItems q) . (+)+  liftIO . atomically . modifyTVar q.items . (+) -addClientError :: MonadIO m => StatDist -> m ()+addClientError :: (MonadIO m) => StatDist -> m () addClientError q =-  liftIO . atomically $ modifyTVar (distClientErrors q) (+ 1)+  liftIO . atomically $ modifyTVar q.clientErrors (+ 1) -addServerError :: MonadIO m => StatDist -> m ()+addServerError :: (MonadIO m) => StatDist -> m () addServerError q =-  liftIO . atomically $ modifyTVar (distServerErrors q) (+ 1)+  liftIO . atomically $ modifyTVar q.serverErrors (+ 1) -flush :: MonadIO m => StatDist -> m StatData+flush :: (MonadIO m) => StatDist -> m StatData flush StatDist {..} = atomically $ do-  statTimes <- flushTQueue distQueue-  statQueries <- readTVar distQueries-  statItems <- readTVar distItems-  statClientErrors <- readTVar distClientErrors-  statServerErrors <- readTVar distServerErrors+  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 :: (Fractional a) => L.Fold a a average = (/) <$> L.sum <*> L.genericLength  avg :: [Int64] -> Int64@@ -192,14 +196,14 @@     [] -> 0     h : _ -> h   where-    sorted = sortBy (comparing Down) ls+    sorted = sortOn Down ls     len = length sorted-    chopped = drop (length sorted * 1 `div` 10) sorted+    chopped = drop (length sorted `div` 10) sorted  p90avg :: [Int64] -> Int64 p90avg ls =   avg chopped   where-    sorted = sortBy (comparing Down) ls+    sorted = sortOn Down ls     len = length sorted-    chopped = drop (length sorted * 1 `div` 10) sorted+    chopped = drop (length sorted `div` 10) sorted
src/Haskoin/Store/Web.hs view
@@ -1,3194 +1,3166 @@ {-# LANGUAGE BlockArguments #-}-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE FlexibleInstances #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE MultiWayIf #-}-{-# LANGUAGE NamedFieldPuns #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE TupleSections #-}-{-# LANGUAGE ImportQualifiedPost #-}-{-# OPTIONS_GHC -Wno-deprecations #-}--module Haskoin.Store.Web-  ( -- * Web-    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.ByteString (ByteString)-import Data.ByteString qualified as B-import Data.ByteString.Base16 qualified as B16-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,-    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)-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.Constants-import Haskoin.Data-import Haskoin.Keys-import Haskoin.Network-import Haskoin.Node-  ( Chain,-    OnlinePeer (..),-    PeerManager,-    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.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-  { maxLimitCount :: !Word32,-    maxLimitFull :: !Word32,-    maxLimitOffset :: !Word32,-    maxLimitDefault :: !Word32,-    maxLimitGap :: !Word32,-    maxLimitInitialGap :: !Word32,-    maxLimitBody :: !Word32,-    maxLimitTimeout :: !Word32-  }-  deriving (Eq, Show)--instance Default WebLimits where-  def =-    WebLimits-      { maxLimitCount = 200000,-        maxLimitFull = 5000,-        maxLimitOffset = 50000,-        maxLimitDefault = 100,-        maxLimitGap = 32,-        maxLimitInitialGap = 20,-        maxLimitBody = 1024 * 1024,-        maxLimitTimeout = 0-      }--data WebConfig = WebConfig-  { webHost :: !String,-    webPort :: !Int,-    webStore :: !Store,-    webMaxDiff :: !Int,-    webMaxPending :: !Int,-    webMaxLimits :: !WebLimits,-    webTimeouts :: !WebTimeouts,-    webVersion :: !String,-    webNoMempool :: !Bool,-    webStats :: !(Maybe Metrics.Store),-    webPriceGet :: !Int,-    webTickerURL :: !String,-    webHistoryURL :: !String,-    webSlow :: !Bool,-    webBlockchain :: !Bool,-    webHealthCheckInterval :: !Int-  }--data WebState = WebState-  { webConfig :: !WebConfig,-    webTicker :: !(TVar (HashMap Text BinfoTicker)),-    webMetrics :: !(Maybe WebMetrics),-    webWreqSession :: !Wreq.Session,-    webHealthCheck :: !(TVar HealthCheck)-  }--data WebMetrics = WebMetrics-  { statAll :: !StatDist,-    -- Addresses-    statAddressTransactions :: !StatDist,-    statAddressTransactionsFull :: !StatDist,-    statAddressBalance :: !StatDist,-    statAddressUnspent :: !StatDist,-    statXpub :: !StatDist,-    statXpubDelete :: !StatDist,-    statXpubTransactionsFull :: !StatDist,-    statXpubTransactions :: !StatDist,-    statXpubBalances :: !StatDist,-    statXpubUnspent :: !StatDist,-    -- Transactions-    statTransaction :: !StatDist,-    statTransactionRaw :: !StatDist,-    statTransactionAfter :: !StatDist,-    statTransactionsBlock :: !StatDist,-    statTransactionsBlockRaw :: !StatDist,-    statTransactionPost :: !StatDist,-    statMempool :: !StatDist,-    -- Blocks-    statBlock :: !StatDist,-    statBlockRaw :: !StatDist,-    -- Blockchain-    statBlockchainMultiaddr :: !StatDist,-    statBlockchainBalance :: !StatDist,-    statBlockchainRawaddr :: !StatDist,-    statBlockchainUnspent :: !StatDist,-    statBlockchainRawtx :: !StatDist,-    statBlockchainRawblock :: !StatDist,-    statBlockchainMempool :: !StatDist,-    statBlockchainBlockHeight :: !StatDist,-    statBlockchainBlocks :: !StatDist,-    statBlockchainLatestblock :: !StatDist,-    statBlockchainExportHistory :: !StatDist,-    -- Blockchain /q endpoints-    statBlockchainQaddresstohash :: !StatDist,-    statBlockchainQhashtoaddress :: !StatDist,-    statBlockchainQaddrpubkey :: !StatDist,-    statBlockchainQpubkeyaddr :: !StatDist,-    statBlockchainQhashpubkey :: !StatDist,-    statBlockchainQgetblockcount :: !StatDist,-    statBlockchainQlatesthash :: !StatDist,-    statBlockchainQbcperblock :: !StatDist,-    statBlockchainQtxtotalbtcoutput :: !StatDist,-    statBlockchainQtxtotalbtcinput :: !StatDist,-    statBlockchainQtxfee :: !StatDist,-    statBlockchainQtxresult :: !StatDist,-    statBlockchainQgetreceivedbyaddress :: !StatDist,-    statBlockchainQgetsentbyaddress :: !StatDist,-    statBlockchainQaddressbalance :: !StatDist,-    statBlockchainQaddressfirstseen :: !StatDist,-    -- Others-    statHealth :: !StatDist,-    statPeers :: !StatDist,-    statDbstats :: !StatDist,-    statEvents :: !Metrics.Gauge.Gauge,-    -- Request-    statKey :: !(V.Key (TVar (Maybe (WebMetrics -> StatDist))))-  }--createMetrics :: MonadIO m => Metrics.Store -> m WebMetrics-createMetrics s = liftIO $ do-  statAll <- d "all"--  -- Addresses-  statAddressTransactions <- d "address_transactions"-  statAddressTransactionsFull <- d "address_transactions_full"-  statAddressBalance <- d "address_balance"-  statAddressUnspent <- d "address_unspent"-  statXpub <- d "xpub"-  statXpubDelete <- d "xpub_delete"-  statXpubTransactionsFull <- d "xpub_transactions_full"-  statXpubTransactions <- d "xpub_transactions"-  statXpubBalances <- d "xpub_balances"-  statXpubUnspent <- d "xpub_unspent"--  -- Transactions-  statTransaction <- d "transaction"-  statTransactionRaw <- d "transaction_raw"-  statTransactionAfter <- d "transaction_after"-  statTransactionPost <- d "transaction_post"-  statTransactionsBlock <- d "transactions_block"-  statTransactionsBlockRaw <- d "transactions_block_raw"-  statMempool <- d "mempool"--  -- Blocks-  statBlockBest <- d "block_best"-  statBlockLatest <- d "block_latest"-  statBlock <- d "block"-  statBlockRaw <- d "block_raw"-  statBlockHeight <- d "block_height"-  statBlockHeightRaw <- d "block_height_raw"-  statBlockTime <- d "block_time"-  statBlockTimeRaw <- d "block_time_raw"-  statBlockMtp <- d "block_mtp"-  statBlockMtpRaw <- d "block_mtp_raw"--  -- Blockchain-  statBlockchainMultiaddr <- d "blockchain_multiaddr"-  statBlockchainBalance <- d "blockchain_balance"-  statBlockchainRawaddr <- d "blockchain_rawaddr"-  statBlockchainUnspent <- d "blockchain_unspent"-  statBlockchainRawtx <- d "blockchain_rawtx"-  statBlockchainRawblock <- d "blockchain_rawblock"-  statBlockchainLatestblock <- d "blockchain_latestblock"-  statBlockchainMempool <- d "blockchain_mempool"-  statBlockchainBlockHeight <- d "blockchain_block_height"-  statBlockchainBlocks <- d "blockchain_blocks"-  statBlockchainExportHistory <- d "blockchain_export_history"--  -- Blockchain /q endpoints-  statBlockchainQaddresstohash <- d "blockchain_q_addresstohash"-  statBlockchainQhashtoaddress <- d "blockchain_q_hashtoaddress"-  statBlockchainQaddrpubkey <- d "blockckhain_q_addrpubkey"-  statBlockchainQpubkeyaddr <- d "blockchain_q_pubkeyaddr"-  statBlockchainQhashpubkey <- d "blockchain_q_hashpubkey"-  statBlockchainQgetblockcount <- d "blockchain_q_getblockcount"-  statBlockchainQlatesthash <- d "blockchain_q_latesthash"-  statBlockchainQbcperblock <- d "blockchain_q_bcperblock"-  statBlockchainQtxtotalbtcoutput <- d "blockchain_q_txtotalbtcoutput"-  statBlockchainQtxtotalbtcinput <- d "blockchain_q_txtotalbtcinput"-  statBlockchainQtxfee <- d "blockchain_q_txfee"-  statBlockchainQtxresult <- d "blockchain_q_txresult"-  statBlockchainQgetreceivedbyaddress <- d "blockchain_q_getreceivedbyaddress"-  statBlockchainQgetsentbyaddress <- d "blockchain_q_getsentbyaddress"-  statBlockchainQaddressbalance <- d "blockchain_q_addressbalance"-  statBlockchainQaddressfirstseen <- d "blockchain_q_addressfirstseen"--  -- Others-  statHealth <- d "health"-  statPeers <- d "peers"-  statDbstats <- d "dbstats"--  statEvents <- g "events_connected"-  statKey <- 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 webMetrics) >>= \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 webMetrics >>= mapM_ go-  where-    go m = do-      req <- S.request-      let t = fromMaybe e $ V.lookup (statKey m) (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 webMetrics >>= mapM_ \m -> do-    addStatItems (statAll m) (fromIntegral i)-    req <- S.request-    forM_ (V.lookup (statKey m) (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 webMetrics-    t <- MaybeT . return $ V.lookup (statKey m) (vault q)-    s <- MaybeT $ readTVarIO t-    return $ addStatItems (s m) . fromIntegral--data WebTimeouts = WebTimeouts-  { txTimeout :: !Word64,-    blockTimeout :: !Word64-  }-  deriving (Eq, Show)--data SerialAs = SerialAsBinary | SerialAsJSON | SerialAsPrettyJSON-  deriving (Eq, Show)--instance Default WebTimeouts where-  def = WebTimeouts {txTimeout = 300, blockTimeout = 7200}--instance-  (MonadUnliftIO m, MonadLoggerIO m) =>-  StoreReadBase (ReaderT WebState m)-  where-  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-  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-  cfg@WebConfig-    { webHost = host,-      webPort = port,-      webStore = store',-      webStats = stats,-      webPriceGet = pget,-      webTickerURL = turl,-      webMaxLimits = WebLimits {..},-      webSlow = slow,-      webBlockchain = blockchain,-      webHealthCheckInterval = healthint-    } = do-    ticker <- newTVarIO HashMap.empty-    metrics <- mapM createMetrics stats-    session <- liftIO Wreq.Session.newAPISession-    health' <- hcheck >>= newTVarIO-    let st =-          WebState-            { webConfig = cfg,-              webTicker = ticker,-              webMetrics = metrics,-              webWreqSession = session,-              webHealthCheck = health'-            }-        net = storeNetwork store'-    withAsync (when blockchain $ price net session turl pget ticker)-      . const-      . withAsync (health health')-      . const-      $ do-        reqLogger <- logIt metrics-        runner <- askRunInIO-        S.scottyOptsT opts (runner . (`runReaderT` st)) $ do-          S.middleware (webSocketEvents st)-          S.middleware reqLogger-          S.middleware (reqSizeLimit maxLimitBody)-          when (maxLimitTimeout > 0) $ S.middleware (reqTimeout maxLimitTimeout)-          S.defaultHandler defHandler-          handlePaths slow blockchain-          S.notFound $ raise ThingNotFound-    where-      hcheck = runReaderT (healthCheck cfg) (storeDB store')-      health v = forever $ do-        threadDelay (healthint * 1000 * 1000)-        hcheck >>= atomically . writeTVar v-      opts = def {S.settings = settings defaultSettings}-      settings = setPort port . setHost (fromString 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 (getNetworkName net))]-    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 webMetrics) >>= \case-    Nothing -> S.raise err-    Just m -> do-      req <- S.request-      mM <- case V.lookup (statKey m) (vault req) of-        Nothing -> return Nothing-        Just t -> readTVarIO t-      let status = errStatus err-      if-          | statusIsClientError status ->-              liftIO $ do-                addClientError (statAll m)-                forM_ mM $ \f -> addClientError (f m)-          | statusIsServerError status ->-              liftIO $ do-                addServerError (statAll m)-                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) =>-  Bool ->-  Bool ->-  S.ScottyT Except (ReaderT WebState m) ()-handlePaths slow blockchain = do-  -- Block Paths-  pathCompact-    (GetBlock <$> paramLazy <*> paramDef)-    scottyBlock-    blockDataToEncoding-    blockDataToJSON-  when slow $-    pathCompact-      (GetBlocks <$> param <*> paramDef)-      (fmap SerialList . scottyBlocks)-      (\n -> list (blockDataToEncoding n) . getSerialList)-      (\n -> json_list blockDataToJSON n . getSerialList)-  when slow $-    pathCompact-      (GetBlockRaw <$> paramLazy)-      scottyBlockRaw-      (const toEncoding)-      (const toJSON)-  pathCompact-    (GetBlockBest <$> paramDef)-    scottyBlockBest-    blockDataToEncoding-    blockDataToJSON-  when slow $-    pathCompact-      (GetBlockBestRaw & return)-      scottyBlockBestRaw-      (const toEncoding)-      (const toJSON)-  when slow $-    pathCompact-      (GetBlockLatest <$> paramDef)-      (fmap SerialList . scottyBlockLatest)-      (\n -> list (blockDataToEncoding n) . getSerialList)-      (\n -> json_list blockDataToJSON n . getSerialList)-  pathCompact-    (GetBlockHeight <$> paramLazy <*> paramDef)-    (fmap SerialList . scottyBlockHeight)-    (\n -> list (blockDataToEncoding n) . getSerialList)-    (\n -> json_list blockDataToJSON n . getSerialList)-  when slow $-    pathCompact-      (GetBlockHeights <$> param <*> paramDef)-      (fmap SerialList . scottyBlockHeights)-      (\n -> list (blockDataToEncoding n) . getSerialList)-      (\n -> json_list blockDataToJSON n . getSerialList)-  when slow $-    pathCompact-      (GetBlockHeightRaw <$> paramLazy)-      scottyBlockHeightRaw-      (const toEncoding)-      (const toJSON)-  pathCompact-    (GetBlockTime <$> paramLazy <*> paramDef)-    scottyBlockTime-    blockDataToEncoding-    blockDataToJSON-  when slow $-    pathCompact-      (GetBlockTimeRaw <$> paramLazy)-      scottyBlockTimeRaw-      (const toEncoding)-      (const toJSON)-  pathCompact-    (GetBlockMTP <$> paramLazy <*> paramDef)-    scottyBlockMTP-    blockDataToEncoding-    blockDataToJSON-  when slow $-    pathCompact-      (GetBlockMTPRaw <$> paramLazy)-      scottyBlockMTPRaw-      (const toEncoding)-      (const toJSON)-  -- Transaction Paths-  pathCompact-    (GetTx <$> paramLazy)-    scottyTx-    transactionToEncoding-    transactionToJSON-  when slow $-    pathCompact-      (GetTxs <$> param)-      (fmap SerialList . scottyTxs)-      (\n -> list (transactionToEncoding n) . getSerialList)-      (\n -> json_list transactionToJSON n . getSerialList)-  pathCompact-    (GetTxRaw <$> paramLazy)-    scottyTxRaw-    (const toEncoding)-    (const toJSON)-  when slow $-    pathCompact-      (GetTxsRaw <$> param)-      scottyTxsRaw-      (const toEncoding)-      (const toJSON)-  when slow $-    pathCompact-      (GetTxsBlock <$> paramLazy)-      (fmap SerialList . scottyTxsBlock)-      (\n -> list (transactionToEncoding n) . getSerialList)-      (\n -> json_list transactionToJSON n . getSerialList)-  when slow $-    pathCompact-      (GetTxsBlockRaw <$> paramLazy)-      scottyTxsBlockRaw-      (const toEncoding)-      (const toJSON)-  when slow $-    pathCompact-      (GetTxAfter <$> paramLazy <*> paramLazy)-      scottyTxAfter-      (const toEncoding)-      (const toJSON)-  pathCompact-    (PostTx <$> parseBody)-    scottyPostTx-    (const toEncoding)-    (const toJSON)-  pathCompact-    (GetMempool <$> paramOptional <*> parseOffset)-    (fmap SerialList . scottyMempool)-    (const toEncoding)-    (const toJSON)-  -- Address Paths-  pathCompact-    (GetAddrTxs <$> paramLazy <*> parseLimits)-    (fmap SerialList . scottyAddrTxs)-    (const toEncoding)-    (const toJSON)-  when slow $-    pathCompact-      (GetAddrsTxs <$> param <*> parseLimits)-      (fmap SerialList . scottyAddrsTxs)-      (const toEncoding)-      (const toJSON)-  when slow $-    pathCompact-      (GetAddrTxsFull <$> paramLazy <*> parseLimits)-      (fmap SerialList . scottyAddrTxsFull)-      (\n -> list (transactionToEncoding n) . getSerialList)-      (\n -> json_list transactionToJSON n . getSerialList)-  when slow $-    pathCompact-      (GetAddrsTxsFull <$> param <*> parseLimits)-      (fmap SerialList . scottyAddrsTxsFull)-      (\n -> list (transactionToEncoding n) . getSerialList)-      (\n -> json_list transactionToJSON n . getSerialList)-  pathCompact-    (GetAddrBalance <$> paramLazy)-    scottyAddrBalance-    balanceToEncoding-    balanceToJSON-  when slow $-    pathCompact-      (GetAddrsBalance <$> param)-      (fmap SerialList . scottyAddrsBalance)-      (\n -> list (balanceToEncoding n) . getSerialList)-      (\n -> json_list balanceToJSON n . getSerialList)-  pathCompact-    (GetAddrUnspent <$> paramLazy <*> parseLimits)-    (fmap SerialList . scottyAddrUnspent)-    (\n -> list (unspentToEncoding n) . getSerialList)-    (\n -> json_list unspentToJSON n . getSerialList)-  when slow $-    pathCompact-      (GetAddrsUnspent <$> param <*> parseLimits)-      (fmap SerialList . scottyAddrsUnspent)-      (\n -> list (unspentToEncoding n) . getSerialList)-      (\n -> json_list unspentToJSON n . getSerialList)-  -- XPubs-  when slow $-    pathCompact-      (GetXPub <$> paramLazy <*> paramDef <*> paramDef)-      scottyXPub-      (const toEncoding)-      (const toJSON)-  when slow $-    pathCompact-      (GetXPubTxs <$> paramLazy <*> paramDef <*> parseLimits <*> paramDef)-      (fmap SerialList . scottyXPubTxs)-      (const toEncoding)-      (const toJSON)-  when slow $-    pathCompact-      (GetXPubTxsFull <$> paramLazy <*> paramDef <*> parseLimits <*> paramDef)-      (fmap SerialList . scottyXPubTxsFull)-      (\n -> list (transactionToEncoding n) . getSerialList)-      (\n -> json_list transactionToJSON n . getSerialList)-  when slow $-    pathCompact-      (GetXPubBalances <$> paramLazy <*> paramDef <*> paramDef)-      (fmap SerialList . scottyXPubBalances)-      (\n -> list (xPubBalToEncoding n) . getSerialList)-      (\n -> json_list xPubBalToJSON n . getSerialList)-  when slow $-    pathCompact-      (GetXPubUnspent <$> paramLazy <*> paramDef <*> parseLimits <*> paramDef)-      (fmap SerialList . scottyXPubUnspent)-      (\n -> list (xPubUnspentToEncoding n) . getSerialList)-      (\n -> json_list xPubUnspentToJSON n . getSerialList)-  when slow $-    pathCompact-      (DelCachedXPub <$> paramLazy <*> paramDef)-      scottyDelXPub-      (const toEncoding)-      (const toJSON)-  -- Network-  pathCompact-    (GetPeers & return)-    (fmap SerialList . scottyPeers)-    (const toEncoding)-    (const toJSON)-  pathCompact-    (GetHealth & return)-    scottyHealth-    (const toEncoding)-    (const toJSON)-  S.get "/events" scottyEvents-  S.get "/dbstats" scottyDbStats-  -- Blockchain.info-  when blockchain $ 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 net = toJSONList . map (f net)--pathCompact ::-  (ApiResource a b, MonadIO m) =>-  WebT m a ->-  (a -> WebT m b) ->-  (Network -> b -> Encoding) ->-  (Network -> 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) ->-  (Network -> b -> Encoding) ->-  (Network -> 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-    net <- lift $ asks (storeNetwork . webStore . webConfig)-    apiRes <- parser-    res <- action apiRes-    S.raw $ protoSerial proto (encJson net) (encValue net) 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 statBlock-  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 statBlock-  bs <- getBlocks hs notx-  addItemCount (length bs)-  return bs--pruneTx :: Bool -> BlockData -> BlockData-pruneTx False b = b-pruneTx True b = b {blockDataTxs = take 1 (blockDataTxs b)}---- GET BlockRaw ----scottyBlockRaw ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetBlockRaw ->-  WebT m (RawResult H.Block)-scottyBlockRaw (GetBlockRaw h) = do-  setMetrics statBlockRaw-  b <- getRawBlock h-  addItemCount (1 + length (H.blockTxns b))-  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 = blockDataTxs b-  txs <- mapM f ths-  return H.Block {H.blockHeader = blockDataHeader b, H.blockTxns = 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 statBlock-  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 statBlockRaw-  getBestBlock >>= \case-    Nothing -> raise ThingNotFound-    Just bb -> do-      b <- getRawBlock bb-      addItemCount (1 + length (H.blockTxns b))-      return $ RawResult b---- GET BlockLatest ----scottyBlockLatest ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetBlockLatest ->-  WebT m [BlockData]-scottyBlockLatest (GetBlockLatest (NoTx noTx)) = do-  setMetrics statBlock-  blocks <--    getBestBlock-      >>= maybe-        (raise ThingNotFound)-        (go [] <=< getBlock)-  addItemCount (length blocks)-  return blocks-  where-    go acc Nothing = return $ reverse acc-    go acc (Just b)-      | blockDataHeight b <= 0 = return $ reverse acc-      | length acc == 99 = return . reverse $ pruneTx noTx b : acc-      | otherwise = do-          let prev = H.prevBlock (blockDataHeader b)-          go (pruneTx noTx b : acc) =<< getBlock prev---- GET BlockHeight / BlockHeights / BlockHeightRaw ----scottyBlockHeight ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetBlockHeight -> WebT m [BlockData]-scottyBlockHeight (GetBlockHeight h (NoTx notx)) = do-  setMetrics statBlock-  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 statBlock-  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 statBlockRaw-  blocks <- mapM getRawBlock =<< getBlocksAtHeight (fromIntegral h)-  addItemCount (length blocks + sum (map (length . H.blockTxns) blocks))-  return $ RawResultList blocks---- GET BlockTime / BlockTimeRaw ----scottyBlockTime ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetBlockTime ->-  WebT m BlockData-scottyBlockTime (GetBlockTime (TimeParam t) (NoTx notx)) = do-  setMetrics statBlock-  ch <- lift $ asks (storeChain . webStore . webConfig)-  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 statBlock-  ch <- lift $ asks (storeChain . webStore . webConfig)-  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 statBlockRaw-  ch <- lift $ asks (storeChain . webStore . webConfig)-  blockAtOrBefore ch t >>= \case-    Nothing -> raise ThingNotFound-    Just b -> do-      raw <- lift $ toRawBlock b-      addItemCount (1 + length (H.blockTxns raw))-      return $ RawResult raw--scottyBlockMTPRaw ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetBlockMTPRaw ->-  WebT m (RawResult H.Block)-scottyBlockMTPRaw (GetBlockMTPRaw (TimeParam t)) = do-  setMetrics statBlockRaw-  ch <- lift $ asks (storeChain . webStore . webConfig)-  blockAtOrAfterMTP ch t >>= \case-    Nothing -> raise ThingNotFound-    Just b -> do-      raw <- lift $ toRawBlock b-      addItemCount (1 + length (H.blockTxns raw))-      return $ RawResult raw---- GET Transactions ----scottyTx :: (MonadUnliftIO m, MonadLoggerIO m) => GetTx -> WebT m Transaction-scottyTx (GetTx txid) = do-  setMetrics statTransaction-  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 statTransaction-  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 statTransactionRaw-  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 statTransactionRaw-  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 (blockDataTxs b)-      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 statTransactionsBlock-  txs <- getTxsBlock h-  addItemCount (length txs)-  return txs--scottyTxsBlockRaw ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetTxsBlockRaw ->-  WebT m (RawResultList Tx)-scottyTxsBlockRaw (GetTxsBlockRaw h) = do-  setMetrics statTransactionsBlockRaw-  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 statTransactionAfter-  (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 . transactionInputs-    ins = HashSet.fromList . map (outPointHash . inputPoint) . transactionInputs-    height_check tx =-      case transactionBlock tx of-        BlockRef h _ -> h > height-        _ -> True---- POST Transaction ----scottyPostTx :: (MonadUnliftIO m, MonadLoggerIO m) => PostTx -> WebT m TxId-scottyPostTx (PostTx tx) = do-  setMetrics statTransactionPost-  addItemCount 1-  lift (asks webConfig) >>= \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 = storePublisher (webStore cfg)-    mgr = storeManager (webStore cfg)-    net = storeNetwork (webStore cfg)-    go s =-      getPeers mgr >>= \case-        [] -> return $ Left PubNoPeers-        OnlinePeer {onlinePeerMailbox = p} : _ -> do-          MTx tx `sendMessage` p-          let v =-                if getSegWit net-                  then InvWitnessTx-                  else InvTx-          sendMessage-            (MGetData (GetData [InvVector v (getTxHash (txHash tx))]))-            p-          f p s-    t = 5 * 1000 * 1000-    f p s-      | webNoMempool cfg = 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 statMempool-  wl <- lift $ asks (webMaxLimits . webConfig)-  let wl' = wl {maxLimitCount = 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 = (storePublisher . webStore . webConfig) s-    gauge = statEvents <$> webMetrics s-    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 statEvents $ do-    setHeaders-    proto <- setupContentType False-    pub <- lift $ asks (storePublisher . webStore . webConfig)-    S.stream $ \io flush' ->-      withSubscription pub $ \sub ->-        forever $-          flush' >> receiveEvent sub >>= maybe (return ()) (io . serial proto)-  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 statAddressTransactions-  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 statAddressTransactions-  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 statAddressTransactionsFull-  txs <- getAddressTxs addr =<< paramToLimits True pLimits-  ts <- catMaybes <$> mapM (getTransaction . txRefHash) txs-  addItemCount (length ts)-  return ts--scottyAddrsTxsFull ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetAddrsTxsFull ->-  WebT m [Transaction]-scottyAddrsTxsFull (GetAddrsTxsFull addrs pLimits) = do-  setMetrics statAddressTransactionsFull-  txs <- getAddressesTxs addrs =<< paramToLimits True pLimits-  ts <- catMaybes <$> mapM (getTransaction . txRefHash) txs-  addItemCount (length ts)-  return ts--scottyAddrBalance ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetAddrBalance ->-  WebT m Balance-scottyAddrBalance (GetAddrBalance addr) = do-  setMetrics statAddressBalance-  addItemCount 1-  getDefaultBalance addr--scottyAddrsBalance ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetAddrsBalance -> WebT m [Balance]-scottyAddrsBalance (GetAddrsBalance addrs) = do-  setMetrics statAddressBalance-  balances <- getBalances addrs-  addItemCount (length balances)-  return balances--scottyAddrUnspent ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetAddrUnspent -> WebT m [Unspent]-scottyAddrUnspent (GetAddrUnspent addr pLimits) = do-  setMetrics statAddressUnspent-  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 statAddressUnspent-  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 statXpub-  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 statXpubDelete-  let xspec = XPubSpec xpub deriv-  cacheM <- lift (asks (storeCache . webStore . webConfig))-  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 statXpubTransactions-  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 statXpubTransactionsFull-  refs <- getXPubTxs xpub deriv plimits nocache-  txs <--    fmap catMaybes $-      lift . runNoCache nocache $-        mapM (getTransaction . txRefHash) refs-  addItemCount (length txs)-  return txs--scottyXPubBalances ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetXPubBalances -> WebT m [XPubBal]-scottyXPubBalances (GetXPubBalances xpub deriv (NoCache noCache)) = do-  setMetrics statXpubBalances-  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 statXpubUnspent-  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-        { getBinfoSymbolCode = "BTC",-          getBinfoSymbolString = "BTC",-          getBinfoSymbolName = "Bitcoin",-          getBinfoSymbolConversion = 100 * 1000 * 1000,-          getBinfoSymbolAfter = True,-          getBinfoSymbolLocal = False-        }-  | net == bch =-      BinfoSymbol-        { getBinfoSymbolCode = "BCH",-          getBinfoSymbolString = "BCH",-          getBinfoSymbolName = "Bitcoin Cash",-          getBinfoSymbolConversion = 100 * 1000 * 1000,-          getBinfoSymbolAfter = True,-          getBinfoSymbolLocal = False-        }-  | otherwise =-      BinfoSymbol-        { getBinfoSymbolCode = "XTS",-          getBinfoSymbolString = "¤",-          getBinfoSymbolName = "Test",-          getBinfoSymbolConversion = 100 * 1000 * 1000,-          getBinfoSymbolAfter = False,-          getBinfoSymbolLocal = False-        }--binfoTickerToSymbol :: Text -> BinfoTicker -> BinfoSymbol-binfoTickerToSymbol code BinfoTicker {..} =-  BinfoSymbol-    { getBinfoSymbolCode = code,-      getBinfoSymbolString = binfoTickerSymbol,-      getBinfoSymbolName = name,-      getBinfoSymbolConversion =-        100 * 1000 * 1000 / binfoTicker15m, -- sat/usd-      getBinfoSymbolAfter = False,-      getBinfoSymbolLocal = 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 (storeNetwork . webStore . webConfig))-  p <- S.param (cs name) `S.rescue` const (return "")-  if T.null p-    then return HashSet.empty-    else case parseBinfoAddr net 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 (storeChain . webStore . webConfig)-  H.nodeHeight <$> chainGetBest ch--scottyBinfoUnspent :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoUnspent = do-  setMetrics statBlockchainUnspent-  (xspecs, addrs) <- getBinfoActive-  numtxid <- getNumTxId-  limit <- get_limit-  min_conf <- get_min_conf-  net <- lift $ asks (storeNetwork . webStore . webConfig)-  height <- getChainHeight-  let mn BinfoUnspent {..} = min_conf > getBinfoUnspentConfirmations-  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 (binfoUnspentsToEncoding net (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 unspentBlock of-            MemRef {} -> 0-            BlockRef h _ -> height - h + 1-          hash = outPointHash unspentPoint-          idx = outPointIndex unspentPoint-          val = unspentAmount-          script = unspentScript-          txi = encodeBinfoTxId numtxid hash-       in BinfoUnspent-            { getBinfoUnspentHash = hash,-              getBinfoUnspentOutputIndex = idx,-              getBinfoUnspentScript = script,-              getBinfoUnspentValue = val,-              getBinfoUnspentConfirmations = fromIntegral conf,-              getBinfoUnspentTxIndex = txi,-              getBinfoUnspentXPub = xp-            }-    conduits = (<>) <$> xconduits <*> pure acounduits-    xconduits = lift $ do-      let f x (XPubUnspent u p) =-            let path = toSoft (listToPath p)-                xp = BinfoXPubPath (xPubSpecKey x) <$> path-             in (u, xp)-          g x = do-            return $-              streamThings-                ( \l -> do-                    us <- xPubUnspents x (xBals x xbals) l-                    counter (length us)-                    return us-                )-                Nothing-                def {limit = 16}-                .| mapC (f x)-      mapM g (HashSet.toList xspecs)-    acounduits =-      let f u = (u, Nothing)-          g a =-            streamThings-              ( \l -> do-                  us <- getAddressUnspents a l-                  counter (length us)-                  return us-              )-              Nothing-              def {limit = 16}-              .| mapC f-       in map g (HashSet.toList addrs)--getXBals :: StoreReadExtra m => HashSet XPubSpec -> m (HashMap XPubSpec [XPubBal])-getXBals =-  fmap HashMap.fromList-    . mapM-      ( \x ->-          (x,) . filter (not . nullBalance . xPubBal)-            <$> (xPubBals x)-      )-    . HashSet.toList--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 =-        lift . return $-          streamThings-            ( \l -> do-                ts <- xPubTxs x (xBals x xbals) l-                counter (length ts)-                return ts-            )-            (Just txRefHash)-            def {limit = 16}-      addr_c a =-        streamThings-          ( \l -> do-              as <- getAddressTxs a l-              counter (length as)-              return as-          )-          (Just txRefHash)-          def {limit = 16}-      binfo_tx b = toBinfoTx numtxid abook prune b-      compute_bal_change BinfoTx {..} =-        let ins = map getBinfoTxInputPrevOut getBinfoTxInputs-            out = getBinfoTxOutputs-            f b BinfoTxOutput {..} =-              let val = fromIntegral getBinfoTxOutputValue-               in case getBinfoTxOutputAddress 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 (getBinfoTxBlockHeight a)-                    Just (d, _) = getBinfoTxResultBal a-                    r = d + fromIntegral (getBinfoTxFee a)-                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 (storeNetwork . webStore . webConfig)-  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 (storeNetwork . webStore . webConfig)-  let x =-        BinfoAddr <$> textToAddr net txt-          <|> BinfoXpub <$> xPubImport net txt-  maybe S.next return x--scottyBinfoHistory :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoHistory = do-  setMetrics statBlockchainExportHistory-  (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 = binfoTicker15m price'-      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 transactionTime txs-  net <- lift $ asks (storeNetwork . webStore . webConfig)-  url <- lift $ asks (webHistoryURL . webConfig)-  session <- lift $ asks webWreqSession-  rates <- map binfoRatePrice <$> 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 BlockData {..}) TxRef {txRefBlock = BlockRef {..}} =-      blockRefHeight >= blockDataHeight-    is_newer _ _ = True-    get_addr = balanceAddress . xPubBal-    get_transaction TxRef {txRefHash = h} =-      getTransaction h-    convert cur addrs tx rate =-      let ins = transactionInputs tx-          outs = transactionOutputs tx-          fins = filter (input_addr addrs) ins-          fouts = filter (output_addr addrs) outs-          vin = fromIntegral . sum $ map inputAmount fins-          vout = fromIntegral . sum $ map outputAmount fouts-          fee = transactionFees tx-          v = vout - vin-          t = transactionTime tx-          h = txHash $ transactionData tx-       in toBinfoHistory v t rate cur fee h-    input_addr addrs' StoreInput {inputAddress = Just a} =-      a `HashSet.member` addrs'-    input_addr _ _ = False-    output_addr addrs' StoreOutput {outputAddr = 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 (storeChain . webStore . webConfig)-      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 =-      streamThings-        ( \l -> do-            ts <- getAddressTxs a l-            counter (length ts)-            return ts-        )-        (Just txRefHash)-        def-          { limit = 16,-            start = AtBlock . blockDataHeight <$> endM-          }-    xpub_c counter endM x bs =-      streamThings-        ( \l -> do-            ts <- xPubTxs x bs l-            counter (length ts)-            return ts-        )-        (Just txRefHash)-        def-          { limit = 16,-            start = AtBlock . blockDataHeight <$> endM-          }--getPrice :: MonadIO m => WebT m (Text, BinfoTicker)-getPrice = do-  code <- T.toUpper <$> S.param "currency" `S.rescue` const (return "USD")-  ticker <- lift $ asks webTicker-  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 statBlockchainBlocks-  t <- min h . (`div` 1000) <$> S.param "milliseconds"-  ch <- lift $ asks (storeChain . webStore . webConfig)-  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)-      | H.blockTimestamp (blockDataHeader b) <= fromIntegral t =-          return []-      | otherwise = do-          b' <- getBlock (H.prevBlock (blockDataHeader b))-          (b :) <$> go t b'--scottyMultiAddr :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyMultiAddr = do-  setMetrics statBlockchainMultiaddr-  (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)-  best <- get_best_block-  addItemCount 1-  peers <- get_peers-  addItemCount (fromIntegral peers)-  net <- lift $ asks (storeNetwork . webStore . webConfig)-  let baddrs = toBinfoAddrs sabals sxbals xtxns-      abaddrs = toBinfoAddrs abals xbals xtxns-      recv = sum $ map getBinfoAddrReceived abaddrs-      sent' = sum $ map getBinfoAddrSent abaddrs-      txn = fromIntegral $ length ftxs-      wallet =-        BinfoWallet-          { getBinfoWalletBalance = bal,-            getBinfoWalletTxCount = txn,-            getBinfoWalletFilteredCount = txn,-            getBinfoWalletTotalReceived = recv,-            getBinfoWalletTotalSent = sent'-          }-      coin = netBinfoSymbol net-      block =-        BinfoBlockInfo-          { getBinfoBlockInfoHash = H.headerHash (blockDataHeader best),-            getBinfoBlockInfoHeight = blockDataHeight best,-            getBinfoBlockInfoTime = H.blockTimestamp (blockDataHeader best),-            getBinfoBlockInfoIndex = blockDataHeight best-          }-      info =-        BinfoInfo-          { getBinfoConnected = peers,-            getBinfoConversion = 100 * 1000 * 1000,-            getBinfoLocal = local',-            getBinfoBTC = coin,-            getBinfoLatestBlock = block-          }-  setHeaders-  streamEncoding $-    binfoMultiAddrToEncoding-      net-      BinfoMultiAddr-        { getBinfoMultiAddrAddresses = baddrs,-          getBinfoMultiAddrWallet = wallet,-          getBinfoMultiAddrTxs = ftxs,-          getBinfoMultiAddrInfo = info,-          getBinfoMultiAddrRecommendFee = True,-          getBinfoMultiAddrCashAddr = cashaddr-        }-  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 _ -> xPubSpecKey k `HashSet.member` sxpubs)-    only_show_xspecs sxpubs = HashSet.filter (\k -> xPubSpecKey k `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 xPubSpecKey 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 = (balanceAddress b, b)-          g = HashMap.fromList . map f-       in fmap g . getBalances . HashSet.toList-    get_peers = do-      ps <--        lift $-          getPeersInformation-            =<< asks (storeManager . webStore . webConfig)-      return (fromIntegral (length ps))-    compute_xabals =-      let f b = (balanceAddress (xPubBal b), xPubBal b)-       in HashMap.fromList . concatMap (map f) . HashMap.elems-    compute_bal =-      let f b = balanceAmount b + balanceZero b-       in sum . map f . HashMap.elems-    compute_abook addrs xbals =-      let f XPubSpec {..} XPubBal {..} =-            let a = balanceAddress xPubBal-                e = error "lions and tigers and bears"-                s = toSoft (listToPath xPubBalPath)-             in (a, Just (BinfoXPubPath xPubSpecKey (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 (balanceAddress . xPubBal)-       in HashSet.fromList . concatMap f . HashMap.elems--getBinfoCount :: (MonadUnliftIO m, MonadLoggerIO m) => TL.Text -> WebT m Int-getBinfoCount str = do-  d <- lift (asks (maxLimitDefault . webMaxLimits . webConfig))-  x <- lift (asks (maxLimitFull . webMaxLimits . webConfig))-  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 (maxLimitOffset . webMaxLimits . webConfig))-  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 statBlockchainRawaddr-    >> 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 (storeNetwork . webStore . webConfig)-      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 =-            xPubSummaryConfirmed summary-              + xPubSummaryZero summary-      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-              { binfoRawAddr = BinfoXpub xpub,-                binfoRawBalance = amnt,-                binfoRawTxCount = fromIntegral $ length txs,-                binfoRawUnredeemed = xPubUnspentCount summary,-                binfoRawReceived = xPubSummaryReceived summary,-                binfoRawSent =-                  fromIntegral (xPubSummaryReceived summary)-                    - fromIntegral amnt,-                binfoRawTxs = txs-              }-      setHeaders-      streamEncoding $ binfoRawAddrToEncoding net ra-    compute_abook xpub xbals =-      let f XPubBal {..} =-            let a = balanceAddress xPubBal-                e = error "black hole swallows all your code"-                s = toSoft (listToPath xPubBalPath)-                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 (storeNetwork . webStore . webConfig)-      let abook = HashMap.singleton addr Nothing-          xspecs = HashSet.empty-          saddrs = HashSet.singleton addr-          bfilter = BinfoFilterAll-          amnt = balanceAmount bal + balanceZero bal-      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-              { binfoRawAddr = BinfoAddr addr,-                binfoRawBalance = amnt,-                binfoRawTxCount = balanceTxCount bal,-                binfoRawUnredeemed = balanceUnspentCount bal,-                binfoRawReceived = balanceTotalReceived bal,-                binfoRawSent =-                  fromIntegral (balanceTotalReceived bal)-                    - fromIntegral amnt,-                binfoRawTxs = txs-              }-      setHeaders-      streamEncoding $ binfoRawAddrToEncoding net ra--scottyBinfoReceived :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoReceived = do-  setMetrics statBlockchainQgetreceivedbyaddress-  a <- getAddress "addr"-  b <- fromMaybe (zeroBalance a) <$> getBalance a-  setHeaders-  addItemCount 1-  S.text . cs . show $ balanceTotalReceived b--scottyBinfoSent :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoSent = do-  setMetrics statBlockchainQgetsentbyaddress-  a <- getAddress "addr"-  b <- fromMaybe (zeroBalance a) <$> getBalance a-  setHeaders-  addItemCount 1-  S.text . cs . show $ balanceTotalReceived b - balanceAmount b - balanceZero b--scottyBinfoAddrBalance :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoAddrBalance = do-  setMetrics statBlockchainQaddressbalance-  a <- getAddress "addr"-  b <- fromMaybe (zeroBalance a) <$> getBalance a-  setHeaders-  addItemCount 1-  S.text . cs . show $ balanceAmount b + balanceZero b--scottyFirstSeen :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyFirstSeen = do-  setMetrics statBlockchainQaddressfirstseen-  a <- getAddress "addr"-  ch <- lift $ asks (storeChain . webStore . webConfig)-  bb <- chainGetBest ch-  let top = H.nodeHeight bb-      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 =-      H.blockTimestamp . H.nodeHeader . 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 statBlockchainBalance-  (xspecs, addrs) <- getBinfoActive-  cashaddr <- getCashAddr-  net <- lift $ asks (storeNetwork . webStore . webConfig)-  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-        { binfoShortBalFinal = balanceAmount + balanceZero,-          binfoShortBalTxCount = balanceTxCount,-          binfoShortBalReceived = balanceTotalReceived-        }-    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 {xPubBalPath = 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 (balanceAmount . xPubBal) xbals-          zro = sum $ map (balanceZero . xPubBal) xbals-          exs = filter is_ext xbals-          rcv = sum $ map (balanceTotalReceived . xPubBal) exs-          sbl =-            BinfoShortBal-              { binfoShortBalFinal = val + zro,-                binfoShortBalTxCount = fromIntegral xts,-                binfoShortBalReceived = rcv-              }-      return (xPubExport net (xPubSpecKey xpub), 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 statBlockchainBlockHeight-  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 (storeNetwork . webStore . webConfig)-  streamEncoding $ binfoBlocksToEncoding net 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 (blockDataHeader block_header)-          get_prev = H.prevBlock . blockDataHeader-          get_hash = H.headerHash . blockDataHeader-      txs <- lift $ mapM get_tx (blockDataTxs block_header)-      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 statBlockchainLatestblock-  best <- get_best_block-  let binfoTxIndices = map (encodeBinfoTxId numtxid) (blockDataTxs best)-      binfoHeaderHash = H.headerHash (blockDataHeader best)-      binfoHeaderTime = H.blockTimestamp (blockDataHeader best)-      binfoHeaderIndex = binfoHeaderTime-      binfoHeaderHeight = blockDataHeight best-  addItemCount 1-  streamEncoding $ toEncoding BinfoHeader {..}-  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 statBlockchainRawblock-  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 (blockDataTxs b)-          addItemCount (length txs)-          let my_hash = H.headerHash (blockDataHeader b)-              get_prev = H.prevBlock . blockDataHeader-              get_hash = H.headerHash . blockDataHeader-          nxt_headers <--            fmap catMaybes $-              mapM getBlock-                =<< getBlocksAtHeight (blockDataHeight b + 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 (blockDataHeader b) (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 (storeNetwork . webStore . webConfig)-              streamEncoding $ binfoBlockToEncoding net 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 statBlockchainRawtx-  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 (storeNetwork . webStore . webConfig)-      setHeaders-      streamEncoding $ binfoTxToEncoding net $ 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 statBlockchainQtxtotalbtcoutput-  tx <--    getBinfoTx txid >>= \case-      Right t -> return t-      Left e -> raise e-  addItemCount 1-  S.text . cs . show . sum . map outputAmount $ transactionOutputs tx--scottyBinfoTxFees :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoTxFees = do-  txid <- S.param "txid"-  setMetrics statBlockchainQtxfee-  tx <--    getBinfoTx txid >>= \case-      Right t -> return t-      Left e -> raise e-  let i =-        sum . map inputAmount . filter f $-          transactionInputs tx-      o = sum . map outputAmount $ transactionOutputs tx-  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 statBlockchainQtxresult-  tx <--    getBinfoTx txid >>= \case-      Right t -> return t-      Left e -> raise e-  let i =-        toInteger . sum . map inputAmount . filter (f addr) $-          transactionInputs tx-      o =-        toInteger . sum . map outputAmount . filter (g addr) $-          transactionOutputs tx-  addItemCount 1-  S.text . cs . show $ o - i-  where-    f addr StoreInput {inputAddress = Just a} = a == addr-    f _ _ = False-    g addr StoreOutput {outputAddr = Just a} = a == addr-    g _ _ = False--scottyBinfoTotalInput :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoTotalInput = do-  txid <- S.param "txid"-  setMetrics statBlockchainQtxtotalbtcinput-  tx <--    getBinfoTx txid >>= \case-      Right t -> return t-      Left e -> raise e-  addItemCount 1-  S.text . cs . show . sum . map inputAmount . filter f $ transactionInputs tx-  where-    f StoreInput {} = True-    f StoreCoinbase {} = False--scottyBinfoMempool :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoMempool = do-  setMetrics statBlockchainMempool-  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 (storeNetwork . webStore . webConfig)-  setHeaders-  let mem = BinfoMempool $ map (toBinfoTxSimple numtxid) txs-  addItemCount (length txs)-  streamEncoding $ binfoMempoolToEncoding net mem--scottyBinfoGetBlockCount :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoGetBlockCount = do-  setMetrics statBlockchainQgetblockcount-  ch <- asks (storeChain . webStore . webConfig)-  bn <- chainGetBest ch-  setHeaders-  addItemCount 1-  S.text . cs . show $ H.nodeHeight bn--scottyBinfoLatestHash :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoLatestHash = do-  setMetrics statBlockchainQlatesthash-  ch <- asks (storeChain . webStore . webConfig)-  bn <- chainGetBest ch-  setHeaders-  addItemCount 1-  S.text . TL.fromStrict . H.blockHashToHex . H.headerHash $ H.nodeHeader bn--scottyBinfoSubsidy :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoSubsidy = do-  setMetrics statBlockchainQbcperblock-  ch <- asks (storeChain . webStore . webConfig)-  net <- asks (storeNetwork . webStore . webConfig)-  bn <- chainGetBest ch-  setHeaders-  addItemCount 1-  S.text . cs . show . (/ (100 * 1000 * 1000 :: Double)) . fromIntegral $-    H.computeSubsidy net (H.nodeHeight bn + 1)--scottyBinfoAddrToHash :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoAddrToHash = do-  setMetrics statBlockchainQaddresstohash-  addr <- getAddress "addr"-  setHeaders-  addItemCount 1-  S.text . encodeHexLazy . runPutL . serialize $ getAddrHash160 addr--scottyBinfoHashToAddr :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyBinfoHashToAddr = do-  setMetrics statBlockchainQhashtoaddress-  bs <- maybe S.next return . decodeHex =<< S.param "hash"-  net <- asks (storeNetwork . webStore . webConfig)-  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 statBlockchainQaddrpubkey-  hex <- S.param "pubkey"-  pubkey <--    maybe S.next (return . pubKeyAddr) $-      eitherToMaybe . runGetS deserialize =<< decodeHex hex-  net <- lift $ asks (storeNetwork . webStore . webConfig)-  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 statBlockchainQpubkeyaddr-  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 $-        streamThings-          ( \l -> do-              ts <- getAddressTxs addr l-              counter (length ts)-              return ts-          )-          (Just txRefHash)-          def {limit = 8}-          .| concatMapMC (getTransaction . txRefHash)-          .| iterMC (\_ -> counter 1)-          .| concatMapC (filter (inp addr) . transactionInputs)-          .| headC-    inp addr StoreInput {inputAddress = Just a} = a == addr-    inp _ _ = False-    extr addr StoreInput {inputSigScript, inputPkScript, inputWitness} = do-      Script sig <- decode inputSigScript-      Script pks <- decode inputPkScript-      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 inputWitness 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 statBlockchainQhashpubkey-  pkm <- (eitherToMaybe . runGetS deserialize <=< decodeHex) <$> S.param "pubkey"-  addr <- case pkm of-    Nothing -> raise $ UserError "Could not decode public key"-    Just pk -> return $ pubKeyAddr pk-  setHeaders-  addItemCount 1-  S.text . encodeHexLazy . runPutL . serialize $ getAddrHash160 addr---- GET Network Information ----scottyPeers ::-  (MonadUnliftIO m, MonadLoggerIO m) =>-  GetPeers ->-  WebT m [PeerInformation]-scottyPeers _ = do-  setMetrics statPeers-  ps <--    lift $-      getPeersInformation-        =<< asks (storeManager . webStore . webConfig)-  addItemCount (length ps)-  return ps---- | Obtain information about connected peers from peer manager process.-getPeersInformation ::-  MonadLoggerIO m => PeerManager -> m [PeerInformation]-getPeersInformation mgr =-  mapMaybe toInfo <$> getPeers mgr-  where-    toInfo op = do-      ver <- onlinePeerVersion op-      let as = onlinePeerAddress op-          ua = getVarString $ userAgent ver-          vs = version ver-          sv = services ver-          rl = relay ver-      return-        PeerInformation-          { peerUserAgent = ua,-            peerAddress = show as,-            peerVersion = vs,-            peerServices = sv,-            peerRelay = rl-          }--scottyHealth ::-  (MonadUnliftIO m, MonadLoggerIO m) => GetHealth -> WebT m HealthCheck-scottyHealth _ = do-  setMetrics statHealth-  h <- asks webHealthCheck >>= 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 = storeChain $ webStore cfg-      blockHealthMaxDiff = fromIntegral $ webMaxDiff cfg-  blockHealthHeaders <--    H.nodeHeight <$> chainGetBest ch-  blockHealthBlocks <--    maybe 0 blockDataHeight-      <$> runMaybeT (MaybeT getBestBlock >>= MaybeT . getBlock)-  return BlockHealth {..}--lastBlockHealthCheck ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>-  Chain ->-  WebTimeouts ->-  m TimeHealth-lastBlockHealthCheck ch tos = do-  n <- fromIntegral . systemSeconds <$> liftIO getSystemTime-  t <- fromIntegral . H.blockTimestamp . H.nodeHeader <$> chainGetBest ch-  let timeHealthAge = n - t-      timeHealthMax = fromIntegral $ blockTimeout tos-  return TimeHealth {..}--lastTxHealthCheck ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>-  WebConfig ->-  m TimeHealth-lastTxHealthCheck WebConfig {..} = do-  n <- fromIntegral . systemSeconds <$> liftIO getSystemTime-  b <- fromIntegral . H.blockTimestamp . H.nodeHeader <$> chainGetBest ch-  t <--    getMempool >>= \case-      t : _ ->-        let x = fromIntegral $ fst t-         in return $ max x b-      [] -> return b-  let timeHealthAge = n - t-      timeHealthMax = fromIntegral to-  return TimeHealth {..}-  where-    ch = storeChain webStore-    to =-      if webNoMempool-        then blockTimeout webTimeouts-        else txTimeout webTimeouts--pendingTxsHealthCheck ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>-  WebConfig ->-  m MaxHealth-pendingTxsHealthCheck cfg = do-  let maxHealthMax = fromIntegral $ webMaxPending cfg-  maxHealthNum <--    fromIntegral-      <$> blockStorePendingTxs (storeBlock (webStore cfg))-  return MaxHealth {..}--peerHealthCheck ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>-  PeerManager ->-  m CountHealth-peerHealthCheck mgr = do-  let countHealthMin = 1-  countHealthNum <- fromIntegral . length <$> getPeers mgr-  return CountHealth {..}--healthCheck ::-  (MonadUnliftIO m, MonadLoggerIO m, StoreReadBase m) =>-  WebConfig ->-  m HealthCheck-healthCheck cfg@WebConfig {..} = do-  healthBlocks <- blockHealthCheck cfg-  healthLastBlock <- lastBlockHealthCheck (storeChain webStore) webTimeouts-  healthLastTx <- lastTxHealthCheck cfg-  healthPendingTxs <- pendingTxsHealthCheck cfg-  healthPeers <- peerHealthCheck (storeManager webStore)-  let healthNetwork = getNetworkName (storeNetwork webStore)-      healthVersion = webVersion-      hc = HealthCheck {..}-  unless (isOK hc) $ do-    let t = toStrict $ encodeToLazyText hc-    $(logErrorS) "Web" $ "Health check failed: " <> t-  return hc--scottyDbStats :: (MonadUnliftIO m, MonadLoggerIO m) => WebT m ()-scottyDbStats = do-  setMetrics statDbstats-  setHeaders-  db <- lift $ asks (databaseHandle . storeDB . webStore . webConfig)-  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 (storeNetwork . webStore . webConfig)-      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 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 = case B16.decodeBase16 $ C.filter (not . isSpace) b of-      Right x -> bin x-      Left s -> Left (T.unpack s)--parseOffset :: MonadIO m => WebT m OffsetParam-parseOffset = do-  res@(OffsetParam o) <- paramDef-  limits <- lift $ asks (webMaxLimits . webConfig)-  when (maxLimitOffset limits > 0 && fromIntegral o > maxLimitOffset limits) $-    raise . UserError $-      "offset exceeded: " <> show o <> " > " <> show (maxLimitOffset limits)-  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 {startParamHash = h} -> start_tx h <|> start_block h-      StartParamHeight {startParamHeight = h} -> start_height h-      StartParamTime {startParamTime = q} -> start_time q-  where-    start_height h = return $ AtBlock $ fromIntegral h-    start_block h = do-      b <- MaybeT $ getBlock (H.BlockHash h)-      return $ AtBlock (blockDataHeight b)-    start_tx h = do-      _ <- MaybeT $ getTxData (TxHash h)-      return $ AtTx (TxHash h)-    start_time q = do-      ch <- lift $ asks (storeChain . webStore . webConfig)-      b <- MaybeT $ blockAtOrBefore ch q-      let g = blockDataHeight b-      return $ AtBlock g--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 (webMaxLimits . webConfig)-  Limits (validateLimit wl full limitM) (fromIntegral o) <$> parseStart startM--validateLimit :: WebLimits -> Bool -> Maybe LimitParam -> Word32-validateLimit wl full limitM =-  f m $ maybe d (fromIntegral . getLimitParam) limitM-  where-    m-      | full && maxLimitFull wl > 0 = maxLimitFull wl-      | otherwise = maxLimitCount wl-    d = maxLimitDefault wl-    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 (storeDB . webStore . webConfig)-  mc <- asks (storeCache . webStore . webConfig)-  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 {webConfig = h (webConfig s)}-    h c = c {webStore = i (webStore c)}-    i s = s {storeCache = 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 (statKey m) 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 (statKey m) (vault req)-          add_stat diff (statAll m)-          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+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE ImportQualifiedPost #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE NoFieldSelectors #-}+{-# OPTIONS_GHC -Wno-deprecations #-}++module Haskoin.Store.Web+  ( -- * Web+    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
test/Haskoin/Store/CacheSpec.hs view
@@ -29,7 +29,7 @@     b = do       h <- choose (0, 0x07ffffff)       p <- choose (0, 0x03ffffff)-      return BlockRef {blockRefHeight = h, blockRefPos = p}+      return BlockRef {height = h, position = p}     m = do       t <- choose (0, 0x001fffffffffffff)-      return MemRef {memRefTime = t}+      return MemRef {timestamp = t}
test/Haskoin/StoreSpec.hs view
@@ -1,6 +1,11 @@+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE ImportQualifiedPost #-} {-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE NoFieldSelectors #-}  module Haskoin.StoreSpec (spec) where @@ -9,7 +14,7 @@ import Control.Monad.Logger import Control.Monad.Reader import Data.ByteString (ByteString)-import qualified Data.ByteString as B+import Data.ByteString qualified as B import Data.ByteString.Base64 import Data.Either import Data.List@@ -37,10 +42,10 @@   }  spec :: Spec-spec = do+spec = prepareContext $ \ctx -> do   describe "Download" $ do     it "gets 8 blocks" $-      withTestStore bchRegTest "eight-blocks" $ \TestStore {..} -> do+      withTestStore bchRegTest ctx "eight-blocks" $ \TestStore {..} -> do         bs <- replicateM 8 . receiveMatch testStoreEvents $ \case           StoreBestBlock b -> Just b           _ -> Nothing@@ -48,10 +53,10 @@         bestNodeM <- chainGetBlock bestHash testStoreChain         bestNodeM `shouldSatisfy` isJust         let bestNode = fromJust bestNodeM-            bestHeight = nodeHeight bestNode+            bestHeight = bestNode.height         bestHeight `shouldBe` 8     it "get a block and its transactions" $-      withTestStore bchRegTest "get-block-txs" $ \TestStore {..} ->+      withTestStore bchRegTest ctx "get-block-txs" $ \TestStore {..} ->         flip runReaderT testStoreDB $ do           let h1 = "5369ef2386c72acdf513ffd80aeba2a1774e2f004d120761e54a8bf614173f3e"               get_the_block h =@@ -59,22 +64,27 @@                   StoreBestBlock b                     | h <= 1 -> return b                     | otherwise ->-                      get_the_block ((h :: Int) - 1)+                        get_the_block ((h :: Int) - 1)                   _ -> get_the_block h           bh <- get_the_block 15           m <- getBlock bh           let bd = fromMaybe (error "Could not get block") m           t1 <- getTransaction h1           lift $ do-            blockDataHeight bd `shouldBe` 15-            length (blockDataTxs bd) `shouldBe` 1-            head (blockDataTxs bd) `shouldBe` h1+            bd.height `shouldBe` 15+            length bd.txs `shouldBe` 1+            head bd.txs `shouldBe` h1             t1 `shouldSatisfy` isJust             txHash (transactionData (fromJust t1)) `shouldBe` h1  withTestStore ::-  MonadUnliftIO m => Network -> String -> (TestStore -> m a) -> m a-withTestStore net t f =+  (MonadUnliftIO m) =>+  Network ->+  Ctx ->+  String ->+  (TestStore -> m a) ->+  m a+withTestStore net ctx t f =   withSystemTempDirectory ("haskoin-store-test-" <> t <> "-") $ \w ->     runNoLoggingT $ do       let ad =@@ -83,33 +93,34 @@               (sockToHostAddress (SockAddrInet 0 0))           cfg =             StoreConfig-              { storeConfMaxPeers = 20,-                storeConfInitPeers = [],-                storeConfDiscover = True,-                storeConfDB = w,-                storeConfNetwork = net,-                storeConfCache = Nothing,-                storeConfGap = gap,-                storeConfInitialGap = 20,-                storeConfCacheMin = 100,-                storeConfMaxKeys = 100 * 1000 * 1000,-                storeConfNoMempool = False,-                storeConfWipeMempool = False,-                storeConfSyncMempool = False,-                storeConfPeerTimeout = 60,-                storeConfPeerMaxLife = 48 * 3600,-                storeConfConnect = dummyPeerConnect net ad,-                storeConfStats = Nothing,-                storeConfCacheMempoolSync = 30+              { maxPeers = 20,+                initPeers = [],+                discover = True,+                db = w,+                net = net,+                ctx = ctx,+                redis = Nothing,+                gap = gap,+                initGap = 20,+                redisMinAddrs = 100,+                redisMaxKeys = 100 * 1000 * 1000,+                noMempool = False,+                wipeMempool = False,+                syncMempool = False,+                peerTimeout = 60,+                maxPeerLife = 48 * 3600,+                connect = dummyPeerConnect net ad,+                statsStore = Nothing,+                redisSyncInterval = 30               }       withStore cfg $ \Store {..} ->-        withSubscription storePublisher $ \sub ->+        withSubscription pub $ \sub ->           lift $             f               TestStore-                { testStoreDB = storeDB,-                  testStoreBlockStore = storeBlock,-                  testStoreChain = storeChain,+                { testStoreDB = db,+                  testStoreBlockStore = block,+                  testStoreChain = chain,                   testStoreEvents = sub                 } @@ -194,7 +205,9 @@           ver = buildVersion net nonce 0 ad rmt now       runPut (putMessage net (MVersion ver)) `send` s       runConduit $-        forever (receive r >>= yield) .| inc .| concatMapC mockPeerReact+        forever (receive r >>= yield)+          .| inc+          .| concatMapC mockPeerReact           .| outc           .| awaitForever (`send` s)     outc = mapMC $ \msg' -> return $ runPut (putMessage net msg')@@ -216,11 +229,11 @@ mockPeerReact (MVersion _) = [MVerAck] mockPeerReact (MGetHeaders (GetHeaders _ _hs _)) = [MHeaders (Headers hs')]   where-    f b = (blockHeader b, VarInt (fromIntegral (length (blockTxns b))))+    f b = (b.header, VarInt $ fromIntegral $ length b.txs)     hs' = map f allBlocks mockPeerReact (MGetData (GetData ivs)) = mapMaybe f ivs   where     f (InvVector InvBlock h) = MBlock <$> find (l h) allBlocks     f _ = Nothing-    l h b = headerHash (blockHeader b) == BlockHash h+    l h b = headerHash b.header == BlockHash h mockPeerReact _ = []