Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f625f86
feat(rpc): runtime api calls
Karrq Nov 6, 2025
f8038ea
fix(rpc): remove now unused RPC method
Karrq Nov 7, 2025
b66af47
feat(rpc): storage query with subxt
Karrq Nov 11, 2025
4c09dbc
refactor: ditch subxt, import runtime
Karrq Nov 13, 2025
b6e8e20
Merge remote-tracking branch 'origin/main' into feat/stats
Karrq Nov 13, 2025
e68890d
chore: typo
Karrq Nov 14, 2025
7c92b0f
chore: cleanup dead code
Karrq Nov 14, 2025
0df73a0
Merge remote-tracking branch 'origin/main' into feat/stats
Karrq Nov 14, 2025
c999285
Merge branch 'main' into feat/stats
TDemeco Nov 25, 2025
3f16c7c
feat: :art: add type safety to runtime API calls
TDemeco Nov 26, 2025
d7eb46c
fix: :art: improve runtime API and state query calls
TDemeco Nov 26, 2025
15d23e5
Merge branch 'main' into feat/stats
TDemeco Nov 26, 2025
6c25364
fix: :arrow_up: update Cargo.lock
TDemeco Nov 26, 2025
72458ae
fix: :arrow_down: downgrade `home` to rust 1.87 compatible version
TDemeco Nov 26, 2025
3d69ae8
fix: :fire: remove unused dependencies
TDemeco Nov 26, 2025
81157fd
Merge branch 'main' into feat/stats
TDemeco Nov 26, 2025
54d0494
test: :white_check_mark: fix integration tests
TDemeco Nov 26, 2025
3fe6560
test: :white_check_mark: improve payment stream backend test to actua…
TDemeco Nov 28, 2025
324877f
test: :white_check_mark: add basic checks to the rest of the fields o…
TDemeco Nov 28, 2025
9f1140f
fix: :fire: remove leftover comment
TDemeco Nov 28, 2025
12b3428
Merge branch 'main' into feat/stats
TDemeco Nov 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,216 changes: 1,128 additions & 1,088 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ axum-test = "18"
base64 = "0.21"
bigdecimal = { version = "0.4.5", features = ["serde"] }
bincode = "1.3.3"
cfg-if = { version = "1.0.4" }
clap = { version = "4.5.3", features = ["derive", "env"] }
chrono = "0.4"
codec = { package = "parity-scale-codec", version = "3.0.0", features = [
Expand Down Expand Up @@ -94,7 +95,6 @@ reference-trie = "0.29.1"
rustls = { version = "0.23", default-features = false, features = ["ring"] }
rustls-platform-verifier = "0.5"
rustls-pemfile = "2.2"

scale-info = { version = "2.11.0", default-features = false, features = [
"derive",
] }
Expand Down
35 changes: 22 additions & 13 deletions backend/lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,36 @@ version = "0.1.1"
edition = "2021"

[dependencies]
alloy-core = { version = "0.8", features = ["serde"] }
alloy-signer = "0.8"
anyhow = { workspace = true }
async-trait = { workspace = true }
axum = { workspace = true }
axum-extra = { workspace = true }
axum-jwt = { workspace = true }
base64 = { workspace = true }
bigdecimal = { workspace = true }
cfg-if = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
codec = { workspace = true }
diesel = { workspace = true }
diesel-async = { workspace = true }
futures = { workspace = true }
alloy-core = { version = "0.8", features = ["serde"] }
alloy-signer = "0.8"
frame-support = { workspace = true }
headers = { workspace = true }
hex = { workspace = true }
hex-literal = { workspace = true }
jsonrpsee = { workspace = true, features = ["client", "ws-client"] }
jsonwebtoken = { workspace = true }
parking_lot = { workspace = true }
rand = { workspace = true }
reqwest = { version = "0.12", default-features = false, features = [
"rustls-tls",
"stream",
] }
rustls = { workspace = true }
rustls-pemfile = { workspace = true }
rustls-platform-verifier = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
Expand All @@ -35,20 +45,13 @@ tower-http = { workspace = true }
tracing = { workspace = true }
tracing-bunyan-formatter = { workspace = true }
tracing-subscriber = { workspace = true }
rustls = { workspace = true }
rustls-platform-verifier = { workspace = true }
trie-db = { workspace = true }
tokio-postgres = { workspace = true }
tokio-postgres-rustls = { workspace = true }
rustls-pemfile = { workspace = true }
reqwest = { version = "0.12", default-features = false, features = [
"rustls-tls",
"stream",
] }
uuid = { version = "1.18", features = ["v7"] }
serde_with = "3.15.1"

# Workspace dependencies
codec = { workspace = true }
sc-network = { workspace = true }
sc-network-types = { workspace = true }
shc-common = { workspace = true, default-features = true }
Expand All @@ -61,7 +64,10 @@ shp-types = { workspace = true }
sp-core = { workspace = true }
sp-runtime = { workspace = true }
sp-trie = { workspace = true }
trie-db = { workspace = true }
pallet-storage-providers = { workspace = true, features = ["std"] }

# Optional deps
sh-solochain-evm-runtime = { workspace = true, features = ["std"], optional = true }

[dev-dependencies]
alloy-signer = "0.8"
Expand All @@ -73,5 +79,8 @@ testcontainers = "0.24"
testcontainers-modules = { version = "0.12", features = ["postgres"] }

[features]
default = []
mocks = []
default = ["solochain"]
mocks = ["solochain"]

# Runtime selection
solochain = ["dep:sh-solochain-evm-runtime"]
127 changes: 122 additions & 5 deletions backend/lib/src/data/rpc/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@

use std::sync::Arc;

use bigdecimal::BigDecimal;
use codec::{Decode, Encode};
use jsonrpsee::core::traits::ToRpcParams;
use serde::de::DeserializeOwned;
use tracing::debug;

use shc_indexer_db::OnchainMspId;
use shc_rpc::{
GetFileFromFileStorageResult, GetValuePropositionsResult, RpcProviderId, SaveFileToDisk,
};

use crate::data::rpc::{connection::error::RpcResult, methods, AnyRpcConnection, RpcConnection};
use crate::data::rpc::{
connection::error::{RpcConnectionError, RpcResult},
methods, runtime_apis, state_queries, AnyRpcConnection, RpcConnection,
};

/// StorageHub RPC client that uses an RpcConnection
pub struct StorageHubRpcClient {
Expand Down Expand Up @@ -56,17 +62,124 @@ impl StorageHubRpcClient {
self.connection.call_no_params(method).await
}

/// Wrapper over [`call`] for runtime APIs
///
/// # Type Parameters:
/// - `RuntimeApiCallType` is a type that implements `RuntimeApiCallTypes`, which encodes both
/// the parameter type and return type for the API call.
///
/// # Arguments:
/// - `params` is the set of parameters for the runtime API call
pub async fn call_runtime_api<RuntimeApiCallType>(
&self,
params: RuntimeApiCallType::Params,
) -> RpcResult<RuntimeApiCallType::ReturnType>
where
RuntimeApiCallType: runtime_apis::RuntimeApiCallTypes,
{
// Get the runtime API call variant
let runtime_api_call = RuntimeApiCallType::runtime_api_call();

// The RPC method expects the parameters to be a hex-encoded SCALE-encoded string
let encoded = format!("0x{}", hex::encode(params.encode()));
debug!(method = %runtime_api_call.method_name(), ?encoded, "calling runtime api");

let response = self
.call::<_, String>(
methods::API_CALL,
jsonrpsee::rpc_params![runtime_api_call.method_name(), encoded],
)
.await?;

// The RPC also replies with a hex-encoded SCALE-encoded response
let response = hex::decode(response.trim_start_matches("0x")).map_err(|e| {
RpcConnectionError::Serialization(format!(
"RPC runtime API did not respond with a valid hex string: {}",
e.to_string()
))
})?;

RuntimeApiCallType::ReturnType::decode(&mut response.as_slice())
.map_err(|e| RpcConnectionError::Serialization(e.to_string()))
}

/// Wrapper over [`call`] for reading storage keys
///
/// # Type Parameters:
/// - `QueryType` is a type that implements `StorageQueryTypes`, which encodes both
/// the key parameters and the value type for the storage query.
///
/// # Arguments:
/// - `params` are the parameters needed to generate the storage key
pub async fn query_storage<QueryType>(
&self,
params: QueryType::KeyParams,
) -> RpcResult<Option<QueryType::Value>>
where
QueryType: state_queries::StorageQueryTypes,
{
let key = QueryType::storage_key(params);
let encoded = format!("0x{}", hex::encode(&key.0));
debug!(key = encoded, "reading storage key");

let response = self
.call::<_, Option<String>>(methods::STATE_QUERY, jsonrpsee::rpc_params![encoded])
.await?;

let Some(response) = response else {
return Ok(None);
};

// The RPC replies with a hex-encoded SCALE-encoded response
let response = hex::decode(response.trim_start_matches("0x")).map_err(|e| {
RpcConnectionError::Serialization(format!(
"RPC runtime API did not respond with a valid hex string: {}",
e.to_string()
))
})?;

QueryType::Value::decode(&mut response.as_slice())
.map(Some)
.map_err(|e| RpcConnectionError::Serialization(e.to_string()))
}

// RPC Calls:
// TODO: Explore the possibility of directly using StorageHubClientApi trait
// from the client's RPC module to avoid having to manually implement new RPC calls

/// Get the current price per giga unit per tick
///
/// Returns the price value (u128) that represents the cost per giga unit per tick
/// Returns the price value that represents the cost per giga unit per tick
/// in the StorageHub network.
pub async fn get_current_price_per_giga_unit_per_tick(&self) -> RpcResult<u128> {
pub async fn get_current_price_per_giga_unit_per_tick(&self) -> RpcResult<BigDecimal> {
debug!(target: "rpc::client::get_current_price_per_giga_unit_per_tick", "RPC call: get_current_price_per_giga_unit_per_tick");

self.call_no_params(methods::CURRENT_PRICE).await
self.call_runtime_api::<runtime_apis::GetCurrentPricePerGigaUnitPerTick>(())
.await
.map(|price| price.into())
}
Comment on lines +154 to +160
Copy link
Contributor

@HermanObst HermanObst Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC this method is not being tested, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same answer as above


/// Retrieve the current number of active users
pub async fn get_number_of_active_users(&self, provider: OnchainMspId) -> RpcResult<usize> {
debug!(target: "rpc::client::get_number_of_active_users", "Runtime API: get_users_of_payment_streams_of_provider");

self.call_runtime_api::<runtime_apis::GetUsersOfPaymentStreamsOfProvider>(
*provider.as_h256(),
)
.await
.map(|users| users.len())
}

/// Retrieve the MSP information for the given provider
///
/// This function will read into the chain state from the Provider pallet's MainStorageProviders map
pub async fn get_msp_info(
&self,
provider: OnchainMspId,
) -> RpcResult<Option<state_queries::MspInfo>> {
debug!(target: "rpc::client::get_msp", provider = %provider, "State Query: get_msp_info");
self.query_storage::<state_queries::MspInfoQuery>(provider)
.await
}

/// Returns whether the given `file_key` is expected to be received by the MSP node
Expand Down Expand Up @@ -151,6 +264,7 @@ impl StorageHubRpcClient {

#[cfg(all(test, feature = "mocks"))]
mod tests {
use bigdecimal::Signed;
use codec::Decode;

use shp_types::Hash;
Expand Down Expand Up @@ -211,7 +325,10 @@ mod tests {
.get_current_price_per_giga_unit_per_tick()
.await
.expect("able to retrieve current price per giga unit");
assert!(price > 0);
assert!(
price.is_positive(),
"Price per giga unit should always be > 0"
);
}

#[tokio::test]
Expand Down
4 changes: 3 additions & 1 deletion backend/lib/src/data/rpc/methods.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
pub const CURRENT_PRICE: &str = "storagehubclient_getCurrentPricePerGigaUnitPerTick";
pub const SAVE_FILE_TO_DISK: &str = "storagehubclient_saveFileToDisk";
pub const FILE_KEY_EXPECTED: &str = "storagehubclient_isFileKeyExpected";
pub const IS_FILE_IN_FILE_STORAGE: &str = "storagehubclient_isFileInFileStorage";
pub const RECEIVE_FILE_CHUNKS: &str = "storagehubclient_receiveBackendFileChunks";
pub const PROVIDER_ID: &str = "storagehubclient_getProviderId";
pub const VALUE_PROPS: &str = "storagehubclient_getValuePropositions";
pub const PEER_IDS: &str = "system_localListenAddresses";

pub const API_CALL: &str = "state_call";
pub const STATE_QUERY: &str = "state_getStorage";
Loading
Loading