diff --git a/explorer/src/Pos/Explorer/Aeson/ClientTypes.hs b/explorer/src/Pos/Explorer/Aeson/ClientTypes.hs index cc677f9d509..d035fa7a33e 100644 --- a/explorer/src/Pos/Explorer/Aeson/ClientTypes.hs +++ b/explorer/src/Pos/Explorer/Aeson/ClientTypes.hs @@ -17,8 +17,9 @@ import Data.Fixed (showFixed) import Pos.Explorer.Web.ClientTypes (CAda (..), CAddress, CAddressSummary, CAddressType, CBlockEntry, CBlockSummary, - CCoin, CGenesisAddressInfo, CGenesisSummary, CHash, - CNetworkAddress, CTxBrief, CTxEntry, CTxId, CTxSummary) + CByteString (..), CCoin, CGenesisAddressInfo, + CGenesisSummary, CHash, CNetworkAddress, CTxBrief, + CTxEntry, CTxId, CTxSummary, CUtxo) import Pos.Explorer.Web.Error (ExplorerError) deriveJSON defaultOptions ''CHash @@ -37,6 +38,10 @@ deriveToJSON defaultOptions ''CNetworkAddress deriveToJSON defaultOptions ''CTxSummary deriveToJSON defaultOptions ''CGenesisSummary deriveToJSON defaultOptions ''CGenesisAddressInfo +deriveToJSON defaultOptions ''CUtxo + +instance ToJSON CByteString where + toJSON (CByteString bs) = (toJSON.toString) bs instance ToJSON CAda where -- https://github.com/bos/aeson/issues/227#issuecomment-245400284 diff --git a/explorer/src/Pos/Explorer/Web/Api.hs b/explorer/src/Pos/Explorer/Web/Api.hs index 1f013b34758..f9fd6079cee 100644 --- a/explorer/src/Pos/Explorer/Web/Api.hs +++ b/explorer/src/Pos/Explorer/Web/Api.hs @@ -15,7 +15,8 @@ import Universum import Control.Exception.Safe (try) import Data.Proxy (Proxy (Proxy)) -import Servant.API ((:>), Capture, Get, JSON, QueryParam, Summary) +import Servant.API ((:>), Capture, Get, JSON, Post, QueryParam, + ReqBody, Summary) import Servant.Generic ((:-), AsApi, ToServant) import Servant.Server (ServantErr (..)) @@ -23,7 +24,7 @@ import Pos.Core (EpochIndex) import Pos.Explorer.Web.ClientTypes (Byte, CAda, CAddress, CAddressSummary, CAddressesFilter, CBlockEntry, CBlockSummary, CGenesisAddressInfo, CGenesisSummary, - CHash, CTxBrief, CTxEntry, CTxId, CTxSummary) + CHash, CTxBrief, CTxEntry, CTxId, CTxSummary, CUtxo) import Pos.Explorer.Web.Error (ExplorerError) import Pos.Util.Servant (DQueryParam, ModifiesApiRes (..), VerbMod) @@ -114,6 +115,14 @@ data ExplorerApiRecord route = ExplorerApiRecord :> Capture "address" CAddress :> ExRes Get CAddressSummary + , _addressUtxoBulk :: route + :- Summary "Get summary information about multiple addresses." + :> "bulk" + :> "addresses" + :> "utxo" + :> ReqBody '[JSON] [CAddress] + :> ExRes Post [CUtxo] + , _epochPages :: route :- Summary "Get epoch pages, all the paged slots in the epoch." :> "epochs" diff --git a/explorer/src/Pos/Explorer/Web/ClientTypes.hs b/explorer/src/Pos/Explorer/Web/ClientTypes.hs index 19db0a29dab..790924bba54 100644 --- a/explorer/src/Pos/Explorer/Web/ClientTypes.hs +++ b/explorer/src/Pos/Explorer/Web/ClientTypes.hs @@ -17,6 +17,7 @@ module Pos.Explorer.Web.ClientTypes , CAddressType (..) , CAddressSummary (..) , CTxBrief (..) + , CUtxo (..) , CNetworkAddress (..) , CTxSummary (..) , CGenesisSummary (..) @@ -29,6 +30,7 @@ module Pos.Explorer.Web.ClientTypes , LocalSlotIndex (..) , StakeholderId , Byte + , CByteString (..) , mkCCoin , mkCCoinMB , toCHash @@ -87,6 +89,8 @@ import Pos.Explorer.Core (TxExtra (..)) import Pos.Explorer.ExplorerMode (ExplorerMode) import Pos.Explorer.ExtraContext (HasExplorerCSLInterface (..)) import Pos.Explorer.TestUtil (secretKeyToAddress) + + ------------------------------------------------------------------------------------- -- Hash types ------------------------------------------------------------------------------------- @@ -308,6 +312,18 @@ data CTxBrief = CTxBrief , ctbOutputSum :: !CCoin } deriving (Show, Generic) +data CUtxo = CUtxo + { cuId :: !CTxId + , cuOutIndex :: !Int + , cuAddress :: !CAddress + , cuCoins :: !CCoin + } + | CUtxoUnknown + { cuTag :: !Int + , cuBs :: !CByteString + } + deriving (Show, Generic) + newtype CNetworkAddress = CNetworkAddress Text deriving (Show, Generic) @@ -431,6 +447,12 @@ sumCoinOfInputsOutputs addressListMB mkCCoin $ mkCoin $ fromIntegral $ sum addressCoinList | otherwise = mkCCoinMB Nothing +newtype CByteString = CByteString ByteString + deriving (Generic) + +instance Show CByteString where + show (CByteString bs) = (show . toString) bs + -------------------------------------------------------------------------------- -- Arbitrary instances -------------------------------------------------------------------------------- diff --git a/explorer/src/Pos/Explorer/Web/Server.hs b/explorer/src/Pos/Explorer/Web/Server.hs index 6c779b291f9..5c245775a31 100644 --- a/explorer/src/Pos/Explorer/Web/Server.hs +++ b/explorer/src/Pos/Explorer/Web/Server.hs @@ -44,7 +44,8 @@ import Network.Wai.Middleware.RequestLogger (logStdoutDev) import qualified Serokell.Util.Base64 as B64 import Servant.Generic (AsServerT, toServant) -import Servant.Server (Server, ServerT, serve) +import Servant.Server (Server, ServerT, err405, errReasonPhrase, + serve) import System.Wlog (logDebug) import Pos.Crypto (WithHash (..), hash, redeemPkBuild, withHash) @@ -65,10 +66,10 @@ import Pos.Core (AddrType (..), Address (..), Coin, EpochIndex, import Pos.Core.Block (Block, HeaderHash, MainBlock, gbHeader, gbhConsensus, mainBlockSlot, mainBlockTxPayload, mcdSlot) import Pos.Core.Chrono (NewestFirst (..)) -import Pos.Core.Txp (Tx (..), TxAux, TxId, TxOutAux (..), taTx, - txOutValue, txpTxs, _txOutputs) -import Pos.DB.Txp (MonadTxpMem, getLocalTxs, getMemPool, - withTxpLocalData) +import Pos.Core.Txp (Tx (..), TxAux, TxId, TxIn (..), TxOutAux (..), + taTx, txOutAddress, txOutValue, txpTxs, _txOutputs) +import Pos.DB.Txp (MonadTxpMem, getFilteredUtxo, getLocalTxs, + getMemPool, withTxpLocalData) import Pos.Infra.Slotting (MonadSlots (..), getSlotStart) import Pos.Util (divRoundUp, maybeThrow) import Pos.Web (serveImpl) @@ -86,15 +87,18 @@ import Pos.Explorer.Web.Api (ExplorerApi, ExplorerApiRecord (..), import Pos.Explorer.Web.ClientTypes (Byte, CAda (..), CAddress (..), CAddressSummary (..), CAddressType (..), CAddressesFilter (..), CBlockEntry (..), - CBlockSummary (..), CGenesisAddressInfo (..), - CGenesisSummary (..), CHash, CTxBrief (..), CTxEntry (..), - CTxId (..), CTxSummary (..), TxInternal (..), - convertTxOutputs, convertTxOutputsMB, fromCAddress, - fromCHash, fromCTxId, getEpochIndex, getSlotIndex, - mkCCoin, mkCCoinMB, tiToTxEntry, toBlockEntry, - toBlockSummary, toCAddress, toCHash, toCTxId, toTxBrief) + CBlockSummary (..), CByteString (..), + CGenesisAddressInfo (..), CGenesisSummary (..), CHash, + CTxBrief (..), CTxEntry (..), CTxId (..), CTxSummary (..), + CUtxo (..), TxInternal (..), convertTxOutputs, + convertTxOutputsMB, fromCAddress, fromCHash, fromCTxId, + getEpochIndex, getSlotIndex, mkCCoin, mkCCoinMB, + tiToTxEntry, toBlockEntry, toBlockSummary, toCAddress, + toCHash, toCTxId, toTxBrief) import Pos.Explorer.Web.Error (ExplorerError (..)) +import qualified Data.Map as M +import Pos.Configuration (explorerExtendedApi) ---------------------------------------------------------------- @@ -132,6 +136,7 @@ explorerHandlers _diffusion = , _txsLast = getLastTxs , _txsSummary = getTxSummary , _addressSummary = getAddressSummary + , _addressUtxoBulk = getAddressUtxoBulk , _epochPages = getEpochPage , _epochSlots = getEpochSlot , _genesisSummary = getGenesisSummary @@ -371,6 +376,42 @@ getAddressSummary cAddr = do ATRedeem -> CRedeemAddress ATUnknown {} -> CUnknownAddress + +getAddressUtxoBulk + :: (ExplorerMode ctx m) + => [CAddress] + -> m [CUtxo] +getAddressUtxoBulk cAddrs = do + unless explorerExtendedApi $ + throwM err405 + { errReasonPhrase = "Explorer extended API is disabled by configuration!" + } + + let nAddrs = length cAddrs + + when (nAddrs > 10) $ + throwM err405 + { errReasonPhrase = "Maximum number of addresses you can send to fetch Utxo in bulk is 10!" + } + + addrs <- mapM cAddrToAddr cAddrs + utxo <- getFilteredUtxo addrs + + pure . map futxoToCUtxo . M.toList $ utxo + where + futxoToCUtxo :: (TxIn, TxOutAux) -> CUtxo + futxoToCUtxo ((TxInUtxo txInHash txInIndex), txOutAux) = CUtxo { + cuId = toCTxId txInHash, + cuOutIndex = fromIntegral txInIndex, + cuAddress = toCAddress . txOutAddress . toaOut $ txOutAux, + cuCoins = mkCCoin . txOutValue . toaOut $ txOutAux + } + futxoToCUtxo ((TxInUnknown tag bs), _) = CUtxoUnknown { + cuTag = fromIntegral tag, + cuBs = CByteString bs + } + + -- | Get transaction summary from transaction id. Looks at both the database -- and the memory (mempool) for the transaction. What we have at the mempool -- are transactions that have to be written in the blockchain. diff --git a/explorer/src/Pos/Explorer/Web/TestServer.hs b/explorer/src/Pos/Explorer/Web/TestServer.hs index 59273b4d9c9..1e58bdb1ed7 100644 --- a/explorer/src/Pos/Explorer/Web/TestServer.hs +++ b/explorer/src/Pos/Explorer/Web/TestServer.hs @@ -24,10 +24,12 @@ import Pos.Explorer.Web.ClientTypes (Byte, CAda (..), CAddress (..), CAddressesFilter (..), CBlockEntry (..), CBlockSummary (..), CGenesisAddressInfo (..), CGenesisSummary (..), CHash (..), CTxBrief (..), - CTxEntry (..), CTxId (..), CTxSummary (..), mkCCoin) + CTxEntry (..), CTxId (..), CTxSummary (..), CUtxo (..), + mkCCoin) import Pos.Explorer.Web.Error (ExplorerError (..)) import Pos.Web () + ---------------------------------------------------------------- -- Top level functionality ---------------------------------------------------------------- @@ -54,6 +56,7 @@ explorerHandlers = , _txsLast = testTxsLast , _txsSummary = testTxsSummary , _addressSummary = testAddressSummary + , _addressUtxoBulk = testAddressUtxoBulk , _epochPages = testEpochPageSearch , _epochSlots = testEpochSlotSearch , _genesisSummary = testGenesisSummary @@ -185,6 +188,16 @@ testAddressSummary -> Handler CAddressSummary testAddressSummary _ = pure sampleAddressSummary +testAddressUtxoBulk + :: [CAddress] + -> Handler [CUtxo] +testAddressUtxoBulk _ = pure [CUtxo + { cuId = CTxId $ CHash "8aac4a6b18fafa2783071c66519332157ce96c67e88fc0cc3cb04ba0342d12a1" + , cuOutIndex = 0 + , cuAddress = CAddress "19F6U1Go5B4KakVoCZfzCtqNAWhUBprxVzL3JsGu74TEwQnXPvAKPUbvG8o4Qe5RaY8Z7WKLfxmNFwBqPV1NQ2hRpKkdEN" + , cuCoins = mkCCoin $ mkCoin 3 + }] + testEpochSlotSearch :: EpochIndex -> Word16 diff --git a/explorer/src/documentation/Main.hs b/explorer/src/documentation/Main.hs index 7904b1c883d..ab33cc0ac2d 100644 --- a/explorer/src/documentation/Main.hs +++ b/explorer/src/documentation/Main.hs @@ -27,8 +27,9 @@ import Control.Lens (mapped, (?~)) import Data.Aeson (encode) import qualified Data.ByteString.Lazy.Char8 as BSL8 import Data.Fixed (Fixed (..), Micro) -import Data.Swagger (Swagger, ToParamSchema (..), ToSchema (..), - declareNamedSchema, defaultSchemaOptions, description, +import Data.Swagger (NamedSchema (..), Swagger, ToParamSchema (..), + ToSchema (..), binarySchema, declareNamedSchema, + defaultSchemaOptions, description, genericDeclareNamedSchema, host, info, name, title, version) import Data.Typeable (Typeable, typeRep) @@ -85,6 +86,7 @@ instance ToParamSchema C.EpochIndex instance ToSchema C.CTxSummary instance ToSchema C.CTxEntry instance ToSchema C.CTxBrief +instance ToSchema C.CUtxo instance ToSchema C.CBlockSummary instance ToSchema C.CBlockEntry instance ToSchema C.CAddressType @@ -100,6 +102,9 @@ instance ToParamSchema C.CAddressesFilter deriving instance Generic Micro +instance ToSchema C.CByteString where + declareNamedSchema _ = return $ NamedSchema (Just "CByteString") binarySchema + -- | Instance for Either-based types (types we return as 'Right') in responses. -- Due 'typeOf' these types must be 'Typeable'. -- We need this instance for correct Swagger-specification. diff --git a/lib/configuration.yaml b/lib/configuration.yaml index 28cc4de3aca..3517171add1 100644 --- a/lib/configuration.yaml +++ b/lib/configuration.yaml @@ -112,6 +112,7 @@ dev: &dev pendingTxResubmissionPeriod: 7 # seconds walletProductionApi: false walletTxCreationDisabled: false + explorerExtendedApi: false tls: &dev_tls ca: @@ -310,6 +311,7 @@ mainnet_base: &mainnet_base pendingTxResubmissionPeriod: 7 # seconds walletProductionApi: true walletTxCreationDisabled: false + explorerExtendedApi: false tls: &mainnet_base_tls ca: diff --git a/lib/src/Pos/Configuration.hs b/lib/src/Pos/Configuration.hs index 232fc7b56b0..cf53dc965e8 100644 --- a/lib/src/Pos/Configuration.hs +++ b/lib/src/Pos/Configuration.hs @@ -17,6 +17,9 @@ module Pos.Configuration , pendingTxResubmitionPeriod , walletProductionApi , walletTxCreationDisabled + + -- * Explorer constants + , explorerExtendedApi ) where import Universum @@ -52,6 +55,9 @@ data NodeConfiguration = NodeConfiguration , ccWalletTxCreationDisabled :: !Bool -- ^ Disallow transaction creation or re-submission of -- pending transactions by the wallet + , ccExplorerExtendedApi :: !Bool + -- ^ Enable explorer extended API for fetching more + -- info about addresses (like utxos) and bulk endpoints } deriving (Show, Generic) instance ToJSON NodeConfiguration where @@ -91,3 +97,12 @@ walletProductionApi = ccWalletProductionApi $ nodeConfiguration -- existing pending transactions. walletTxCreationDisabled :: HasNodeConfiguration => Bool walletTxCreationDisabled = ccWalletTxCreationDisabled $ nodeConfiguration + +---------------------------------------------------------------------------- +-- Explorer parameters +---------------------------------------------------------------------------- + +-- | If 'True', explorer extended API, like fetching utxos for address is enabled. +-- WARNING Those endpoints are potentially expensive! +explorerExtendedApi :: HasNodeConfiguration => Bool +explorerExtendedApi = ccExplorerExtendedApi $ nodeConfiguration