Skip to content

Commit

Permalink
PLT-1311 Add JSON-RPC querying of the stake pool delegation (SPD) by …
Browse files Browse the repository at this point in the history
…epoch in `marconi-sidechain`. (#1026)

* Add indexing and JSON-RPC querying of the SPD by epoch in `marconi-sidechain`.

* Added golden test for JSON response of stake pool delegation by
  epoch

* Fixed resuming of Utxo indexer to include *all* possible chain points,
  not only the latest ones.

* Updated README of `marconi-sidechain` to include an example of request
  and response for the JSON-RPC server on that specific endpoint.
  • Loading branch information
koslambrou committed Mar 24, 2023
1 parent dba9cb0 commit a6b95b0
Show file tree
Hide file tree
Showing 19 changed files with 390 additions and 157 deletions.
Expand Up @@ -75,6 +75,7 @@ import Cardano.Slotting.Slot (EpochNo)
import Codec.CBOR.Read qualified as CBOR
import Codec.CBOR.Write qualified as CBOR
import Control.Monad (filterM, forM_, when)
import Data.Aeson (FromJSON (parseJSON), ToJSON (toJSON), Value (Object), object, (.:), (.=))
import Data.ByteString.Base16 qualified as Base16
import Data.ByteString.Lazy qualified as BS
import Data.Coerce (coerce)
Expand Down Expand Up @@ -231,6 +232,33 @@ data EpochSPDRow = EpochSPDRow
, epochSPDRowBlockNo :: !C.BlockNo
} deriving (Eq, Ord, Show, Generic, SQL.FromRow, SQL.ToRow)

instance FromJSON EpochSPDRow where
parseJSON (Object v) =
EpochSPDRow
<$> (C.EpochNo <$> v .: "epochNo")
<*> v .: "poolId"
<*> v .: "lovelace"
<*> (C.SlotNo <$> v .: "slotNo")
<*> v .: "blockHeaderHash"
<*> (C.BlockNo <$> v .: "blockNo")
parseJSON _ = mempty

instance ToJSON EpochSPDRow where
toJSON (EpochSPDRow (C.EpochNo epochNo)
poolId
lovelace
(C.SlotNo slotNo)
blockHeaderHash
(C.BlockNo blockNo)) =
object
[ "epochNo" .= epochNo
, "poolId" .= poolId
, "lovelace" .= lovelace
, "slotNo" .= slotNo
, "blockHeaderHash" .= blockHeaderHash
, "blockNo" .= blockNo
]

instance Buffered EpochSPDHandle where
-- We should only store on disk SPD from the last slot of each epoch.
persistToStorage
Expand Down
63 changes: 45 additions & 18 deletions marconi-sidechain/README.md
Expand Up @@ -217,18 +217,21 @@ OR
}
```

#### getUtxoFromAddress
#### getUtxoFromAddress (NOT IMPLEMENTED YET)

Retrieves user provided addresses.
Retrieves UTXOs of a given address until a given point in time (measured in slots).

**Parameters**:

**Parameters**: Address encoded in the Bech32 representation
* `address`: address encoded in the Bech32 format
* `slotNo`: slot number.

**Returns**: List of UTXOs linked to the provided address.
**Returns**: List of resolved UTXOs.

**Example**:

```sh
$ curl -d '{"jsonrpc": "2.0" , "method": "getUtxoFromAddress" , "params": "addr_test1qz0ru2w9suwv8mcskg8r9ws3zvguekkkx6kpcnn058pe2ql2ym0y64huzhpu0wl8ewzdxya0hj0z5ejyt3g98lpu8xxs8faq0m", "id": 1}' -H 'Content-Type: application/json' -X POST http://localhost:3000/json-rpc | jq
$ curl -d '{"jsonrpc": "2.0" , "method": "getUtxoFromAddress" , "params": { "address": "addr_test1qz0ru2w9suwv8mcskg8r9ws3zvguekkkx6kpcnn058pe2ql2ym0y64huzhpu0wl8ewzdxya0hj0z5ejyt3g98lpu8xxs8faq0m", "slotNo": 1 }, "id": 1}' -H 'Content-Type: application/json' -X POST http://localhost:3000/json-rpc | jq
{
"id": 1,
"jsonrpc": "2.0",
Expand Down Expand Up @@ -278,16 +281,19 @@ $ curl -d '{"jsonrpc": "2.0" , "method": "getUtxoFromAddress" , "params": "addr_

#### getTxWithMintingPolicy (NOT IMPLEMENTED YET)

Retrieves transactions that include a minting policy for minting/burning tokens.
Retrieves transactions that include a minting policy for minting/burning tokens until a given point in time (measured in slots).

**Parameters**: Hash of the minting policy
**Parameters**:

* `mps`: Hash of the minting policy
* `slotNo`: slot number

**Returns**: List of transaction IDs

**Example**:

```sh
$ curl -d '{"jsonrpc": "2.0" , "method": "getTxWithMintingPolicy" , "params": "...", "id": 1}' -H 'Content-Type: application/json' -X POST http://localhost:3000/json-rpc | jq
$ curl -d '{"jsonrpc": "2.0" , "method": "getTxWithMintingPolicy" , "params": { "mps": "284d60f7e56f5fd54faed4c50fd5cab0307da1c4034d6a92c5dbb940", "slotNo": 1 }, "id": 1}' -H 'Content-Type: application/json' -X POST http://localhost:3000/json-rpc | jq
{
"id": 1,
"jsonrpc": "2.0",
Expand Down Expand Up @@ -316,27 +322,48 @@ $ curl -d '{"jsonrpc": "2.0" , "method": "getTxWithMintingPolicy" , "params": ".
}
```

#### getStakePoolDelegationByEpoch (NOT IMPLEMENTED YET)
#### getStakePoolDelegationByEpoch

Retrieves the stake pool delegation per epoch.

**Parameters**: Epoch number

**Returns**: List of stake pool IDs with the amount of staked lovelace
**Returns**: List of stake pool IDs in Bech32 format with the total staked lovelace for that epoch.

**Example**:

```sh
$ curl -d '{"jsonrpc": "2.0" , "method": "getStakePoolDelegationByEpoch" , "params": "...", "id": 1}' -H 'Content-Type: application/json' -X POST http://localhost:3000/json-rpc | jq
$ curl -d '{"jsonrpc": "2.0" , "method": "getStakePoolDelegationByEpoch" , "params": 6, "id": 1}' -H 'Content-Type: application/json' -X POST http://localhost:3000/json-rpc | jq
{
"id": 1,
"jsonrpc": "2.0",
"result": [
{
"poolId": "...",
"lovelace": 10000000
}
]
"result":
[
{
"blockHeaderHash": "578f3cb70f4153e1622db792fea9005c80ff80f83df028210c7a914fb780a6f6",
"blockNo": 64903,
"epochNo": 6,
"lovelace": 100000000000000,
"poolId": "pool1z22x50lqsrwent6en0llzzs9e577rx7n3mv9kfw7udwa2rf42fa",
"slotNo": 1382422
},
{
"blockHeaderHash": "578f3cb70f4153e1622db792fea9005c80ff80f83df028210c7a914fb780a6f6",
"blockNo": 64903,
"epochNo": 6,
"lovelace": 100000000000000,
"poolId": "pool1547tew8vmuj0g6vj3k5jfddudextcw6hsk2hwgg6pkhk7lwphe6",
"slotNo": 1382422
},
{
"blockHeaderHash": "578f3cb70f4153e1622db792fea9005c80ff80f83df028210c7a914fb780a6f6",
"blockNo": 64903,
"epochNo": 6,
"lovelace": 100000000000000,
"poolId": "pool174mw7e20768e8vj4fn8y6p536n8rkzswsapwtwn354dckpjqzr8",
"slotNo": 1382422
}
]
}
```

Expand All @@ -351,7 +378,7 @@ Retrieves transactions that include a minting policy for minting/burning tokens.
**Example**:

```sh
$ curl -d '{"jsonrpc": "2.0" , "method": "getNonceByEpoch " , "params": 398, "id": 1}' -H 'Content-Type: application/json' -X POST http://localhost:3000/json-rpc | jq
$ curl -d '{"jsonrpc": "2.0" , "method": "getNonceByEpoch" , "params": 398, "id": 1}' -H 'Content-Type: application/json' -X POST http://localhost:3000/json-rpc | jq
{
"id": 1,
"jsonrpc": "2.0",
Expand Down
5 changes: 2 additions & 3 deletions marconi-sidechain/app/Main.hs
Expand Up @@ -4,7 +4,7 @@ module Main where
import Control.Concurrent.Async (race_)
import Marconi.Sidechain.Api.HttpServer qualified as Http
import Marconi.Sidechain.Api.Types (CliArgs (CliArgs, httpPort, targetAddresses))
import Marconi.Sidechain.Bootstrap (bootstrapIndexers, initializeIndexerEnv)
import Marconi.Sidechain.Bootstrap (bootstrapIndexers, initializeSidechainEnv)
import Marconi.Sidechain.CLI (parseCli)

-- | Concurrently start:
Expand All @@ -16,8 +16,7 @@ import Marconi.Sidechain.CLI (parseCli)
main :: IO ()
main = do
cli@CliArgs { httpPort, targetAddresses } <- parseCli

rpcEnv <- initializeIndexerEnv httpPort targetAddresses
rpcEnv <- initializeSidechainEnv httpPort targetAddresses

race_
(Http.bootstrap rpcEnv) -- Start HTTP server
Expand Down
24 changes: 12 additions & 12 deletions marconi-sidechain/examples/json-rpc-server/src/Main.hs
Expand Up @@ -15,24 +15,24 @@ import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (race_)
import Control.Concurrent.STM (atomically)
import Control.Lens.Operators ((^.))
import Options.Applicative (Parser, execParser, help, helper, info, long, metavar, optional, short, strOption, (<**>))
import System.FilePath ((</>))

import Marconi.ChainIndex.CLI (multiString)
import Marconi.ChainIndex.Indexers.Utxo qualified as Utxo
import Marconi.ChainIndex.Types (TargetAddresses)
import Marconi.Sidechain.Api.HttpServer qualified as Http
import Marconi.Sidechain.Api.HttpServer (bootstrap)
import Marconi.Sidechain.Api.Query.Indexers.Utxo qualified as UIQ
import Marconi.Sidechain.Api.Types (IndexerEnv, queryEnv, uiIndexer)
import Marconi.Sidechain.Bootstrap (initializeIndexerEnv)
import Marconi.Sidechain.Api.Types (SidechainEnv, sidechainAddressUtxoIndexer, sidechainEnvIndexers)
import Marconi.Sidechain.Bootstrap (initializeSidechainEnv)
import Options.Applicative (Parser, execParser, help, helper, info, long, metavar, optional, short, strOption, (<**>))
import System.FilePath ((</>))

data CliOptions = CliOptions
{ _utxoDirPath :: FilePath -- ^ Filepath to utxo sqlite database
, _addresses :: Maybe TargetAddresses
{ _utxoDirPath :: !FilePath -- ^ Filepath to utxo sqlite database
, _addresses :: !(Maybe TargetAddresses)
}

utxoDbFileName :: String
utxoDbFileName = "utxodb"

cliParser :: Parser CliOptions
cliParser = CliOptions
<$> strOption (long "utxo-db"
Expand All @@ -54,16 +54,16 @@ main = do
<>"\nport =" <> show (3000 :: Int)
<> "\nmarconi-db-dir =" <> dbpath
<> "\nnumber of addresses to index = " <> show (length <$> addresses)
env <- initializeIndexerEnv Nothing addresses
race_ (Http.bootstrap env) (mocUtxoIndexer dbpath (env ^. queryEnv) )
env <- initializeSidechainEnv Nothing addresses
race_ (bootstrap env) (mocUtxoIndexer dbpath env)

-- | 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 -> IndexerEnv -> IO ()
mocUtxoIndexer :: FilePath -> SidechainEnv -> IO ()
mocUtxoIndexer dbpath env =
Utxo.open dbpath (Utxo.Depth 4) >>= callback >> innerLoop
where
callback :: Utxo.UtxoIndexer -> IO ()
callback = atomically . UIQ.writeTMVar' (env ^. uiIndexer)
callback = atomically . UIQ.updateEnvState (env ^. sidechainEnvIndexers . sidechainAddressUtxoIndexer)
innerLoop = threadDelay 1000000 >> innerLoop -- create some latency
4 changes: 4 additions & 0 deletions marconi-sidechain/marconi-sidechain.cabal
Expand Up @@ -45,11 +45,13 @@ library
hs-source-dirs: src
exposed-modules:
Marconi.Sidechain.Api.HttpServer
Marconi.Sidechain.Api.Query.Indexers.EpochSPD
Marconi.Sidechain.Api.Query.Indexers.Utxo
Marconi.Sidechain.Api.Routes
Marconi.Sidechain.Api.Types
Marconi.Sidechain.Bootstrap
Marconi.Sidechain.CLI
Marconi.Sidechain.Utils

--------------------
-- Local components
Expand All @@ -69,6 +71,7 @@ library
build-depends:
, aeson
, base >=4.9 && <5
, containers
, filepath
, lens
, optparse-applicative
Expand All @@ -77,6 +80,7 @@ library
, stm >=2.5
, text
, time
, vector
, warp

executable marconi-sidechain
Expand Down
55 changes: 33 additions & 22 deletions marconi-sidechain/src/Marconi/Sidechain/Api/HttpServer.hs
Expand Up @@ -15,10 +15,12 @@ import Data.Proxy (Proxy (Proxy))
import Data.Text (Text, pack)
import Data.Time (defaultTimeLocale, formatTime, getCurrentTime)
import Data.Word (Word64)
import Marconi.Sidechain.Api.Query.Indexers.EpochSPD qualified as EpochSPD
import Marconi.Sidechain.Api.Query.Indexers.Utxo qualified as Q.Utxo
import Marconi.Sidechain.Api.Routes (API, AddressUtxoResult, CurrentSyncedPointResult, EpochNonceResult,
EpochStakePoolDelegationResult, JsonRpcAPI, MintingPolicyHashTxResult, RestAPI)
import Marconi.Sidechain.Api.Types (HasSidechainEnv (httpSettings, queryEnv), IndexerEnv, QueryExceptions, SidechainEnv)
import Marconi.Sidechain.Api.Types (QueryExceptions, SidechainEnv, sidechainAddressUtxoIndexer,
sidechainEnvHttpSettings, sidechainEnvIndexers)
import Network.JsonRpc.Server.Types ()
import Network.JsonRpc.Types (JsonRpcErr (JsonRpcErr, errorCode, errorData, errorMessage), parseErrorCode)
import Network.Wai.Handler.Warp (runSettings)
Expand All @@ -28,14 +30,14 @@ import Servant.Server (Application, Handler, Server, serve)
-- | Bootstraps the HTTP server
bootstrap :: SidechainEnv -> IO ()
bootstrap env = runSettings
(env ^. httpSettings)
(marconiApp (env ^. queryEnv))
(env ^. sidechainEnvHttpSettings)
(marconiApp env)

marconiApp :: IndexerEnv -> Application
marconiApp :: SidechainEnv -> Application
marconiApp env = serve (Proxy @API) (httpRpcServer env)

jsonRpcServer
:: IndexerEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
:: SidechainEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
-> Server JsonRpcAPI
jsonRpcServer env = echo
:<|> getTargetAddressesQueryHandler env
Expand All @@ -46,12 +48,12 @@ jsonRpcServer env = echo
:<|> getEpochNonceHandler env

restApiServer
:: IndexerEnv
:: SidechainEnv
-> Server RestAPI
restApiServer env = getTimeHandler :<|> getTargetAddressesHandler env

httpRpcServer
:: IndexerEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
:: SidechainEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
-> Server API
httpRpcServer env = jsonRpcServer env :<|> restApiServer env

Expand All @@ -68,58 +70,67 @@ getTimeHandler = timeString <$> liftIO getCurrentTime
where
timeString = formatTime defaultTimeLocale "%T"

-- | prints TargetAddresses Bech32 representation to the console
-- | Prints TargetAddresses Bech32 representation to the console
getTargetAddressesHandler
:: IndexerEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
:: SidechainEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
-> Handler [Text]
getTargetAddressesHandler = pure . Q.Utxo.reportBech32Addresses
getTargetAddressesHandler env =
pure $ Q.Utxo.reportBech32Addresses
$ env ^. sidechainEnvIndexers . sidechainAddressUtxoIndexer

-- | prints TargetAddresses Bech32 representation as thru JsonRpc
getTargetAddressesQueryHandler
:: IndexerEnv -- ^ database configuration
:: SidechainEnv -- ^ database configuration
-> String
-- ^ Will always be an empty string as we are ignoring this param, and returning everything
-> Handler (Either (JsonRpcErr String) [Text])
getTargetAddressesQueryHandler env _ = pure . Right . Q.Utxo.reportBech32Addresses $ env
getTargetAddressesQueryHandler env _ =
pure $ Right
$ Q.Utxo.reportBech32Addresses (env ^. sidechainEnvIndexers . sidechainAddressUtxoIndexer)

-- | Handler for retrieving current synced chain point.
getCurrentSyncedPointHandler
:: IndexerEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
:: SidechainEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
-> String -- ^ Dummy parameter
-> Handler (Either (JsonRpcErr String) CurrentSyncedPointResult)
getCurrentSyncedPointHandler _ _ =
pure $ Left $ JsonRpcErr 1 "Endpoint not implemented yet" Nothing

-- | Handler for retrieving UTXOs by Address
getAddressUtxoHandler
:: IndexerEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
-> String -- ^ Bech32 addressCredential
:: SidechainEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
-> String -- ^ Bech32 addressCredential
-> Handler (Either (JsonRpcErr String) AddressUtxoResult)
getAddressUtxoHandler env address = liftIO $
first toRpcErr <$> (Q.Utxo.findByBech32Address env . pack $ address)
first toRpcErr
<$> Q.Utxo.findByBech32Address
(env ^. sidechainEnvIndexers . sidechainAddressUtxoIndexer)
(pack address)

-- | Handler for retrieving TXs by Minting Policy Hash.
-- | Handler for retrieving Txs by Minting Policy Hash.
getMintingPolicyHashTxHandler
:: IndexerEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
:: SidechainEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
-> String -- ^ Minting policy hash
-> Handler (Either (JsonRpcErr String) MintingPolicyHashTxResult)
getMintingPolicyHashTxHandler _ _ = pure $ Left $ JsonRpcErr 1 "Endpoint not implemented yet" Nothing

-- | Handler for retrieving stake pool delegation per epoch
getEpochStakePoolDelegationHandler
:: IndexerEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
:: SidechainEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
-> Word64 -- ^ EpochNo
-> Handler (Either (JsonRpcErr String) EpochStakePoolDelegationResult)
getEpochStakePoolDelegationHandler _ _ = pure $ Left $ JsonRpcErr 1 "Endpoint not implemented yet" Nothing
getEpochStakePoolDelegationHandler env epochNo = liftIO $
first toRpcErr
<$> EpochSPD.querySPDByEpochNo env epochNo

-- | Handler for retrieving stake pool delegation per epoch
getEpochNonceHandler
:: IndexerEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
:: SidechainEnv -- ^ Utxo Environment to access Utxo Storage running on the marconi thread
-> Word64 -- ^ EpochNo
-> Handler (Either (JsonRpcErr String) EpochNonceResult)
getEpochNonceHandler _ _ = pure $ Left $ JsonRpcErr 1 "Endpoint not implemented yet" Nothing

-- | convert form to Jsonrpc protocal error
-- | Convert to JSON-RPC protocol error.
toRpcErr
:: QueryExceptions
-> JsonRpcErr String
Expand Down

0 comments on commit a6b95b0

Please sign in to comment.