Repository: scs/substrate-api-client (language: rust)

Issue:
Support Runtime Api calls
So far the api-client does not support runtime api calls. The goal of this issue is to facilitate runtime api calls, in general, and provide helper functions for the runtime api calls of the most commonly used pallets.

Available Runtime Calls: https://polkadot.js.org/docs/polkadot/runtime

Hands on testing: https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frpc.dotters.network%2Fkusama#/runtime

Subxt has already put some thoughts into it: see https://github.com/paritytech/subxt/pull/918

### Task description
Implement helper functions, just like our rpc calls 
https://github.com/scs/substrate-api-client/blob/53be73d9faf72ba492cb3af9a163f2e26d2f8619/src/api/rpc_api/frame_system.rs#L73-L81

that directly access the node api and return the information. 

Example code from subxt:
```rust
// Create a dynamically runtime API payload that calls the
// `AccountNonceApi_account_nonce` function.
let account = dev::alice().public_key();
let runtime_api_call = subxt::dynamic::runtime_api_call(
    "AccountNonceApi",
    "account_nonce",
    vec![Value::from_bytes(account)],
);
```

And as reference: here's the call executed by the rpc: https://github.com/paritytech/subxt/blob/9a8fc33f2fe830009011e997773071099101d69b/subxt/src/runtime_api/runtime_types.rs#L43-L106


### Already implemented:
- Support for Metadata v15
- Api Metadata (maybe needs adaption)
https://github.com/scs/substrate-api-client/blob/53be73d9faf72ba492cb3af9a163f2e26d2f8619/node-api/src/metadata/metadata_types.rs#L48


Discussion / hints:
Up to date Substrate nodes should already include metadata v15 by default. So this feature should (once the Susbtrate deps are upgraded) already be testable by CI (see PR https://github.com/paritytech/substrate/pull/14481 for more information)
To me it's not clear: do we implement all calls manually? Or do we generate code? If we generate code automatically, do we do this frequently/whenever there are changes in the runtime api call?
Currently, we are not generating code at all in the api-client. So for now, I'd stick to hard coded helper functions and one function that allows a generic "String" input... or Probably two Strings, as in polkadot.js:
![grafik](https://github.com/scs/substrate-api-client/assets/73821294/d5e6f1d8-6ba1-47a7-958e-d406770c605a)

If we want to go more in the direction of code producing code, I believe that should be addressed in a different issue, as it also affects the already implemented RPC calls.


Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 03b940cd2..37a3bc5da 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -163,10 +163,11 @@ jobs:
           keystore_tests,
           pallet_balances_tests,
           pallet_transaction_payment_tests,
-          state_tests,
+          runtime_api_tests,
           tungstenite_client_test,
           ws_client_test,
           state_tests,
+          query_runtime_api,
           runtime_update_sync,
           runtime_update_async,
         ]
diff --git a/Cargo.lock b/Cargo.lock
index 49587f216..7a311fa87 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -7194,6 +7194,7 @@ dependencies = [
  "serde_json",
  "sp-core",
  "sp-crypto-hashing",
+ "sp-inherents",
  "sp-runtime",
  "sp-runtime-interface 24.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?branch=master)",
  "sp-storage 19.0.0 (git+https://github.com/paritytech/polkadot-sdk.git?branch=master)",
diff --git a/Cargo.toml b/Cargo.toml
index 4e1fe7485..4855d12b0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -49,6 +49,7 @@ ws = { version = "0.9.2", optional = true, features = ["ssl"] }
 # Substrate no_std dependencies
 sp-core = { default-features = false, features = ["full_crypto", "serde"], git = "https://github.com/paritytech/polkadot-sdk.git", branch = "master" }
 sp-crypto-hashing = { default-features = false, git = "https://github.com/paritytech/polkadot-sdk.git", branch = "master" }
+sp-inherents = { default-features = false, git = "https://github.com/paritytech/polkadot-sdk.git", branch = "master" }
 sp-runtime = { default-features = false, features = ["serde"], git = "https://github.com/paritytech/polkadot-sdk.git", branch = "master" }
 sp-runtime-interface = { default-features = false, git = "https://github.com/paritytech/polkadot-sdk.git", branch = "master" }
 sp-storage = { default-features = false, features = ["serde"], git = "https://github.com/paritytech/polkadot-sdk.git", branch = "master" }
diff --git a/README.md b/README.md
index 0c4e81fd0..f3d602186 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,7 @@ The following async examples can be found in the [async examples](/examples/asyn
 * [get_blocks](/examples/async/examples/get_blocks.rs): Read header, block and signed block from storage.
 * [get_storage](/examples/async/examples/get_storage.rs): Read storage values.
 * [print_metadata](/examples/async/examples/print_metadata.rs): Print the metadata of the node in a readable way.
+* [query_runtime_api](/src/examples/async/examples/query_runtime_api.rs): How to query the runtime api.
 * [runtime_update_async](/examples/async/examples/runtime_update_async.rs): How to do an runtime upgrade asynchronously.
 * [staking_batch_payout](/examples/async/examples/staking_batch_payout.rs): Batch reward payout for validator.
 * [subscribe_events](/examples/async/examples/subscribe_events.rs): Subscribe and react on events.
diff --git a/examples/async/examples/query_runtime_api.rs b/examples/async/examples/query_runtime_api.rs
new file mode 100644
index 000000000..688dc1384
--- /dev/null
+++ b/examples/async/examples/query_runtime_api.rs
@@ -0,0 +1,99 @@
+/*
+	Copyright 2024 Supercomputing Systems AG
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+		http://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+//! Very simple example that shows how to query Runtime Api of a Substrate node.
+
+use codec::Encode;
+use sp_core::sr25519;
+use sp_keyring::AccountKeyring;
+use substrate_api_client::{
+	ac_primitives::AssetRuntimeConfig,
+	extrinsic::BalancesExtrinsics,
+	rpc::JsonrpseeClient,
+	runtime_api::{AuthorityDiscoveryApi, CoreApi, MetadataApi, RuntimeApi, TransactionPaymentApi},
+	Api, GetChainInfo,
+};
+
+// To test this example with CI we run it against the Substrate kitchensink node, which uses the asset pallet.
+// Therefore, we need to use the `AssetRuntimeConfig` in this example.
+// ! However, most Substrate runtimes do not use the asset pallet at all. So if you run an example against your own node
+// you most likely should use `DefaultRuntimeConfig` instead.
+
+#[tokio::main]
+async fn main() {
+	env_logger::init();
+
+	// Initialize the api, which retrieves the metadata from the node upon initialization.
+	let client = JsonrpseeClient::with_default_url().await.unwrap();
+	let mut api = Api::<AssetRuntimeConfig, _>::new(client).await.unwrap();
+	let alice_pair = AccountKeyring::Alice.pair();
+	api.set_signer(alice_pair.into());
+	let runtime_api = api.runtime_api();
+
+	// Query the fee of an extrinsic.
+	let bob = AccountKeyring::Bob.to_account_id();
+	let balance_extrinsic = api.balance_transfer_allow_death(bob.clone().into(), 1000).await;
+	let extrinsic_fee_details = runtime_api
+		.query_fee_details(balance_extrinsic.clone(), 1000, None)
+		.await
+		.unwrap();
+	let final_fee = extrinsic_fee_details.final_fee();
+	println!("To exceute the balance extrinsic, the following fee is required: {:?}", final_fee);
+
+	// Get the authority Ids.
+	let authority_ids: Vec<sr25519::Public> = runtime_api.authorities(None).await.unwrap();
+	println!("The following authorities are currently active:");
+	for authority in authority_ids {
+		println!("{:?}", authority);
+	}
+
+	// Query the runtime api version.
+	let version = runtime_api.version(None).await.unwrap();
+	println!("{:?}", version);
+
+	// Query the available metadata versions.
+	let metadata_versions = runtime_api.metadata_versions(None).await.unwrap();
+	assert_eq!(metadata_versions, [14, 15]);
+
+	// List all apis and functions thereof.
+	let trait_names = runtime_api.list_traits(None).await.unwrap();
+	println!();
+	println!("Available traits:");
+	for name in trait_names {
+		println!("{name}");
+	}
+	println!();
+
+	let trait_name = "BabeApi";
+	let method_names = runtime_api.list_methods_of_trait(trait_name, None).await.unwrap();
+	println!("Available methods of {trait_name}:");
+	for name in method_names {
+		println!("{name}");
+	}
+	println!();
+
+	// Create your own runtime api call.
+	let parameters = vec![1000.encode()];
+	let latest_block_hash = api.get_block_hash(None).await.unwrap().unwrap();
+	let result: Result<u128, substrate_api_client::Error> = runtime_api
+		.runtime_call(
+			"TransactionPaymentApi_query_length_to_fee",
+			parameters,
+			Some(latest_block_hash),
+		)
+		.await;
+	let output = result.unwrap();
+	println!("Received the following output: {:?}", output);
+}
diff --git a/primitives/src/types.rs b/primitives/src/types.rs
index 8cbe6cf76..dff686230 100644
--- a/primitives/src/types.rs
+++ b/primitives/src/types.rs
@@ -18,7 +18,7 @@
 //! Re-defintion of substrate primitives.
 //! Needed because substrate pallets compile to wasm in no_std.
 
-use alloc::string::String;
+use alloc::{string::String, vec::Vec};
 use codec::{Decode, Encode, MaxEncodedLen};
 use scale_info::TypeInfo;
 use serde::{Deserialize, Serialize};
@@ -299,3 +299,63 @@ pub enum ChainType {
 /// Arbitrary properties defined in chain spec as a JSON object
 // https://github.com/paritytech/substrate/blob/c172d0f683fab3792b90d876fd6ca27056af9fe9/client/chain-spec/src/lib.rs#L215-L216
 pub type Properties = serde_json::map::Map<String, serde_json::Value>;
+
+// Merkle-mountain-range primitives. sp-mmr-primitives does not seem to be no-std compatible as of now.
+// Might be caused by the thiserror import in the toml. Parity probably will accept a PR if opened.
+
+/// A type-safe wrapper for the concrete leaf type.
+///
+/// This structure serves merely to avoid passing raw `Vec<u8>` around.
+/// It must be `Vec<u8>`-encoding compatible.
+// https://github.com/paritytech/polkadot-sdk/blob/a190e0e9253562fdca9c1b6e9541a7ea0a50c018/substrate/primitives/merkle-mountain-range/src/lib.rs#L138-L146
+#[derive(codec::Encode, codec::Decode, PartialEq, Eq, TypeInfo, Clone)]
+pub struct EncodableOpaqueLeaf(pub Vec<u8>);
+
+/// An MMR proof data for a group of leaves.
+// https://github.com/paritytech/polkadot-sdk/blob/a190e0e9253562fdca9c1b6e9541a7ea0a50c018/substrate/primitives/merkle-mountain-range/src/lib.rs#L351-L360
+#[derive(codec::Encode, codec::Decode, RuntimeDebug, Clone, PartialEq, Eq, TypeInfo)]
+pub struct Proof<Hash> {
+	/// The indices of the leaves the proof is for.
+	pub leaf_indices: Vec<LeafIndex>,
+	/// Number of leaves in MMR, when the proof was generated.
+	pub leaf_count: NodeIndex,
+	/// Proof elements (hashes of siblings of inner nodes on the path to the leaf).
+	pub items: Vec<Hash>,
+}
+
+/// A type to describe leaf position in the MMR.
+///
+/// Note this is different from [`NodeIndex`], which can be applied to
+/// both leafs and inner nodes. Leafs will always have consecutive `LeafIndex`,
+/// but might be actually at different positions in the MMR `NodeIndex`.
+// https://github.com/paritytech/polkadot-sdk/blob/a190e0e9253562fdca9c1b6e9541a7ea0a50c018/substrate/primitives/merkle-mountain-range/src/lib.rs#L45
+pub type LeafIndex = u64;
+/// A type to describe node position in the MMR (node index).
+// https://github.com/paritytech/polkadot-sdk/blob/a190e0e9253562fdca9c1b6e9541a7ea0a50c018/substrate/primitives/merkle-mountain-range/src/lib.rs#L138-L146
+pub type NodeIndex = u64;
+
+/// Merkle Mountain Range operation error.
+// https://github.com/paritytech/polkadot-sdk/blob/a190e0e9253562fdca9c1b6e9541a7ea0a50c018/substrate/primitives/merkle-mountain-range/src/lib.rs#L362-L396
+#[derive(codec::Encode, codec::Decode, PartialEq, Eq, TypeInfo, RuntimeDebug)]
+pub enum MmrError {
+	/// Error during translation of a block number into a leaf index.
+	InvalidNumericOp,
+	/// Error while pushing new node.
+	Push,
+	/// Error getting the new root.
+	GetRoot,
+	/// Error committing changes.
+	Commit,
+	/// Error during proof generation.
+	GenerateProof,
+	/// Proof verification error.
+	Verify,
+	/// Leaf not found in the storage.
+	LeafNotFound,
+	/// Mmr Pallet not included in runtime
+	PalletNotIncluded,
+	/// Cannot find the requested leaf index
+	InvalidLeafIndex,
+	/// The provided best know block number is invalid.
+	InvalidBestKnownBlock,
+}
diff --git a/src/api/api_client.rs b/src/api/api_client.rs
index 84606ab23..90e33ef30 100644
--- a/src/api/api_client.rs
+++ b/src/api/api_client.rs
@@ -14,6 +14,7 @@
 use crate::{
 	api::error::{Error, Result},
 	rpc::Request,
+	runtime_api::RuntimeApiClient,
 	GetAccountInformation,
 };
 use ac_compose_macros::rpc_params;
@@ -21,6 +22,7 @@ use ac_node_api::metadata::Metadata;
 use ac_primitives::{Config, ExtrinsicParams, SignExtrinsic};
 #[cfg(not(feature = "sync-api"))]
 use alloc::boxed::Box;
+use alloc::sync::Arc;
 use codec::Decode;
 use core::convert::TryFrom;
 use frame_metadata::RuntimeMetadataPrefixed;
@@ -36,7 +38,8 @@ pub struct Api<T: Config, Client> {
 	genesis_hash: T::Hash,
 	metadata: Metadata,
 	runtime_version: RuntimeVersion,
-	client: Client,
+	client: Arc<Client>,
+	runtime_api: RuntimeApiClient<T, Client>,
 	additional_extrinsic_params:
 		Option<<T::ExtrinsicParams as ExtrinsicParams<T::Index, T::Hash>>::AdditionalParams>,
 }
@@ -49,12 +52,15 @@ impl<T: Config, Client> Api<T, Client> {
 		runtime_version: RuntimeVersion,
 		client: Client,
 	) -> Self {
+		let client = Arc::new(client);
+		let runtime_api = RuntimeApiClient::new(client.clone());
 		Self {
 			signer: None,
 			genesis_hash,
 			metadata,
 			runtime_version,
 			client,
+			runtime_api,
 			additional_extrinsic_params: None,
 		}
 	}
@@ -102,6 +108,11 @@ impl<T: Config, Client> Api<T, Client> {
 		self.additional_extrinsic_params = Some(add_params);
 	}
 
+	/// Access the RuntimeApi.
+	pub fn runtime_api(&self) -> &RuntimeApiClient<T, Client> {
+		&self.runtime_api
+	}
+
 	/// Get the extrinsic params with the set additional params. If no additional params are set,
 	/// the default is taken.
 	pub fn extrinsic_params(&self, nonce: T::Index) -> T::ExtrinsicParams {
diff --git a/src/api/mod.rs b/src/api/mod.rs
index 0ce95708b..1be571acd 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -32,6 +32,7 @@ pub use rpc_api::{
 pub mod api_client;
 pub mod error;
 pub mod rpc_api;
+pub mod runtime_api;
 
 /// Extrinsic report returned upon a submit_and_watch request.
 /// Holds as much information as available.
diff --git a/src/api/runtime_api/account_nonce.rs b/src/api/runtime_api/account_nonce.rs
new file mode 100644
index 000000000..80468aa07
--- /dev/null
+++ b/src/api/runtime_api/account_nonce.rs
@@ -0,0 +1,52 @@
+/*
+   Copyright 2024 Supercomputing Systems AG
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+	   http://www.apache.org/licenses/LICENSE-2.0
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+use super::{RuntimeApi, RuntimeApiClient};
+use crate::{api::Result, rpc::Request};
+use ac_primitives::config::Config;
+#[cfg(not(feature = "sync-api"))]
+use alloc::boxed::Box;
+use alloc::vec;
+use sp_core::Encode;
+
+#[maybe_async::maybe_async(?Send)]
+pub trait AccountNonceApi: RuntimeApi {
+	type Index;
+	type AccountId;
+
+	/// The API to query account nonce (aka transaction index).
+	async fn account_nonce(
+		&self,
+		account_id: Self::AccountId,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::Index>;
+}
+
+#[maybe_async::maybe_async(?Send)]
+impl<T, Client> AccountNonceApi for RuntimeApiClient<T, Client>
+where
+	T: Config,
+	Client: Request,
+{
+	type Index = T::Index;
+	type AccountId = T::AccountId;
+
+	async fn account_nonce(
+		&self,
+		account_id: Self::AccountId,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::Index> {
+		self.runtime_call("AccountNonceApi_account_nonce", vec![account_id.encode()], at_block)
+			.await
+	}
+}
diff --git a/src/api/runtime_api/api_core.rs b/src/api/runtime_api/api_core.rs
new file mode 100644
index 000000000..06c014db2
--- /dev/null
+++ b/src/api/runtime_api/api_core.rs
@@ -0,0 +1,75 @@
+/*
+   Copyright 2024 Supercomputing Systems AG
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+	   http://www.apache.org/licenses/LICENSE-2.0
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+use super::{RuntimeApi, RuntimeApiClient};
+use crate::{api::Result, rpc::Request};
+use ac_primitives::{config::Config, RuntimeVersion};
+#[cfg(not(feature = "sync-api"))]
+use alloc::boxed::Box;
+use alloc::vec;
+use sp_core::{Bytes, Encode};
+
+#[maybe_async::maybe_async(?Send)]
+pub trait CoreApi: RuntimeApi {
+	type Block;
+	type Header;
+	type RuntimeVersion;
+
+	/// Execute the given block.
+	async fn execute_block(&self, block: Self::Block, at_block: Option<Self::Hash>) -> Result<()>;
+
+	/// Execute the given opaque block.
+	async fn execute_opaque_block(&self, block: Bytes, at_block: Option<Self::Hash>) -> Result<()>;
+
+	/// Initialize a block with the given header.
+	async fn initialize_block(
+		&self,
+		header: Self::Header,
+		at_block: Option<Self::Hash>,
+	) -> Result<()>;
+
+	/// Returns the version of the runtime.
+	async fn version(&self, at_block: Option<Self::Hash>) -> Result<Self::RuntimeVersion>;
+}
+
+#[maybe_async::maybe_async(?Send)]
+impl<T, Client> CoreApi for RuntimeApiClient<T, Client>
+where
+	T: Config,
+	Client: Request,
+{
+	type Block = T::Block;
+	type Header = T::Header;
+	type RuntimeVersion = RuntimeVersion;
+
+	async fn execute_block(&self, block: Self::Block, at_block: Option<Self::Hash>) -> Result<()> {
+		self.execute_opaque_block(block.encode().into(), at_block).await
+	}
+
+	async fn execute_opaque_block(&self, block: Bytes, at_block: Option<Self::Hash>) -> Result<()> {
+		self.runtime_call("Core_execute_block", vec![block.0], at_block).await
+	}
+
+	async fn initialize_block(
+		&self,
+		header: Self::Header,
+		at_block: Option<Self::Hash>,
+	) -> Result<()> {
+		self.runtime_call("Core_initialize_block", vec![header.encode()], at_block)
+			.await
+	}
+
+	async fn version(&self, at_block: Option<Self::Hash>) -> Result<Self::RuntimeVersion> {
+		self.runtime_call("Core_version", vec![], at_block).await
+	}
+}
diff --git a/src/api/runtime_api/authority_discovery.rs b/src/api/runtime_api/authority_discovery.rs
new file mode 100644
index 000000000..f41026246
--- /dev/null
+++ b/src/api/runtime_api/authority_discovery.rs
@@ -0,0 +1,43 @@
+/*
+   Copyright 2024 Supercomputing Systems AG
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+	   http://www.apache.org/licenses/LICENSE-2.0
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+use super::{RuntimeApi, RuntimeApiClient};
+use crate::{api::Result, rpc::Request};
+use ac_primitives::config::Config;
+#[cfg(not(feature = "sync-api"))]
+use alloc::boxed::Box;
+use alloc::{vec, vec::Vec};
+use codec::Decode;
+
+#[maybe_async::maybe_async(?Send)]
+pub trait AuthorityDiscoveryApi: RuntimeApi {
+	/// Retrieve authority identifiers of the current and next authority set.
+	async fn authorities<AuthorityId: Decode>(
+		&self,
+		at_block: Option<Self::Hash>,
+	) -> Result<Vec<AuthorityId>>;
+}
+
+#[maybe_async::maybe_async(?Send)]
+impl<T, Client> AuthorityDiscoveryApi for RuntimeApiClient<T, Client>
+where
+	T: Config,
+	Client: Request,
+{
+	async fn authorities<AuthorityId: Decode>(
+		&self,
+		at_block: Option<Self::Hash>,
+	) -> Result<Vec<AuthorityId>> {
+		self.runtime_call("AuthorityDiscoveryApi_authorities", vec![], at_block).await
+	}
+}
diff --git a/src/api/runtime_api/block_builder.rs b/src/api/runtime_api/block_builder.rs
new file mode 100644
index 000000000..427de462f
--- /dev/null
+++ b/src/api/runtime_api/block_builder.rs
@@ -0,0 +1,131 @@
+/*
+   Copyright 2024 Supercomputing Systems AG
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+	   http://www.apache.org/licenses/LICENSE-2.0
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+use super::{RuntimeApi, RuntimeApiClient};
+use crate::{api::Result, rpc::Request};
+use ac_primitives::{config::Config, UncheckedExtrinsicV4};
+#[cfg(not(feature = "sync-api"))]
+use alloc::boxed::Box;
+use alloc::{vec, vec::Vec};
+use sp_core::{Bytes, Encode};
+use sp_inherents::{CheckInherentsResult, InherentData};
+use sp_runtime::ApplyExtrinsicResult;
+
+#[maybe_async::maybe_async(?Send)]
+pub trait BlockBuilderApi: RuntimeApi {
+	type ApplyExtrinsicResult;
+	type Block;
+	type InherentData;
+	type CheckInherentsResult;
+	type Header;
+
+	/// Apply the given extrinsic.
+	async fn apply_extrinsic<Address, Call, Signature, SignedExtra>(
+		&self,
+		extrinsic: UncheckedExtrinsicV4<Address, Call, Signature, SignedExtra>,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::ApplyExtrinsicResult>
+	where
+		Address: Encode,
+		Call: Encode,
+		Signature: Encode,
+		SignedExtra: Encode;
+
+	/// Apply the given opaque extrinsic.
+	async fn apply_opaque_extrinsic(
+		&self,
+		extrinsic: Vec<u8>,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::ApplyExtrinsicResult>;
+
+	/// Check that the inherents are valid.
+	async fn check_inherents(
+		&self,
+		block: Self::Block,
+		data: Self::InherentData,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::CheckInherentsResult>;
+
+	/// Finish the current block.
+	async fn finalize_block(&self, at_block: Option<Self::Hash>) -> Result<Self::Header>;
+
+	/// Generate inherent extrinsics and return them as encoded Bytes.
+	async fn inherent_extrinsics(
+		&self,
+		inherent: Self::InherentData,
+		at_block: Option<Self::Hash>,
+	) -> Result<Vec<Bytes>>;
+}
+
+#[maybe_async::maybe_async(?Send)]
+impl<T, Client> BlockBuilderApi for RuntimeApiClient<T, Client>
+where
+	T: Config,
+	Client: Request,
+{
+	type ApplyExtrinsicResult = ApplyExtrinsicResult;
+	type Block = T::Block;
+	type InherentData = InherentData;
+	type CheckInherentsResult = CheckInherentsResult;
+	type Header = T::Header;
+
+	async fn apply_extrinsic<Address, Call, Signature, SignedExtra>(
+		&self,
+		extrinsic: UncheckedExtrinsicV4<Address, Call, Signature, SignedExtra>,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::ApplyExtrinsicResult>
+	where
+		Address: Encode,
+		Call: Encode,
+		Signature: Encode,
+		SignedExtra: Encode,
+	{
+		self.apply_opaque_extrinsic(extrinsic.encode(), at_block).await
+	}
+
+	async fn apply_opaque_extrinsic(
+		&self,
+		extrinsic: Vec<u8>,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::ApplyExtrinsicResult> {
+		self.runtime_call("BlockBuilder_apply_extrinsic", vec![extrinsic], at_block)
+			.await
+	}
+
+	async fn check_inherents(
+		&self,
+		block: Self::Block,
+		data: Self::InherentData,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::CheckInherentsResult> {
+		self.runtime_call(
+			"BlockBuilder_check_inherents",
+			vec![block.encode(), data.encode()],
+			at_block,
+		)
+		.await
+	}
+
+	async fn finalize_block(&self, at_block: Option<Self::Hash>) -> Result<Self::Header> {
+		self.runtime_call("BlockBuilder_finalize_block", vec![], at_block).await
+	}
+
+	async fn inherent_extrinsics(
+		&self,
+		inherent: Self::InherentData,
+		at_block: Option<Self::Hash>,
+	) -> Result<Vec<Bytes>> {
+		self.runtime_call("BlockBuilder_inherent_extrinsics", vec![inherent.encode()], at_block)
+			.await
+	}
+}
diff --git a/src/api/runtime_api/metadata.rs b/src/api/runtime_api/metadata.rs
new file mode 100644
index 000000000..bb08ceeed
--- /dev/null
+++ b/src/api/runtime_api/metadata.rs
@@ -0,0 +1,150 @@
+/*
+   Copyright 2024 Supercomputing Systems AG
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+	   http://www.apache.org/licenses/LICENSE-2.0
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+use super::{RuntimeApi, RuntimeApiClient};
+use crate::{api::Result, rpc::Request};
+use ac_node_api::{error::MetadataError, Metadata};
+use ac_primitives::config::Config;
+#[cfg(not(feature = "sync-api"))]
+use alloc::boxed::Box;
+use alloc::{
+	string::{String, ToString},
+	vec,
+	vec::Vec,
+};
+use codec::Decode;
+use sp_core::{Encode, OpaqueMetadata};
+
+#[maybe_async::maybe_async(?Send)]
+pub trait MetadataApi: RuntimeApi {
+	type OpaqueMetadata;
+
+	/// Returns the metadata of a runtime.
+	async fn metadata(&self, at_block: Option<Self::Hash>) -> Result<Metadata>;
+
+	/// Returns the opaque metadata of a runtime.
+	async fn opaque_metadata(&self, at_block: Option<Self::Hash>) -> Result<Self::OpaqueMetadata>;
+
+	/// Returns the metadata at a given version.
+	async fn metadata_at_version(
+		&self,
+		version: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Option<Metadata>>;
+
+	/// Returns the opaque metadata at a given version.
+	async fn opaque_metadata_at_version(
+		&self,
+		version: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Option<Self::OpaqueMetadata>>;
+
+	/// Returns the supported metadata versions.
+	async fn metadata_versions(&self, at_block: Option<Self::Hash>) -> Result<Vec<u32>>;
+
+	// Returns a list of the all available api traits.
+	async fn list_traits(&self, at_block: Option<Self::Hash>) -> Result<Vec<String>>;
+
+	// Returns a list of the method names of a specific trait.
+	async fn list_methods_of_trait(
+		&self,
+		trait_name: &str,
+		at_block: Option<Self::Hash>,
+	) -> Result<Vec<String>>;
+}
+
+#[maybe_async::maybe_async(?Send)]
+impl<T, Client> MetadataApi for RuntimeApiClient<T, Client>
+where
+	T: Config,
+	Client: Request,
+{
+	type OpaqueMetadata = OpaqueMetadata;
+
+	async fn metadata(&self, at_block: Option<Self::Hash>) -> Result<Metadata> {
+		let metadata_bytes = self.opaque_metadata(at_block).await?;
+		let metadata = Metadata::decode(&mut metadata_bytes.as_slice())?;
+		Ok(metadata)
+	}
+
+	async fn opaque_metadata(&self, at_block: Option<Self::Hash>) -> Result<Self::OpaqueMetadata> {
+		self.runtime_call("Metadata_metadata", vec![], at_block).await
+	}
+
+	async fn metadata_at_version(
+		&self,
+		version: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Option<Metadata>> {
+		let metadata_bytes = self.opaque_metadata_at_version(version, at_block).await?;
+		let metadata = match metadata_bytes {
+			Some(bytes) => Some(Metadata::decode(&mut bytes.as_slice())?),
+			None => None,
+		};
+		Ok(metadata)
+	}
+
+	async fn opaque_metadata_at_version(
+		&self,
+		version: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Option<Self::OpaqueMetadata>> {
+		self.runtime_call("Metadata_metadata_at_version", vec![version.encode()], at_block)
+			.await
+	}
+
+	async fn metadata_versions(&self, at_block: Option<Self::Hash>) -> Result<Vec<u32>> {
+		self.runtime_call("Metadata_metadata_versions", vec![], at_block).await
+	}
+
+	async fn list_traits(&self, at_block: Option<Self::Hash>) -> Result<Vec<String>> {
+		let metadata = self.get_metadata_v15(at_block).await?;
+		let trait_names = metadata
+			.runtime_api_traits()
+			.map(|substrate_trait| substrate_trait.name().to_string())
+			.collect();
+
+		Ok(trait_names)
+	}
+
+	async fn list_methods_of_trait(
+		&self,
+		trait_name: &str,
+		at_block: Option<Self::Hash>,
+	) -> Result<Vec<String>> {
+		let metadata = self.get_metadata_v15(at_block).await?;
+		let maybe_runtime_api_metadata = metadata
+			.runtime_api_traits()
+			.find(|substrate_trait| substrate_trait.name() == trait_name);
+
+		let methods = match maybe_runtime_api_metadata {
+			Some(trait_metadata) =>
+				trait_metadata.methods().map(|method| method.name.clone()).collect(),
+			None => return Err(MetadataError::RuntimeApiNotFound(trait_name.to_string()).into()),
+		};
+		Ok(methods)
+	}
+}
+
+impl<T, Client> RuntimeApiClient<T, Client>
+where
+	T: Config,
+	Client: Request,
+{
+	#[maybe_async::maybe_async(?Send)]
+	async fn get_metadata_v15(&self, at_block: Option<T::Hash>) -> Result<Metadata> {
+		self.metadata_at_version(15, at_block)
+			.await?
+			.ok_or(MetadataError::RuntimeApiNotFound("No metadata v15 found".to_string()).into())
+	}
+}
diff --git a/src/api/runtime_api/mmr.rs b/src/api/runtime_api/mmr.rs
new file mode 100644
index 000000000..70e998627
--- /dev/null
+++ b/src/api/runtime_api/mmr.rs
@@ -0,0 +1,119 @@
+/*
+   Copyright 2024 Supercomputing Systems AG
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+	   http://www.apache.org/licenses/LICENSE-2.0
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+use super::{RuntimeApi, RuntimeApiClient};
+use crate::{api::Result, rpc::Request};
+use ac_primitives::{config::Config, EncodableOpaqueLeaf, MmrError, Proof};
+#[cfg(not(feature = "sync-api"))]
+use alloc::boxed::Box;
+use alloc::{vec, vec::Vec};
+use core::result::Result as StdResult;
+use sp_core::Encode;
+
+#[maybe_async::maybe_async(?Send)]
+pub trait MmrApi: RuntimeApi {
+	type Error;
+	type BlockNumber;
+	type EncodableOpaqueLeaf;
+	type Proof;
+
+	/// Generate MMR proof for the given block numbers.
+	#[allow(clippy::type_complexity)]
+	async fn generate_proof(
+		&self,
+		block_numbers: Vec<Self::BlockNumber>,
+		best_known_block_number: Option<Self::BlockNumber>,
+		at_block: Option<Self::Hash>,
+	) -> Result<StdResult<(Vec<Self::EncodableOpaqueLeaf>, Self::Proof), Self::Error>>;
+
+	/// Return the on-chain MMR root hash.
+	async fn root(
+		&self,
+		at_block: Option<Self::Hash>,
+	) -> Result<StdResult<Vec<Self::Hash>, Self::Error>>;
+
+	/// Verify MMR proof against on-chain MMR.
+	async fn verify_proof(
+		&self,
+		leaves: Vec<Self::EncodableOpaqueLeaf>,
+		proof: Self::Proof,
+		at_block: Option<Self::Hash>,
+	) -> Result<StdResult<(), Self::Error>>;
+
+	/// Verify MMR proof against given root hash.
+	async fn verify_proof_stateless(
+		&self,
+		root: Self::Hash,
+		leaves: Vec<Self::EncodableOpaqueLeaf>,
+		proof: Self::Proof,
+		at_block: Option<Self::Hash>,
+	) -> Result<StdResult<(), Self::Error>>;
+}
+
+#[maybe_async::maybe_async(?Send)]
+impl<T, Client> MmrApi for RuntimeApiClient<T, Client>
+where
+	T: Config,
+	Client: Request,
+{
+	type Error = MmrError;
+	type BlockNumber = T::BlockNumber;
+	type EncodableOpaqueLeaf = EncodableOpaqueLeaf;
+	type Proof = Proof<T::Hash>;
+
+	async fn generate_proof(
+		&self,
+		block_numbers: Vec<Self::BlockNumber>,
+		best_known_block_number: Option<Self::BlockNumber>,
+		at_block: Option<Self::Hash>,
+	) -> Result<StdResult<(Vec<Self::EncodableOpaqueLeaf>, Self::Proof), Self::Error>> {
+		self.runtime_call(
+			"MmrApi_generate_proof",
+			vec![block_numbers.encode(), best_known_block_number.encode()],
+			at_block,
+		)
+		.await
+	}
+
+	async fn root(
+		&self,
+		at_block: Option<Self::Hash>,
+	) -> Result<StdResult<Vec<Self::Hash>, Self::Error>> {
+		self.runtime_call("MmrApi_root", vec![], at_block).await
+	}
+
+	async fn verify_proof(
+		&self,
+		leaves: Vec<Self::EncodableOpaqueLeaf>,
+		proof: Self::Proof,
+		at_block: Option<Self::Hash>,
+	) -> Result<StdResult<(), Self::Error>> {
+		self.runtime_call("MmrApi_verify_proof", vec![leaves.encode(), proof.encode()], at_block)
+			.await
+	}
+
+	async fn verify_proof_stateless(
+		&self,
+		root: Self::Hash,
+		leaves: Vec<Self::EncodableOpaqueLeaf>,
+		proof: Self::Proof,
+		at_block: Option<Self::Hash>,
+	) -> Result<StdResult<(), Self::Error>> {
+		self.runtime_call(
+			"MmrApi_verify_proof_stateless",
+			vec![root.encode(), leaves.encode(), proof.encode()],
+			at_block,
+		)
+		.await
+	}
+}
diff --git a/src/api/runtime_api/mod.rs b/src/api/runtime_api/mod.rs
new file mode 100644
index 000000000..66b2da9d8
--- /dev/null
+++ b/src/api/runtime_api/mod.rs
@@ -0,0 +1,134 @@
+/*
+   Copyright 2019 Supercomputing Systems AG
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+	   http://www.apache.org/licenses/LICENSE-2.0
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+pub use self::{
+	account_nonce::*, api_core::*, authority_discovery::*, block_builder::*, metadata::*, mmr::*,
+	session_keys::*, staking::*, transaction_payment::*, transaction_payment_call::*,
+};
+
+pub mod account_nonce;
+pub mod api_core;
+pub mod authority_discovery;
+pub mod block_builder;
+pub mod metadata;
+pub mod mmr;
+pub mod session_keys;
+pub mod staking;
+pub mod transaction_payment;
+pub mod transaction_payment_call;
+
+use crate::{api::Result, rpc::Request};
+use ac_compose_macros::rpc_params;
+use ac_primitives::config::Config;
+#[cfg(not(feature = "sync-api"))]
+use alloc::boxed::Box;
+use alloc::{sync::Arc, vec::Vec};
+use codec::Decode;
+use core::marker::PhantomData;
+use sp_core::Bytes;
+
+#[derive(Clone)]
+pub struct RuntimeApiClient<T, Client> {
+	client: Arc<Client>,
+	_phantom: PhantomData<T>,
+}
+
+impl<T, Client> RuntimeApiClient<T, Client> {
+	pub fn new(client: Arc<Client>) -> Self {
+		Self { client, _phantom: PhantomData }
+	}
+}
+
+#[maybe_async::maybe_async(?Send)]
+pub trait RuntimeApi {
+	type Hash;
+
+	/// Query a runtime api call with automatic decoding to the expected return type.
+	async fn runtime_call<V: Decode>(
+		&self,
+		method: &str,
+		data: Vec<Vec<u8>>,
+		at_block: Option<Self::Hash>,
+	) -> Result<V>;
+
+	/// Query a raw runtime api call without decoding.
+	async fn opaque_runtime_call(
+		&self,
+		method: &str,
+		data: Vec<Vec<u8>>,
+		at_block: Option<Self::Hash>,
+	) -> Result<Bytes>;
+
+	// Perform a rpc call to a builtin on the chain.
+	async fn rpc_call(
+		&self,
+		method: &str,
+		data: Option<Bytes>,
+		at_block: Option<Self::Hash>,
+	) -> Result<Bytes>;
+}
+
+#[maybe_async::maybe_async(?Send)]
+impl<T, Client> RuntimeApi for RuntimeApiClient<T, Client>
+where
+	T: Config,
+	Client: Request,
+{
+	type Hash = T::Hash;
+
+	async fn runtime_call<V: Decode>(
+		&self,
+		method: &str,
+		data: Vec<Vec<u8>>,
+		at_block: Option<Self::Hash>,
+	) -> Result<V> {
+		let bytes = self.opaque_runtime_call(method, data, at_block).await?;
+		Ok(Decode::decode(&mut bytes.0.as_slice())?)
+	}
+
+	async fn opaque_runtime_call(
+		&self,
+		method: &str,
+		data: Vec<Vec<u8>>,
+		at_block: Option<Self::Hash>,
+	) -> Result<Bytes> {
+		let data = match data.is_empty() {
+			true => None,
+			false => {
+				let mut appended_data = Vec::new();
+				for mut item in data {
+					appended_data.append(&mut item);
+				}
+				Some(appended_data.into())
+			},
+		};
+		self.rpc_call(method, data, at_block).await
+	}
+
+	async fn rpc_call(
+		&self,
+		method: &str,
+		data: Option<Bytes>,
+		at_block: Option<Self::Hash>,
+	) -> Result<Bytes> {
+		let extracted_data: Bytes = match data {
+			Some(data) => data,
+			None => Vec::new().into(),
+		};
+		let return_bytes = self
+			.client
+			.request("state_call", rpc_params![method, extracted_data, at_block])
+			.await?;
+		Ok(return_bytes)
+	}
+}
diff --git a/src/api/runtime_api/session_keys.rs b/src/api/runtime_api/session_keys.rs
new file mode 100644
index 000000000..04b63ce55
--- /dev/null
+++ b/src/api/runtime_api/session_keys.rs
@@ -0,0 +1,68 @@
+/*
+   Copyright 2024 Supercomputing Systems AG
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+	   http://www.apache.org/licenses/LICENSE-2.0
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+use super::{RuntimeApi, RuntimeApiClient};
+use crate::{api::Result, rpc::Request};
+use ac_primitives::config::Config;
+#[cfg(not(feature = "sync-api"))]
+use alloc::boxed::Box;
+use alloc::{vec, vec::Vec};
+use codec::Encode;
+use sp_core::{crypto::KeyTypeId, Bytes};
+
+#[maybe_async::maybe_async(?Send)]
+pub trait SessionKeysApi: RuntimeApi {
+	type KeyTypeId;
+
+	/// Decode the given public session keys.
+	#[allow(clippy::type_complexity)]
+	async fn decode_session_keys(
+		&self,
+		encoded: Bytes,
+		at_block: Option<Self::Hash>,
+	) -> Result<Option<Vec<(Bytes, Self::KeyTypeId)>>>;
+
+	/// Generate a set of session keys with optionally using the given seed.
+	async fn generate_session_keys(
+		&self,
+		seed: Option<Bytes>,
+		at_block: Option<Self::Hash>,
+	) -> Result<Bytes>;
+}
+
+#[maybe_async::maybe_async(?Send)]
+impl<T, Client> SessionKeysApi for RuntimeApiClient<T, Client>
+where
+	T: Config,
+	Client: Request,
+{
+	type KeyTypeId = KeyTypeId;
+
+	async fn decode_session_keys(
+		&self,
+		encoded: Bytes,
+		at_block: Option<Self::Hash>,
+	) -> Result<Option<Vec<(Bytes, Self::KeyTypeId)>>> {
+		self.runtime_call("SessionKeys_decode_session_keys", vec![encoded.0], at_block)
+			.await
+	}
+
+	async fn generate_session_keys(
+		&self,
+		seed: Option<Bytes>,
+		at_block: Option<Self::Hash>,
+	) -> Result<Bytes> {
+		self.runtime_call("SessionKeys_generate_session_keys", vec![seed.encode()], at_block)
+			.await
+	}
+}
diff --git a/src/api/runtime_api/staking.rs b/src/api/runtime_api/staking.rs
new file mode 100644
index 000000000..1e94b75ab
--- /dev/null
+++ b/src/api/runtime_api/staking.rs
@@ -0,0 +1,50 @@
+/*
+   Copyright 2024 Supercomputing Systems AG
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+	   http://www.apache.org/licenses/LICENSE-2.0
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+use super::{RuntimeApi, RuntimeApiClient};
+use crate::{api::Result, rpc::Request};
+use ac_primitives::config::Config;
+#[cfg(not(feature = "sync-api"))]
+use alloc::boxed::Box;
+use alloc::vec;
+use sp_core::Encode;
+
+#[maybe_async::maybe_async(?Send)]
+pub trait StakingApi: RuntimeApi {
+	type Balance;
+
+	/// Returns the nominations quota for a nominator with a given balance.
+	async fn nominations_quota(
+		&self,
+		balance: Self::Balance,
+		at_block: Option<Self::Hash>,
+	) -> Result<u32>;
+}
+
+#[maybe_async::maybe_async(?Send)]
+impl<T, Client> StakingApi for RuntimeApiClient<T, Client>
+where
+	T: Config,
+	Client: Request,
+{
+	type Balance = T::Balance;
+
+	async fn nominations_quota(
+		&self,
+		balance: Self::Balance,
+		at_block: Option<Self::Hash>,
+	) -> Result<u32> {
+		self.runtime_call("StakingApi_nominations_quota", vec![balance.encode()], at_block)
+			.await
+	}
+}
diff --git a/src/api/runtime_api/transaction_payment.rs b/src/api/runtime_api/transaction_payment.rs
new file mode 100644
index 000000000..54b469909
--- /dev/null
+++ b/src/api/runtime_api/transaction_payment.rs
@@ -0,0 +1,182 @@
+/*
+   Copyright 2024 Supercomputing Systems AG
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+	   http://www.apache.org/licenses/LICENSE-2.0
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+use super::{RuntimeApi, RuntimeApiClient};
+use crate::{api::Result, rpc::Request};
+use ac_primitives::{
+	config::Config, FeeDetails, RuntimeDispatchInfo, UncheckedExtrinsicV4, Weight,
+};
+#[cfg(not(feature = "sync-api"))]
+use alloc::boxed::Box;
+use alloc::{vec, vec::Vec};
+use sp_core::Encode;
+
+#[maybe_async::maybe_async(?Send)]
+pub trait TransactionPaymentApi: RuntimeApi {
+	type FeeDetails;
+	type RuntimeDispatchInfo;
+	type Balance;
+	type Weight;
+
+	/// Query the transaction fee details.
+	async fn query_fee_details<Address, Call, Signature, SignedExtra>(
+		&self,
+		extrinsic: UncheckedExtrinsicV4<Address, Call, Signature, SignedExtra>,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::FeeDetails>
+	where
+		Address: Encode,
+		Call: Encode,
+		Signature: Encode,
+		SignedExtra: Encode;
+
+	/// Query the transaction fee details of opaque extrinsic.
+	async fn query_fee_details_opaque(
+		&self,
+		extrinsic: Vec<u8>,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::FeeDetails>;
+
+	/// Query the transaction fee info.
+	async fn query_info<Address, Call, Signature, SignedExtra>(
+		&self,
+		extrinsic: UncheckedExtrinsicV4<Address, Call, Signature, SignedExtra>,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::RuntimeDispatchInfo>
+	where
+		Address: Encode,
+		Call: Encode,
+		Signature: Encode,
+		SignedExtra: Encode;
+
+	/// Query the transaction info of opaque extrinsic.
+	async fn query_info_opaque(
+		&self,
+		extrinsic: Vec<u8>,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::RuntimeDispatchInfo>;
+
+	/// Query the output of the current LengthToFee given some input.
+	async fn query_length_to_fee(
+		&self,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::Balance>;
+
+	/// Query the output of the current WeightToFee given some input.
+	async fn query_weight_to_fee(
+		&self,
+		weight: Self::Weight,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::Balance>;
+}
+
+#[maybe_async::maybe_async(?Send)]
+impl<T, Client> TransactionPaymentApi for RuntimeApiClient<T, Client>
+where
+	T: Config,
+	Client: Request,
+{
+	type FeeDetails = FeeDetails<T::Balance>;
+	type RuntimeDispatchInfo = RuntimeDispatchInfo<T::Balance>;
+	type Balance = T::Balance;
+	type Weight = Weight;
+
+	async fn query_fee_details<Address, Call, Signature, SignedExtra>(
+		&self,
+		extrinsic: UncheckedExtrinsicV4<Address, Call, Signature, SignedExtra>,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::FeeDetails>
+	where
+		Address: Encode,
+		Call: Encode,
+		Signature: Encode,
+		SignedExtra: Encode,
+	{
+		self.query_fee_details_opaque(extrinsic.encode(), length, at_block).await
+	}
+
+	async fn query_fee_details_opaque(
+		&self,
+		extrinsic: Vec<u8>,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::FeeDetails> {
+		self.runtime_call(
+			"TransactionPaymentApi_query_fee_details",
+			vec![extrinsic, length.encode()],
+			at_block,
+		)
+		.await
+	}
+
+	async fn query_info<Address, Call, Signature, SignedExtra>(
+		&self,
+		extrinsic: UncheckedExtrinsicV4<Address, Call, Signature, SignedExtra>,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::RuntimeDispatchInfo>
+	where
+		Address: Encode,
+		Call: Encode,
+		Signature: Encode,
+		SignedExtra: Encode,
+	{
+		self.query_info_opaque(extrinsic.encode(), length, at_block).await
+	}
+
+	async fn query_info_opaque(
+		&self,
+		extrinsic: Vec<u8>,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::RuntimeDispatchInfo> {
+		self.runtime_call(
+			"TransactionPaymentApi_query_info",
+			vec![extrinsic, length.encode()],
+			at_block,
+		)
+		.await
+	}
+
+	async fn query_length_to_fee(
+		&self,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::Balance> {
+		self.runtime_call(
+			"TransactionPaymentApi_query_length_to_fee",
+			vec![length.encode()],
+			at_block,
+		)
+		.await
+	}
+
+	async fn query_weight_to_fee(
+		&self,
+		weight: Self::Weight,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::Balance> {
+		self.runtime_call(
+			"TransactionPaymentApi_query_weight_to_fee",
+			vec![weight.encode()],
+			at_block,
+		)
+		.await
+	}
+}
diff --git a/src/api/runtime_api/transaction_payment_call.rs b/src/api/runtime_api/transaction_payment_call.rs
new file mode 100644
index 000000000..133e596d3
--- /dev/null
+++ b/src/api/runtime_api/transaction_payment_call.rs
@@ -0,0 +1,124 @@
+/*
+   Copyright 2024 Supercomputing Systems AG
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+	   http://www.apache.org/licenses/LICENSE-2.0
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+use super::{RuntimeApi, RuntimeApiClient};
+use crate::{api::Result, rpc::Request};
+use ac_primitives::{config::Config, FeeDetails, RuntimeDispatchInfo, Weight};
+#[cfg(not(feature = "sync-api"))]
+use alloc::boxed::Box;
+use alloc::vec;
+use sp_core::Encode;
+
+#[maybe_async::maybe_async(?Send)]
+pub trait TransactionPaymentCallApi: RuntimeApi {
+	type FeeDetails;
+	type RuntimeDispatchInfo;
+	type Balance;
+	type Weight;
+
+	/// Query the call fee details.
+	async fn query_call_fee_details<Call: Encode>(
+		&self,
+		call: Call,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::FeeDetails>;
+
+	/// Query the call info
+	async fn query_call_info<Call: Encode>(
+		&self,
+		call: Call,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::RuntimeDispatchInfo>;
+
+	/// Query the output of the current LengthToFee given some input.
+	async fn query_length_to_fee_call(
+		&self,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::Balance>;
+
+	/// Query the output of the current WeightToFee given some input.
+	async fn query_weight_to_fee_call(
+		&self,
+		weight: Self::Weight,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::Balance>;
+}
+
+#[maybe_async::maybe_async(?Send)]
+impl<T, Client> TransactionPaymentCallApi for RuntimeApiClient<T, Client>
+where
+	T: Config,
+	Client: Request,
+{
+	type FeeDetails = FeeDetails<T::Balance>;
+	type RuntimeDispatchInfo = RuntimeDispatchInfo<T::Balance>;
+	type Balance = T::Balance;
+	type Weight = Weight;
+
+	async fn query_call_fee_details<Call: Encode>(
+		&self,
+		call: Call,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::FeeDetails> {
+		self.runtime_call(
+			"TransactionPaymentCallApi_query_call_fee_details",
+			vec![call.encode(), length.encode()],
+			at_block,
+		)
+		.await
+	}
+
+	async fn query_call_info<Call: Encode>(
+		&self,
+		call: Call,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::RuntimeDispatchInfo> {
+		self.runtime_call(
+			"TransactionPaymentCallApi_query_call_info",
+			vec![call.encode(), length.encode()],
+			at_block,
+		)
+		.await
+	}
+
+	async fn query_length_to_fee_call(
+		&self,
+		length: u32,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::Balance> {
+		self.runtime_call(
+			"TransactionPaymentCallApi_query_length_to_fee",
+			vec![length.encode()],
+			at_block,
+		)
+		.await
+	}
+
+	async fn query_weight_to_fee_call(
+		&self,
+		weight: Self::Weight,
+		at_block: Option<Self::Hash>,
+	) -> Result<Self::Balance> {
+		self.runtime_call(
+			"TransactionPaymentCallApi_query_weight_to_fee",
+			vec![weight.encode()],
+			at_block,
+		)
+		.await
+	}
+}


Test patch:
diff --git a/testing/async/examples/runtime_api_tests.rs b/testing/async/examples/runtime_api_tests.rs
new file mode 100644
index 000000000..4f5aec15c
--- /dev/null
+++ b/testing/async/examples/runtime_api_tests.rs
@@ -0,0 +1,116 @@
+/*
+	Copyright 2019 Supercomputing Systems AG
+	Licensed under the Apache License, Version 2.0 (the "License");
+	you may not use this file except in compliance with the License.
+	You may obtain a copy of the License at
+
+		http://www.apache.org/licenses/LICENSE-2.0
+
+	Unless required by applicable law or agreed to in writing, software
+	distributed under the License is distributed on an "AS IS" BASIS,
+	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+	See the License for the specific language governing permissions and
+	limitations under the License.
+*/
+
+//! Tests for the runtime api.
+
+use sp_core::{sr25519, Decode};
+use sp_keyring::AccountKeyring;
+use substrate_api_client::{
+	ac_primitives::AssetRuntimeConfig,
+	extrinsic::BalancesExtrinsics,
+	rpc::JsonrpseeClient,
+	runtime_api::{
+		AccountNonceApi, AuthorityDiscoveryApi, BlockBuilderApi, CoreApi, MetadataApi, RuntimeApi,
+		StakingApi, TransactionPaymentApi, TransactionPaymentCallApi,
+	},
+	Api, GetChainInfo,
+};
+
+#[tokio::main]
+async fn main() {
+	// Setup
+	let client = JsonrpseeClient::with_default_url().await.unwrap();
+	let alice_pair = AccountKeyring::Alice.pair();
+	let mut api = Api::<AssetRuntimeConfig, _>::new(client).await.unwrap();
+	api.set_signer(alice_pair.into());
+
+	let runtime_api = api.runtime_api();
+
+	let alice = AccountKeyring::Alice.to_account_id();
+	let bob = AccountKeyring::Bob.to_account_id();
+
+	// General Runtime Api
+	let bytes = runtime_api.rpc_call("Metadata_metadata_versions", None, None).await.unwrap();
+	let metadata_versions = Vec::<u32>::decode(&mut bytes.0.as_slice()).unwrap();
+	assert_eq!(metadata_versions, [14, 15]);
+
+	// AccountNonce
+	let alice_nonce = runtime_api.account_nonce(alice, None).await.unwrap();
+	assert_eq!(alice_nonce, api.get_nonce().await.unwrap());
+
+	// Authority Discovery
+	let authority_id: Vec<sr25519::Public> = runtime_api.authorities(None).await.unwrap();
+	assert!(authority_id.len() > 0);
+
+	// BlockBuilder
+	let extrinsic = api.balance_transfer_allow_death(bob.clone().into(), 1000).await;
+	runtime_api.apply_extrinsic(extrinsic, None).await.unwrap().unwrap().unwrap();
+	let block = api.get_block_by_num(Some(0)).await.unwrap().unwrap();
+	let check = runtime_api.check_inherents(block, Default::default(), None).await.unwrap();
+	assert!(check.ok());
+	// This doesn't seem to work with the current substrate node. Tried it on polkadot.js as well, but it keeps on runtime panicking.
+	//let _bytes = runtime_api.inherent_extrinsics(Default::default(), None).unwrap();
+	//let _header = runtime_api.finalize_block(None).unwrap();
+
+	// Core
+	let _version = runtime_api.version(None).await.unwrap();
+
+	// Metadata
+	let _metadata = runtime_api.metadata(None).await.unwrap();
+	let _metadata = runtime_api.metadata_at_version(15, None).await.unwrap().unwrap();
+	let _method_names = runtime_api.list_methods_of_trait("BabeApi", None).await.unwrap();
+	let _trait_names = runtime_api.list_traits(None).await.unwrap();
+	let metadata_versions = runtime_api.metadata_versions(None).await.unwrap();
+	assert_eq!(metadata_versions, [14, 15]);
+
+	// MMR
+	// This doesn't seem to work with the current substrate node. Tried it on polkadot.js aswell, but it keeps on runtime panicking.
+	// let generated_proof = runtime_api.generate_proof(vec![0, 1], None, None).unwrap().unwrap();
+	// let root = runtime_api.root(None).unwrap().unwrap();
+	// runtime_api
+	// 	.verify_proof(generated_proof.0, generated_proof.1, None)
+	// 	.unwrap()
+	// 	.unwrap();
+	// let generated_proof = runtime_api.generate_proof(vec![1], None, None).unwrap().unwrap();
+	// runtime_api
+	// 	.verify_proof_stateless(root[0], generated_proof.0, generated_proof.1, None)
+	// 	.unwrap()
+	// 	.unwrap();
+
+	// Sessions keys
+	// This doesn't seem to work with the current substrate node. Tried it on polkadot.js aswell, but it keeps on runtime panicking.
+	// let encoded_session_keys = runtime_api.generate_session_keys(None, None).unwrap();
+	// let _session_keys =
+	// 	runtime_api.decode_session_keys(encoded_session_keys, None).unwrap().unwrap();
+
+	// Staking
+	let _quota = runtime_api.nominations_quota(100000000, None).await.unwrap();
+
+	// Transaction Payment
+	let extrinsic = api.balance_transfer_allow_death(bob.clone().into(), 1000).await;
+	let _tx_fee_details =
+		runtime_api.query_fee_details(extrinsic.clone(), 1000, None).await.unwrap();
+	let _tx_info = runtime_api.query_info(extrinsic, 1000, None).await.unwrap();
+	let _fee = runtime_api.query_length_to_fee(1000, None).await.unwrap();
+	let _fee = runtime_api.query_weight_to_fee(1000.into(), None).await.unwrap();
+
+	// Transaction Payment Call
+	let call = api.balance_transfer_allow_death(bob.clone().into(), 1000).await.function;
+	let _tx_fee_details =
+		runtime_api.query_call_fee_details(call.clone(), 1000, None).await.unwrap();
+	let _tx_info = runtime_api.query_call_info(call, 1000, None).await.unwrap();
+	let _fee = runtime_api.query_length_to_fee_call(1000, None).await.unwrap();
+	let _fee = runtime_api.query_weight_to_fee_call(1000.into(), None).await.unwrap();
+}