Improve "Runtime API" / "View Function" APIs: allow trying to decode results into multiple target types, like Storage APIs do.
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 489
- Forks
- 293
- Avg merge
- 18h 35m
- Merged PRs (30d)
- 3
Description
The Subxt storage APIs allow trying to decode the values we get back into either the default target type given by the Payload, or some custom types.
Particularly when dealing with historic types (eg Rest API), one often needs to support multiple different target types for a call. A motivating example is in this PR.
We have a couple of ways to do this but none are ideal:
// Option 1: call the Runtime API multiple times, trying different decode targets (slow because networking!):
let payload = subxt::runtime_apis::dynamic<Args, ResponseTypeV1>("Some", "runtime_api_call", args);
let response: GenericResponseType = match client_at_block.runtime_apis().call(payload).await {
Ok(res) => response_v1_to_generic_response_type(res),
Err(_e) => {
let payload2 = subxt::runtime_apis::dynamic<Args, ResponseTypeV2>("Some", "runtime_api_call", args);
match client_at_block.runtime_apis().call(payload2).await {
Ok(res2) => response_v2_to_generic_response_type(res),
Err(_e) => {
let payload3 = subxt::runtime_apis::dynamic<Args, ResponseTypeV3>("Some", "runtime_api_call", args);
client_at_block.runtime_apis().call(payload2).await.map(response_v3_to_generic_response_type)?
}
}
}
}
// Option 2: Decode into inefficient scale_value::Value and manually inspect it (slow because Value bad for CPU/memory):
let payload = subxt::runtime_apis::dynamic<Args, scale_value::Value>("Some", "runtime_api_call", args);
let response_value = match client_at_block.runtime_apis().call(payload).await?;
let response = {
if let Some(response) = from_v1_value_to_generic(&response_value) {
response
} else if let Some(response) = from_v2_value_to_generic(&response_value) {
response
} else if let Some(response) = from_v3_value_to_generic(&response_value) {
response
} else {
return Err(SomeErrorThatResponseIsBad)
}
};
// Option 3: make a raw call and manually decode things (verbose and error prone):
let call_args = (arg1, arg2, arg3).encode();
let response_bytes = client_at_block.runtime_apis().call_raw("Some_runtime_api_call", Some(&call_args)).await?;
let return_type_id = client_at_block
.metadata()
.runtime_api_trait_by_name("Some")?
.method_by_name("runtime_api_call")?
.output_ty();
let response = {
if let Ok(res) = scale_decode::decode_as_type::<ResponseTypeV1>(&mut &*response_bytes), return_type_id, metadata.types()) {
response_v1_to_generic_response_type(res)
} else if let Ok(res) = scale_decode::decode_as_type::<ResponseTypeV2>(&mut &*response_bytes), return_type_id, metadata.types()) {
response_v2_to_generic_response_type(res)
} else if let Ok(res) = scale_decode::decode_as_type::<ResponseTypeV3>(&mut &*response_bytes), return_type_id, metadata.types()) {
response_v3_to_generic_response_type(res)
}
};
Let's instead make it possible to do something more like the storage APIs allow eg:
let payload = subxt::runtime_apis::dynamic<Args, ()>("Some", "runtime_api_call", args);
let response = client_at_block.runtime_apis().call(payload).await?;
let response = {
if let Ok(res) = response.decode_as::<ResponseTypeV1>() {
response_v1_to_generic_response_type(res)
} else if let Ok(res) = response.decode_as::<ResponseTypeV2>() {
response_v2_to_generic_response_type(res)
} else if let Ok(res) = response.decode_as::<ResponseTypeV3>() {
response_v3_to_generic_response_type(res)
}
};
This avoids the unnecessary network calls, and makes it easy to try decoding into multiple statically defined types and transform these each into some general representation you'll return. Like storage, if a return type is defined in the payload then response.decode()? is all that you'd need to decode into said static type (which is the default path if you use Subxts generated types too).
It may be worth exposing similar APIs for View Functions and possibly constants too while we are at it.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the runtime_apis().call and call_raw entry points, then compare their response handling with the storage APIs' decoding behavior. Define how a response can be decoded into multiple target types while retaining the payload's default decode path, and consider the requested View Functions and constants scope. Done means the proposed API supports the example's sequential decode attempts without repeated network calls.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, backend-api-design
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 44/100