Give more representative names in the different datatypes and JSON-RPC endpoints in `marconi-mamba`.
Co-authored-by: koslambrou <[email protected]>
Co-authored-by: koslambrou <[email protected]>
, aeson
, async
, base
, base16-bytestring
, bytestring
, containers
, filepath
import Cardano.Api ()
import Cardano.Api qualified as C
import "cardano-api" Cardano.Api.Shelley qualified as Shelley
import Data.Aeson (ToJSON (toJSON), object, (.=))
import Data.ByteString (ByteString)
import Marconi.ChainIndex.Orphans ()
import Marconi.ChainIndex.Types (CurrentEra, TargetAddresses, TxOut, pattern CurrentEra)
import Marconi.Core.Storable (Buffered (getStoredEvents, persistToStorage), HasPoint (getPoint),
= _txId left <= _txId right
&& _txIx left <= _txIx right
instance ToJSON Utxo where
toJSON (Utxo addr tId tIx dtum dtumHash val scrpt scrptHash) = object
[ "address" .= addr
, "txId" .= tId
, "txIx" .= tIx
, "datum" .= (C.serialiseToCBOR <$> dtum)
, "datumHash" .= dtumHash
, "value" .= val
, "inlineScript" .= (scriptToCBOR <$> scrpt)
, "inlineScriptHash" .= scrptHash
]
-- | convert to Script to CBOR bytestring
scriptToCBOR :: Shelley.ScriptInAnyLang -> ByteString
scriptToCBOR (Shelley.ScriptInAnyLang(C.SimpleScriptLanguage C.SimpleScriptV1) script) =
C.serialiseToCBOR script
scriptToCBOR (Shelley.ScriptInAnyLang(C.SimpleScriptLanguage C.SimpleScriptV2) script) =
C.serialiseToCBOR script
scriptToCBOR (Shelley.ScriptInAnyLang(C.PlutusScriptLanguage C.PlutusScriptV1) script) =
C.serialiseToCBOR script
scriptToCBOR (Shelley.ScriptInAnyLang(C.PlutusScriptLanguage C.PlutusScriptV2) script) =
C.serialiseToCBOR script
data UtxoRow = UtxoRow
{ _urUtxo :: Utxo
, _urBlockNo :: C.BlockNo
$(makeLenses ''UtxoRow)
instance ToJSON UtxoRow where
toJSON (UtxoRow u b s h) = object
[ "utxo" .= u
, "blockNo" .= b
, "slotNo" .= s
, "blockHeaderHash" .= h
]
newtype instance StorableResult UtxoHandle = UtxoResult [UtxoRow] deriving Show
data instance StorableEvent UtxoHandle = UtxoEvent
import Cardano.Api qualified as C
import Cardano.Binary (fromCBOR, toCBOR)
import Codec.Serialise (Serialise (decode, encode), deserialiseOrFail, serialise)
import Data.Aeson (ToJSON)
import Data.Aeson qualified as Aeson
import Data.ByteString (ByteString)
import Data.ByteString.Base16 qualified as Base16
import Data.ByteString.Lazy (toStrict)
import Data.Char qualified as Char
import Data.Functor ((<&>))
import Data.Maybe (fromMaybe)
import Data.Proxy (Proxy (Proxy))
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text
import Database.SQLite.Simple qualified as SQL
import Database.SQLite.Simple.FromField qualified as SQL
import Database.SQLite.Simple.ToField qualified as SQL
instance SQL.ToField C.BlockNo where
toField (C.BlockNo s) = SQL.SQLInteger $ fromIntegral s
instance ToJSON C.BlockNo
-- * C.AddressAny
instance SQL.FromField C.AddressAny where
instance SQL.ToField C.AddressAny where
toField = SQL.SQLBlob . C.serialiseToRawBytes
instance ToJSON C.AddressAny where
toJSON = Aeson.String . C.serialiseAddress
-- * C.Hash C.ScriptData
instance SQL.FromField (C.Hash C.ScriptData) where
(const $ SQL.returnError SQL.ConversionFailed f "Cannot deserialise scriptDataHash.")
pure . C.deserialiseFromRawBytesHex (C.proxyToAsType Proxy)
instance ToJSON ByteString where
toJSON bs
| Right s <- Text.decodeUtf8' bs, Text.all Char.isPrint s = Aeson.String s
| otherwise
= Aeson.String (bytesPrefix <> Text.decodeLatin1 (Base16.encode bs))
-- from cardano-node: https://github.com/input-output-hk/cardano-node/blob/master/cardano-api/src/Cardano/Api/ScriptData.hs#L444-L447
-- | JSON strings that are base16 encoded and prefixed with 'bytesPrefix' will
-- be encoded as CBOR bytestrings.
bytesPrefix :: Text
bytesPrefix = "0x"
|-----------+-----------+------------------------+---------------------------------------------|
| HTTP Verb | Endpoints | RPC method | Description |
|-----------+-----------+------------------------+---------------------------------------------|
| POST | json-rpc | addresseesBech32Report | Retrieves user provided addresses |
| POST | json-rpc | utxoJsonReport | Retrieves TxRefs for an address |
| POST | json-rpc | targetAddress-query | Retrieves user provided addresses |
| POST | json-rpc | utxo-query | Retrieves TxRefs for an address |
| POST | JSON-rpc | echo | echo's user input to console |
|-----------+-----------+------------------------+---------------------------------------------|
```
Here is a curl script to exploit the JSON-RPC server:
``` sh
curl -d '{"jsonrpc": "2.0" , "method": "utxoJsonReport" , "params": "addr_test1qzzxxkwnz4k60fjdjqspt58c8xe069kemfw4gljnnqtc4aarszs09x52vy8kfknj0rrr9400e39ufz5tuct74h52kcrqaytqk7", "id": 19}' -H 'Content-Type: application/json' -X POST http://localhost:3000/json-rpc | jq
curl -d '{"jsonrpc": "2.0" , "method": "utxo-query" , "params": "addr_test1qzzxxkwnz4k60fjdjqspt58c8xe069kemfw4gljnnqtc4aarszs09x52vy8kfknj0rrr9400e39ufz5tuct74h52kcrqaytqk7", "id": 19}' -H 'Content-Type: application/json' -X POST http://localhost:3000/json-rpc | jq
{
"id": 19,
"jsonrpc": "2.0",
"result": {
"urAddress": "addr_test1qzzxxkwnz4k60fjdjqspt58c8xe069kemfw4gljnnqtc4aarszs09x52vy8kfknj0rrr9400e39ufz5tuct74h52kcrqaytqk7",
"urReport": [
"uqAddress": "addr_test1qzzxxkwnz4k60fjdjqspt58c8xe069kemfw4gljnnqtc4aarszs09x52vy8kfknj0rrr9400e39ufz5tuct74h52kcrqaytqk7",
"uqResults": [
{
"_urBlockHash": "8ccc256dda1f8c499dd91beb9e19e0a794463e876c5602b74c82997e31f16bde",
"_urBlockNo": {
import Control.Concurrent.Async (race_)
import Marconi.Mamba.Api.Types (CliArgs (CliArgs))
import Marconi.Mamba.Bootstrap (bootstrapHttp, bootstrapJsonRpc, bootstrapUtxoIndexers)
import Marconi.Mamba.Bootstrap (bootstrapHttp, bootstrapIndexers, initializeIndexerEnv)
import Marconi.Mamba.CLI (parseCli)
-- | concurrently start:
main :: IO ()
main = do
[email protected](CliArgs _ _ maybePort _ tAddress) <- parseCli
rpcEnv <- bootstrapJsonRpc maybePort tAddress
rpcEnv <- initializeIndexerEnv maybePort tAddress
race_
(bootstrapHttp rpcEnv) -- start http server
(bootstrapUtxoIndexers cli rpcEnv)
(bootstrapIndexers cli rpcEnv)
import Marconi.ChainIndex.CLI (multiString)
import Marconi.ChainIndex.Indexers.Utxo qualified as Utxo
import Marconi.ChainIndex.Types (TargetAddresses)
import Marconi.Mamba.Api.Types (UtxoIndexerEnv, queryEnv, uiIndexer)
import Marconi.Mamba.Api.UtxoIndexersQuery qualified as UIQ
import Marconi.Mamba.Bootstrap (bootstrapHttp, bootstrapJsonRpc)
import Marconi.Mamba.Api.Query.UtxoIndexer qualified as UIQ
import Marconi.Mamba.Api.Types (IndexerEnv, queryEnv, uiIndexer)
import Marconi.Mamba.Bootstrap (bootstrapHttp, initializeIndexerEnv)
data CliOptions = CliOptions
<>"\nport =" <> show (3000 :: Int)
<> "\nmarconi-db-dir =" <> dbpath
<> "\nnumber of addresses to index = " <> show (length addresses)
env <- bootstrapJsonRpc Nothing addresses
env <- initializeIndexerEnv Nothing addresses
race_ (bootstrapHttp env) (mocUtxoIndexer dbpath (env ^. queryEnv) )
-- | moc marconi utxo indexer.
-- This will allow us to use the UtxoIndexer query interface without having cardano-node or marconi online
-- Effectively we are going to query SQLite only
mocUtxoIndexer :: FilePath -> UtxoIndexerEnv -> IO ()
mocUtxoIndexer :: FilePath -> IndexerEnv -> IO ()
mocUtxoIndexer dbpath env =
Utxo.open dbpath (Utxo.Depth 4) >>= callback >> innerLoop
where
Content-Type: application/json
{"jsonrpc": "2.0", "method": "add", "params": [1,1], "id": 0}
#
# should print message on the console
POST http://localhost:3000/json-rpc
Content-Type: application/json-rpc
{"jsonrpc": "2.0", "method": "print", "params": "print me", "id": 0}
#
#
# lookup fot txoutrefs , should return result, low
# should fail, Unknown method: unknownMethod
POST http://localhost:3000/json-rpc
Content-Type: application/json
{
"jsonrpc": "2.0"
, "method": "utxoJsonReport"
, "method": "unknownMethod"
, "params": "addr_test1wz3937ykmlcaqxkf4z7stxpsfwfn4re7ncy48yu8vutcpxgnj28k0"
, "id": 2
}
#
# lookup fot txoutrefs , should return result, high
# lookup for UTXOs , should return result lots of data
POST http://localhost:3000/json-rpc
Content-Type: application/json
{
"jsonrpc": "2.0"
, "method": "utxoJsonReport"
, "method": "getUtxoFromAddress"
, "params": "addr_test1vpfwv0ezc5g8a4mkku8hhy3y3vp92t7s3ul8g778g5yegsgalc6gc"
, "id": 92
}
#
# lookup fot txoutrefs, should generate error if the address is not in the target list
# lookup for UTXOs, should generate error if the address is not in the target list
POST http://localhost:3000/json-rpc
Content-Type: application/json
{
"jsonrpc": "2.0"
, "method": "utxoJsonReport"
, "method": "getUtxoFromAddress"
, "params": "addr_test1wqr4uz0tp75fu8wrg6gm83t20aphuc9vt6n8kvu09ctkugq6ch8kj"
, "id":21
}
#
# lookup for UTXOs, should generate error if the address is not in the target list
POST http://localhost:3000/json-rpc
Content-Type: application/json
{
"jsonrpc": "2.0"
, "method": "getUtxoFromAddress"
, "params": ["addr_test1wqr4uz0tp75fu8wrg6gm83t20aphuc9vt6n8kvu09ctkugq6ch8kj"]
, "id":21
}
#
# addresses with non null dautm
#
#addr_test1wz3937ykmlcaqxkf4z7stxpsfwfn4re7ncy48yu8vutcpxgnj28k0
#
# issue non-existing mthode
# it should fail with code -32601, unknown method
# it should fail, "Error in $: expected String, but encountered Number"
POST http://localhost:3000/json-rpc
Content-Type: application/json
{
"jsonrpc": "2.0"
, "method": "utxoJsonReports"
, "method": "getUtxoFromAddress"
, "params": 100
, "id": 14
}
Content-Type: application/json
{
"jsonrpc": "2.0"
, "method": "addressesBech32Report"
, "params": 100
, "method": "getTargetAddresses"
, "params": ""
, "id": 11
}
#
#
# get the top 100 utxos, sorted by ledger.Address
POST http://localhost:3000/json-rpc
Content-Type: application/json
{
"jsonrpc": "2.0"
, "method": "utxosReport"
, "params": 100
, "id": 14
}
#
#### REST calls ####
#
# REST call, get the current time. This may be used as health-check
hs-source-dirs: src
exposed-modules:
Marconi.Mamba.Api.HttpServer
Marconi.Mamba.Api.Query.UtxoIndexer
Marconi.Mamba.Api.Routes
Marconi.Mamba.Api.Types
Marconi.Mamba.Api.UtxoIndexersQuery
Marconi.Mamba.Bootstrap
Marconi.Mamba.CLI
------------------------
build-depends:
, aeson
, async
, base >=4.9 && <5
, base16-bytestring
, bytestring
, lens
, optparse-applicative
, prettyprinter
--------------------
-- Local components
--------------------
build-depends: marconi-chain-index:{marconi-chain-index, json-rpc}
build-depends: marconi-chain-index
------------------------
-- Non-IOG dependencies
type: exitcode-stdio-1.0
main-is: Spec.hs
hs-source-dirs: test
other-modules: Spec.Marconi.Mamba.Api.UtxoIndexersQuery
other-modules: Spec.Marconi.Mamba.Api.Query.UtxoIndexer
--------------------
-- Local components
import Data.Text (Text, pack)
import Data.Time (defaultTimeLocale, formatTime, getCurrentTime)
import Network.Wai.Handler.Warp (runSettings)
import Servant.API (NoContent (NoContent), (:<|>) ((:<|>)))
import Servant.API ((:<|>) ((:<|>)))
import Servant.Server (Handler, Server, serve)
import Cardano.Api ()
import Marconi.Mamba.Api.Query.UtxoIndexer qualified as Q.Utxo
import Marconi.Mamba.Api.Routes (API)
import Marconi.Mamba.Api.Types (HasJsonRpcEnv (httpSettings, queryEnv), JsonRpcEnv, QueryExceptions, UtxoIndexerEnv,
UtxoReport)
import Marconi.Mamba.Api.UtxoIndexersQuery qualified as Q.Utxo
import Marconi.Mamba.Api.Types (HasMambaEnv (httpSettings, queryEnv), IndexerEnv, MambaEnv, QueryExceptions,
UtxoQueryResult)
import Network.JsonRpc.Server.Types ()
import Network.JsonRpc.Types (JsonRpcErr (JsonRpcErr, errorCode, errorData, errorMessage), parseErrorCode)
-- | Bootstraps the HTTP server
bootstrap :: JsonRpcEnv -> IO ()
bootstrap :: MambaEnv -> IO ()
bootstrap env = runSettings
(env ^. httpSettings)
(serve (Proxy @API) (server (env ^. queryEnv ) ) )
server
:: UtxoIndexerEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
:: IndexerEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
-> Server API
server env
= ( echo
:<|> utxoJsonReport env
:<|> targetAddressesReport env
:<|> printMessage env )
= (echo
:<|> utxoQuery env
:<|> targetAddressesQuery env)
:<|> (getTime
:<|> getTargetAddresses env
:<|> printMessage env)
:<|> getTargetAddresses env)
-- | prints message to console
-- Used for testing the server from console
printMessage
:: UtxoIndexerEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
-> String
-> Handler NoContent
printMessage env msg = NoContent <$ (
liftIO $ do
putStrLn msg
putStrLn "\n"
print (Q.Utxo.reportBech32Addresses env)
)
-- | echos message back as a jsonrpc response
-- | echos message back as a Jsonrpc response
-- Used for testing the server
echo
:: String
-> Handler (Either (JsonRpcErr String) String)
echo = return . Right
echo = return . Right
-- | echos current time as REST response
-- Used for testing the http server outside of jsonrpc protocol
where
timeString = formatTime defaultTimeLocale "%T"
-- | prints TargetAddresses Bech32 representation to the console
getTargetAddresses
:: UtxoIndexerEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
:: IndexerEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
-> Handler [Text]
getTargetAddresses = pure . Q.Utxo.reportBech32Addresses
-- | Retrieves a set of TxOutRef
utxoJsonReport
:: UtxoIndexerEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
-- | Retrieves Utxos by Address
utxoQuery
:: IndexerEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
-> String -- ^ bech32 addressCredential
-> Handler (Either (JsonRpcErr String) UtxoReport)
utxoJsonReport env address = liftIO $
first toRpcErr <$> (Q.Utxo.findByAddress env . pack $ address)
-> Handler (Either (JsonRpcErr String) UtxoQueryResult)
utxoQuery env address = liftIO $
first toRpcErr <$> (Q.Utxo.findByBech32Address env . pack $ address)
targetAddressesReport
:: UtxoIndexerEnv -- ^ database configuration
-> Int -- ^ limit, for now we are ignoring returning everyting
-- | prints TargetAddresses Bech32 representation as thru JsonRpc
targetAddressesQuery
:: IndexerEnv -- ^ database configuration
-> String
-- ^ Will always be an empty string as we are ignoring this param, and returning everyting
-> Handler (Either (JsonRpcErr String) [Text] )
targetAddressesReport env _ = pure . Right . Q.Utxo.reportBech32Addresses $ env
targetAddressesQuery env _ = pure . Right . Q.Utxo.reportBech32Addresses $ env
-- | convert form to jsonrpc protocal error
-- | convert form to Jsonrpc protocal error
toRpcErr
:: QueryExceptions
-> JsonRpcErr String
module Marconi.Mamba.Api.UtxoIndexersQuery
( bootstrap
, findByCardanoAddress
module Marconi.Mamba.Api.Query.UtxoIndexer
( initializeEnv
, findByAddress
, findAll
, findByBech32Address
, reportQueryAddresses
, Utxo.UtxoRow(..)
, Utxo.UtxoIndexer
, reportQueryCardanoAddresses
, reportBech32Addresses
, withQueryAction
, writeTMVar
, writeTMVar'
) where
import Control.Concurrent.Async (forConcurrently)
import Control.Concurrent.STM (atomically)
import Control.Concurrent.STM.TMVar (TMVar, newEmptyTMVar, putTMVar, takeTMVar, tryTakeTMVar)
import Control.Exception (bracket)
import Control.Lens ((^.))
import Control.Monad.STM (STM)
import Data.Functor ((<&>))
import Data.List.NonEmpty qualified as NonEmpty
import Data.Text (Text, intercalate, pack, unpack)
import Data.Text (Text, unpack)
import Cardano.Api qualified as C
import Marconi.ChainIndex.Indexers.Utxo qualified as Utxo
import Marconi.ChainIndex.Types (TargetAddresses)
import Marconi.Core.Storable qualified as Storable
import Marconi.Mamba.Api.Types (HasUtxoIndexerEnv (uiIndexer, uiQaddresses),
QueryExceptions (AddressNotInListError, QueryError),
UtxoIndexerEnv (UtxoIndexerEnv, _uiIndexer, _uiQaddresses),
UtxoIndexerWrapper (UtxoIndexerWrapper, unWrap), UtxoReport (UtxoReport))
import Marconi.Mamba.Api.Types (HasIndexerEnv (uiIndexer, uiQaddresses),
IndexerEnv (IndexerEnv, _uiIndexer, _uiQaddresses),
IndexerWrapper (IndexerWrapper, unWrapUtxoIndexer),
QueryExceptions (AddressNotInListError, QueryError), UtxoQueryResult (UtxoQueryResult))
-- | Bootstraps the utxo query environment.
-- The module is responsible for accessing SQLite for quries.
-- The main issue we try to avoid here is mixing inserts and quries in SQLite to avoid locking the database
bootstrap
:: TargetAddresses -- ^ user provided target addresses
-> IO UtxoIndexerEnv -- ^ returns Query runtime environment
bootstrap targetAddresses = do
initializeEnv
:: TargetAddresses -- ^ user provided target addresses
-> IO IndexerEnv -- ^ returns Query runtime environment
initializeEnv targetAddresses = do
ix <- atomically (newEmptyTMVar :: STM (TMVar Utxo.UtxoIndexer) )
pure $ UtxoIndexerEnv
{ _uiIndexer = UtxoIndexerWrapper ix
pure $ IndexerEnv
{ _uiIndexer = IndexerWrapper ix
, _uiQaddresses = targetAddresses
}
-- | finds reports for all user-provided addresses.
-- TODO consider sqlite streaming, https://hackage.haskell.org/package/sqlite-simple-0.4.18.2/docs/Database-SQLite-Simple.html#g:14
--
findAll
:: UtxoIndexerEnv -- ^ Query run time environment
-> IO [UtxoReport] -- ^ set of corresponding TxOutRefs
findAll env = forConcurrently addresses f
where
addresses = NonEmpty.toList (env ^. uiQaddresses)
f :: C.Address C.ShelleyAddr -> IO UtxoReport
f addr = (findByCardanoAddress env . C.toAddressAny $ addr) <&> UtxoReport (pack . show $ addr)
-- | Query utxos by Cardano Address
-- To Cardano error may occur
findByCardanoAddress
:: UtxoIndexerEnv -- ^ Query run time environment
-- | Query utxos by Address
-- Address conversion error from Bech32 may occur
findByAddress
:: IndexerEnv -- ^ Query run time environment
-> C.AddressAny -- ^ Cardano address to query
-> IO [Utxo.UtxoRow]
findByCardanoAddress = withQueryAction
findByAddress = withQueryAction
-- | Retrieve a Set of TxOutRefs associated with the given Cardano Era address
-- We return an empty Set if no address is found
findByAddress
:: UtxoIndexerEnv -- ^ Query run time environment
-- | Retrieve Utxos associated with the given address
-- We return an empty list if no address is found
findByBech32Address
:: IndexerEnv -- ^ Query run time environment
-> Text -- ^ Bech32 Address
-> IO (Either QueryExceptions UtxoReport) -- ^ To Plutus address conversion error may occure
findByAddress env addressText =
-> IO (Either QueryExceptions UtxoQueryResult) -- ^ To Plutus address conversion error may occure
findByBech32Address env addressText =
let
f :: Either C.Bech32DecodeError (C.Address C.ShelleyAddr) -> IO (Either QueryExceptions UtxoReport)
f :: Either C.Bech32DecodeError (C.Address C.ShelleyAddr) -> IO (Either QueryExceptions UtxoQueryResult)
f (Right address)
| address `elem` (env ^. uiQaddresses) = -- allow for targetAddress search only
(pure . C.toAddressAny $ address)
>>= findByCardanoAddress env
<&> Right . UtxoReport addressText
>>= findByAddress env
<&> Right . UtxoQueryResult addressText
| otherwise = pure . Left . AddressNotInListError . QueryError $
unpack addressText <> " not in the provided target addresses"
f (Left e) = pure . Left $ QueryError (unpack addressText
-- | Execute the query function
-- We must stop the utxo inserts before doing the query
withQueryAction
:: UtxoIndexerEnv -- ^ Query run time environment
-> C.AddressAny -- ^ Cardano address to query
:: IndexerEnv -- ^ Query run time environment
-> C.AddressAny -- ^ Cardano address to query
-> IO [Utxo.UtxoRow]
withQueryAction env address =
let
utxoIndexer = unWrap $ env ^. uiIndexer
utxoIndexer = unWrapUtxoIndexer $ env ^. uiIndexer
action :: Utxo.UtxoIndexer -> IO [Utxo.UtxoRow]
action indexer = do
Utxo.UtxoResult rows <- Storable.query Storable.QEverything indexer (Utxo.UtxoAddress address)
-- | report target addresses
-- Used by JSON-RPC
reportQueryAddresses
:: UtxoIndexerEnv
:: IndexerEnv
-> IO [C.Address C.ShelleyAddr]
reportQueryAddresses env
= pure
. NonEmpty.toList
$ (env ^. uiQaddresses )
reportQueryCardanoAddresses
:: UtxoIndexerEnv
-> Text
reportQueryCardanoAddresses = intercalate ", " . reportBech32Addresses
reportBech32Addresses
:: UtxoIndexerEnv
:: IndexerEnv
-> [Text]
reportBech32Addresses env
= NonEmpty.toList
writeTMVar :: TMVar a -> a -> STM ()
writeTMVar t new = tryTakeTMVar t >> putTMVar t new
writeTMVar' :: UtxoIndexerWrapper-> Utxo.UtxoIndexer -> STM ()
writeTMVar' (UtxoIndexerWrapper t) = writeTMVar t
writeTMVar' :: IndexerWrapper-> Utxo.UtxoIndexer -> STM ()
writeTMVar' (IndexerWrapper t) = writeTMVar t
-- | module defines REST and JSON-RPC routes
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}
module Marconi.Mamba.Api.Routes where
module Marconi.Mamba.Api.Routes (
API
) where
import Data.Text (Text)
import Marconi.Mamba.Api.Types (UtxoReport)
import Network.JsonRpc.Types (JsonRpc, JsonRpcNotification, RawJsonRpc)
import Servant.API (Get, JSON, NoContent, PlainText, Post, ReqBody, (:<|>), (:>))
-- RPC method parameter(s) return-type
type Echo = JsonRpc "echo" String String String
import Marconi.Mamba.Api.Types (UtxoQueryResult)
import Network.JsonRpc.Types (JsonRpc, RawJsonRpc)
import Servant.API (Get, JSON, PlainText, (:<|>), (:>))
type UtxoJsonReport = JsonRpc "utxoJsonReport" String String UtxoReport
------------------------------------------
-- RPC types
-- methodName -> parameter(s) -> return-type
------------------------------------------
type RpcEcho = JsonRpc "echo" String String String
type TargetAddressesReport = JsonRpc "addressesBech32Report" Int String [Text]
type RpcUtxoQueryResult = JsonRpc "getUtxoFromAddress" String String UtxoQueryResult
type Print = JsonRpcNotification "print" String
type RpcTargetAddresses = JsonRpc "getTargetAddresses" String String [Text]
type RpcAPI = Echo :<|> UtxoJsonReport :<|> TargetAddressesReport :<|> Print
-- | Rpc routes
type RpcAPI
= RpcEcho
:<|> RpcUtxoQueryResult
:<|> RpcTargetAddresses
-- | JSON-RPC API, endpoint
type JsonRpcAPI = "json-rpc" :> RawJsonRpc RpcAPI
--------------------
--- REST related ---
--------------------
type GetTime = "time" :> Get '[PlainText] String
type GetTargetAddresses = "addresses" :> Get '[JSON] [Text]
type PrintMessage = "print" :> ReqBody '[PlainText] String :> Post '[PlainText] NoContent
type RestAPI = "rest" :> (GetTime :<|> GetTargetAddresses :<|> PrintMessage)
-- | REST API, endpoints
type RestAPI = "rest" :> (GetTime :<|> GetTargetAddresses)
-- | marconi-mamba APIs
type API = JsonRpcAPI :<|> RestAPI
type NonEndpoint = "json-rpc" :> RawJsonRpc (JsonRpc "launch-missles" Int String Bool)
import Control.Concurrent.STM.TMVar (TMVar)
import Control.Exception (Exception)
import Control.Lens (makeClassy)
import Data.Aeson (ToJSON (toEncoding, toJSON), defaultOptions, genericToEncoding, object, (.=))
import Data.Aeson qualified as Aeson
import Data.ByteString (ByteString)
import Data.ByteString.Base16 qualified as Base16
import Data.Char qualified as Char
import Data.Aeson (ToJSON (toEncoding), defaultOptions, genericToEncoding)
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text
import GHC.Generics (Generic)
import Network.Wai.Handler.Warp (Settings)
import Cardano.Api qualified as C
import Cardano.Api.Shelley qualified as Shelley
import Marconi.ChainIndex.Indexers.Utxo qualified as Utxo
import Marconi.ChainIndex.Types as Export (TargetAddresses)
-- | Type represents http port for JSON-RPC
type RpcPortNumber = Int
data CliArgs = CliArgs
{ socket :: FilePath -- ^ POSIX socket file to communicate with cardano node
, targetAddresses :: TargetAddresses -- ^ white-space sepparated list of Bech32 Cardano Shelley addresses
} deriving (Show)
newtype UtxoIndexerWrapper = UtxoIndexerWrapper
{ unWrap :: TMVar Utxo.UtxoIndexer -- ^ for query thread to access in-memory utxos
-- | Should contain all the indexers required by Mamba
newtype IndexerWrapper = IndexerWrapper
{ unWrapUtxoIndexer :: TMVar Utxo.UtxoIndexer -- ^ for query thread to access in-memory utxos
}
data UtxoIndexerEnv = UtxoIndexerEnv
{ _uiIndexer :: UtxoIndexerWrapper
data IndexerEnv = IndexerEnv
{ _uiIndexer :: IndexerWrapper
, _uiQaddresses :: TargetAddresses -- ^ user provided addresses to filter
}
makeClassy ''UtxoIndexerEnv
makeClassy ''IndexerEnv
-- | JSON-RPC configuration
data JsonRpcEnv = JsonRpcEnv
-- | JSON-RPC as well as the Query Indexer Env
data MambaEnv = MambaEnv
{ _httpSettings :: Settings -- ^ HTTP server setting
, _queryEnv :: UtxoIndexerEnv -- ^ used for query sqlite
, _queryEnv :: IndexerEnv -- ^ used for query sqlite
}
makeClassy ''JsonRpcEnv
makeClassy ''MambaEnv
data QueryExceptions
= AddressNotInListError QueryExceptions
deriving stock Show
deriving anyclass Exception
-- from cardano-node: https://github.com/input-output-hk/cardano-node/blob/master/cardano-api/src/Cardano/Api/ScriptData.hs#L444-L447
-- | JSON strings that are base16 encoded and prefixed with 'bytesPrefix' will
-- be encoded as CBOR bytestrings.
bytesPrefix :: Text
bytesPrefix = "0x"
instance ToJSON ByteString where
toJSON bs
| Right s <- Text.decodeUtf8' bs, Text.all Char.isPrint s = Aeson.String s
| otherwise
= Aeson.String (bytesPrefix <> Text.decodeLatin1 (Base16.encode bs))
instance ToJSON Utxo.Utxo where
toJSON (Utxo.Utxo addr tId tIx dtum dtumHash val scrpt scrptHash) = object
["address" .= addr
, "txId" .= tId
, "txIx" .= tIx
, "datum" .= (C.serialiseToCBOR <$> dtum)
, "datumHash" .= dtumHash
, "value" .= val
, "inlineScript" .= (scriptToCBOR <$>scrpt)
, "inlineScriptHash" .= scrptHash
]
data UtxoReport = UtxoReport
{ urAddress :: Text
, urReport :: [Utxo.UtxoRow]
data UtxoQueryResult = UtxoQueryResult
{ uqAddress :: Text
, uqResults :: ![Utxo.UtxoRow]
} deriving (Eq, Ord, Generic)
instance ToJSON UtxoReport where
instance ToJSON UtxoQueryResult where
toEncoding = genericToEncoding defaultOptions
instance ToJSON C.AddressAny where
toJSON = Aeson.String . C.serialiseAddress
instance ToJSON (Utxo.StorableQuery Utxo.UtxoHandle) where
toJSON (Utxo.UtxoAddress addr) = toJSON addr
instance ToJSON C.BlockNo
instance ToJSON Utxo.UtxoRow where
toJSON (Utxo.UtxoRow u b s h) = object
[ "utxo" .= u
, "blockNo" .= b
, "slotNo" .= s
, "blockHeaderHash" .= h]
-- | convert to Script to CBOR bytestring
scriptToCBOR :: Shelley.ScriptInAnyLang -> ByteString
scriptToCBOR (Shelley.ScriptInAnyLang(C.SimpleScriptLanguage C.SimpleScriptV1) script) =
C.serialiseToCBOR script
scriptToCBOR (Shelley.ScriptInAnyLang(C.SimpleScriptLanguage C.SimpleScriptV2) script) =
C.serialiseToCBOR script
scriptToCBOR (Shelley.ScriptInAnyLang(C.PlutusScriptLanguage C.PlutusScriptV1) script) =
C.serialiseToCBOR script
scriptToCBOR (Shelley.ScriptInAnyLang(C.PlutusScriptLanguage C.PlutusScriptV2) script) =
C.serialiseToCBOR script
import Control.Lens ((^.))
import Data.List.NonEmpty (fromList, nub)
import Data.Text (pack)
import Network.Wai.Handler.Warp (defaultSettings, setPort)
import Network.Wai.Handler.Warp (Port, defaultSettings, setPort)
import Prettyprinter (defaultLayoutOptions, layoutPretty, pretty, (<+>))
import Prettyprinter.Render.Text (renderStrict)
import Marconi.ChainIndex.Indexers (mkIndexerStream, startIndexers, utxoWorker)
import Marconi.ChainIndex.Types (TargetAddresses)
import Marconi.Mamba.Api.HttpServer qualified as Http
import Marconi.Mamba.Api.Types (CliArgs (CliArgs), HasJsonRpcEnv (queryEnv), HasUtxoIndexerEnv (uiIndexer),
JsonRpcEnv (JsonRpcEnv, _httpSettings, _queryEnv), RpcPortNumber)
import Marconi.Mamba.Api.UtxoIndexersQuery (UtxoIndexer, bootstrap, writeTMVar')
import Marconi.Mamba.Api.Query.UtxoIndexer (UtxoIndexer, initializeEnv, writeTMVar')
import Marconi.Mamba.Api.Types (CliArgs (CliArgs), HasIndexerEnv (uiIndexer), HasMambaEnv (queryEnv),
MambaEnv (MambaEnv, _httpSettings, _queryEnv))
-- | Bootstraps the JSON-RPC http server with appropriate settings and marconi cache
-- this is just a wrapper for the bootstrapHttp in json-rpc package
bootstrapJsonRpc
:: Maybe RpcPortNumber
initializeIndexerEnv
:: Maybe Port
-> TargetAddresses
-> IO JsonRpcEnv
bootstrapJsonRpc maybePort targetAddresses = do
queryenv <- bootstrap targetAddresses
-> IO MambaEnv
initializeIndexerEnv maybePort targetAddresses = do
queryenv <- initializeEnv targetAddresses
let httpsettings = maybe defaultSettings (flip setPort defaultSettings ) maybePort
pure $ JsonRpcEnv
pure $ MambaEnv
{ _httpSettings = httpsettings
, _queryEnv = queryenv
}
bootstrapHttp
:: JsonRpcEnv
:: MambaEnv
-> IO ()
bootstrapHttp = Http.bootstrap
-- | marconi cardano blockchain indexer
bootstrapUtxoIndexers
bootstrapIndexers
:: CliArgs
-> JsonRpcEnv
-> MambaEnv
-> IO ()
bootstrapUtxoIndexers (CliArgs socket dbPath _ networkId targetAddresses) env = do
bootstrapIndexers (CliArgs socket dbPath _ networkId targetAddresses) env = do
let callbackIndexer :: UtxoIndexer -> IO ()
callbackIndexer = atomically . writeTMVar' (env ^. queryEnv . uiIndexer)
(_, coordinator) <-
import Test.Tasty (TestTree, defaultMain, testGroup)
import Spec.Marconi.Mamba.Api.UtxoIndexersQuery qualified as Spec.UtxoIndexersQuery
import Spec.Marconi.Mamba.Api.Query.UtxoIndexer qualified as Spec.Query.UtxoIndexer
main :: IO ()
main = defaultMain tests
tests :: TestTree
tests = testGroup "marconi-mamba"
[Spec.UtxoIndexersQuery.tests]
[Spec.Query.UtxoIndexer.tests]
{-# OPTIONS_GHC -fno-warn-incomplete-patterns #-}
{-# OPTIONS_GHC -Wno-orphans #-}
module Spec.Marconi.Mamba.Api.UtxoIndexersQuery (tests) where
module Spec.Marconi.Mamba.Api.Query.UtxoIndexer (tests) where
import Control.Concurrent.STM (atomically)
import Control.Lens.Operators ((^.))
import Cardano.Api qualified as C
import Gen.Cardano.Api.Typed qualified as CGen
import Marconi.Mamba.Api.Types (HasUtxoIndexerEnv (uiIndexer))
import Marconi.Mamba.Api.Types (HasIndexerEnv (uiIndexer))
import Marconi.ChainIndex.Indexers.Utxo qualified as Utxo
import Marconi.ChainIndex.Types (TargetAddresses)
import Marconi.Core.Storable (StorableEvent)
import Marconi.Core.Storable qualified as Storable
import Marconi.Mamba.Api.UtxoIndexersQuery qualified as UIQ
import Marconi.Mamba.Api.Query.UtxoIndexer qualified as UIQ
genSlotNo :: Hedgehog.MonadGen m => m C.SlotNo
genSlotNo = C.SlotNo <$> Gen.word64 (Range.linear 10 1000)
. fmap Utxo._address
. concatMap (Set.toList . Utxo.ueUtxos)
$ events
env <- liftIO . UIQ.bootstrap $ targetAddresses
env <- liftIO . UIQ.initializeEnv $ targetAddresses
let
callback :: Utxo.UtxoIndexer -> IO ()
callback = atomically . UIQ.writeTMVar' (env ^. uiIndexer)
liftIO . mocUtxoWorker callback events $ Utxo.Depth depth
fetchedRows <-
liftIO
. fmap (nub . concat)
. traverse (UIQ.findByCardanoAddress env)
. traverse (UIQ.findByAddress env)
. fmap C.toAddressAny
$ targetAddresses
let rows = nub . concatMap Utxo.eventsToRows $ events