paritytech / paritytech/subxt

Improve "Runtime API" / "View Function" APIs: allow trying to decode results into multiple target types, like Storage APIs do.

Open
#2,199 1 comment 0 reactions 0 assignees View on GitHub

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.