Skip to content

Commit 5ae2ca3

Browse files
authored
Merge pull request #2367 from oasisprotocol/ptrus/feature/rofl-appd-queries
rofl-appd: Support runtime state queries
2 parents e4afc0e + 61fc5ab commit 5ae2ca3

6 files changed

Lines changed: 132 additions & 0 deletions

File tree

rofl-appd/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ where
4646
routes![routes::metadata::set, routes::metadata::get],
4747
);
4848

49+
let server = server.mount("/rofl/v1/query", routes![routes::query::query]);
50+
4951
#[cfg(feature = "tx")]
5052
let server = server
5153
.manage(routes::tx::Config::default())

rofl-appd/src/routes/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
pub mod app;
22
pub mod keys;
33
pub mod metadata;
4+
pub mod query;
45
#[cfg(feature = "tx")]
56
pub mod tx;

rofl-appd/src/routes/query.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use std::sync::Arc;
2+
3+
use rocket::{http::Status, serde::json::Json, State};
4+
use serde_with::serde_as;
5+
6+
use crate::state::Env;
7+
8+
/// Query request.
9+
#[serde_as]
10+
#[derive(Clone, Debug, serde::Deserialize)]
11+
pub struct QueryRequest {
12+
/// Method name.
13+
pub method: String,
14+
/// CBOR encoded arguments.
15+
#[serde_as(as = "serde_with::hex::Hex")]
16+
pub args: Vec<u8>,
17+
}
18+
19+
/// Query response.
20+
#[serde_as]
21+
#[derive(Clone, Default, serde::Serialize)]
22+
pub struct QueryResponse {
23+
/// Raw response data.
24+
#[serde_as(as = "serde_with::hex::Hex")]
25+
pub data: Vec<u8>,
26+
}
27+
28+
/// Query submits a query to the registration paratime.
29+
#[rocket::post("/", data = "<body>")]
30+
pub async fn query(
31+
body: Json<QueryRequest>,
32+
env: &State<Arc<dyn Env>>,
33+
) -> Result<Json<QueryResponse>, (Status, String)> {
34+
let result = env
35+
.query(&body.method, body.args.clone())
36+
.await
37+
.map_err(|err| (Status::InternalServerError, format!("Query failed: {err}")))?;
38+
39+
// Return the response.
40+
let response = QueryResponse { data: result };
41+
42+
Ok(Json(response))
43+
}

rofl-appd/src/state.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ pub trait Env: Send + Sync {
1717
tx: transaction::Transaction,
1818
opts: SubmitTxOpts,
1919
) -> Result<transaction::CallResult>;
20+
21+
/// Securely query the on-chain paratime state.
22+
async fn query(&self, method: &str, args: Vec<u8>) -> Result<Vec<u8>>;
2023
}
2124

2225
pub(crate) struct EnvImpl<A: App> {
@@ -50,4 +53,11 @@ impl<A: App> Env for EnvImpl<A> {
5053
.multi_sign_and_submit_tx_opts(&[signer], tx, opts)
5154
.await
5255
}
56+
57+
async fn query(&self, method: &str, args: Vec<u8>) -> Result<Vec<u8>> {
58+
let args: cbor::Value = cbor::from_slice(&args)?;
59+
let round = self.env.client().latest_round().await?;
60+
let result: cbor::Value = self.env.client().query(round, method, args).await?;
61+
Ok(cbor::to_vec(result))
62+
}
5363
}

rofl-client/py/src/oasis_rofl_client/rofl_client.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,25 @@ async def set_metadata(self, metadata: dict[str, str]) -> None:
145145
"""
146146
path: str = "/rofl/v1/metadata"
147147
await self._appd_request("POST", path, metadata)
148+
149+
async def query(self, method: str, args: bytes) -> bytes:
150+
"""Query the on-chain paratime state.
151+
152+
Args:
153+
method: The query method name
154+
args: CBOR-encoded query arguments
155+
156+
Returns:
157+
CBOR-encoded response data
158+
159+
Raises:
160+
httpx.HTTPStatusError: If the request fails
161+
"""
162+
payload: dict[str, str] = {
163+
"method": method,
164+
"args": args.hex(),
165+
}
166+
167+
path: str = "/rofl/v1/query"
168+
response: dict[str, str] = await self._appd_request("POST", path, payload)
169+
return bytes.fromhex(response["data"])

rofl-client/py/tests/test_client.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,60 @@ async def test_get_metadata_with_http_url(self, mock_client_class):
286286
"https://rofl.example.com/rofl/v1/metadata", timeout=60.0
287287
)
288288

289+
@patch("oasis_rofl_client.rofl_client.httpx.AsyncClient")
290+
async def test_query(self, mock_client_class):
291+
"""Test query method."""
292+
# Setup mock
293+
mock_response = MagicMock()
294+
mock_response.json.return_value = {"data": "48656c6c6f"} # "Hello" in hex
295+
mock_response.raise_for_status = MagicMock()
296+
297+
mock_client = AsyncMock()
298+
mock_client.post.return_value = mock_response
299+
mock_client_class.return_value.__aenter__.return_value = mock_client
300+
301+
# Test query
302+
client = RoflClient()
303+
args = b"\xa1\x64test\x65value" # CBOR-encoded test data
304+
result = await client.query("test.Method", args)
305+
306+
# Verify the result
307+
self.assertEqual(result, b"Hello")
308+
309+
# Verify the API call
310+
mock_client.post.assert_called_once_with(
311+
"http://localhost/rofl/v1/query",
312+
json={"method": "test.Method", "args": args.hex()},
313+
timeout=60.0,
314+
)
315+
316+
@patch("oasis_rofl_client.rofl_client.httpx.AsyncClient")
317+
async def test_query_with_http_url(self, mock_client_class):
318+
"""Test query method with HTTP URL."""
319+
# Setup mock
320+
mock_response = MagicMock()
321+
mock_response.json.return_value = {"data": "deadbeef"}
322+
mock_response.raise_for_status = MagicMock()
323+
324+
mock_client = AsyncMock()
325+
mock_client.post.return_value = mock_response
326+
mock_client_class.return_value.__aenter__.return_value = mock_client
327+
328+
# Test with HTTP URL
329+
client = RoflClient(url="https://rofl.example.com")
330+
args = b"\x00\x01\x02"
331+
result = await client.query("state.Query", args)
332+
333+
# Verify the result
334+
self.assertEqual(result, bytes.fromhex("deadbeef"))
335+
336+
# Verify the API call uses the custom URL
337+
mock_client.post.assert_called_once_with(
338+
"https://rofl.example.com/rofl/v1/query",
339+
json={"method": "state.Query", "args": "000102"},
340+
timeout=60.0,
341+
)
342+
289343

290344
if __name__ == "__main__":
291345
unittest.main()

0 commit comments

Comments
 (0)