paritytech / paritytech/subxt

Add additional method to parse raw call data bytes into an unsigned payload so it can be signed and submitted later on

Open
#2,194 8 comments 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

Hi @jsdw,

Opening this issue to share what I'm currently doing since I haven't found a default option on the latest version of subxt and I think it might be good to have something similar available.

I often find it useful to keep the raw bytes exported from here tx().call_data(&proxy_call)? around, and only sign and submit on a second stage. I couldn't find a method on the latest version that could easily decode these bytes, the option tx().from_bytes(encoded) requires the encoded data to be already signed. I have looked to the other methods on how to create partial transaction, but they don't seem to apply here.

Do you have any suggestions?

Sharing the steps and code blocks to demonstrate my case:

  1. Export call data:
pub fn wrap_call_into_proxy(
    api: &ClientAtBlock<SubstrateConfig, OnlineClientAtBlockImpl<SubstrateConfig>>,
    call: RuntimeCall,
    proxied_account: &AccountId32,
) -> Result<Vec<u8>, Error> {
    let proxy_call = node_runtime::tx().proxy().proxy(
        (*proxied_account).into(),
        Some(ProxyType::NonTransfer),
        call,
    );

    let payload = api.tx().call_data(&proxy_call).boxed()?;

    Ok(payload)
}
  1. With the chain Metadata available, decode the original call data from previous method wrap_call_into_proxy, sign it with a valid Keypair and submit:
    async fn sign_and_submit_call_data(
        &self,
        api: &OnlineClient<SubstrateConfig>,
        proxy_signer: &Keypair,
        call_data: &[u8],
    ) -> Result<Response, Error> {
        let at_block = api.at_current_block().await.boxed()?;
        let metadata = at_block.metadata();
        let payload = RawPayload::from_bytes(&metadata, call_data).boxed()?;
        let response = api
            .tx()
            .await
            .boxed()?
            .sign_and_submit_then_watch_default(&payload, proxy_signer)
            .await
            .boxed()?;

        Ok(Response::transaction_submitted(response))
    }
  1. RawPayload implementation:
use scale_encode::{EncodeAsFields, Error as EncodeError, FieldIter, TypeResolver};
use subxt::{
    transactions::{Payload, ValidationDetails},
    Metadata,
};

pub type Bytes = Vec<u8>;

pub struct RawPayload {
    pallet_name: String,
    call_name: String,
    field_bytes: RawFields,
}

pub struct RawFields(Bytes);

impl EncodeAsFields for RawFields {
    fn encode_as_fields_to<R: TypeResolver>(
        &self,
        _fields: &mut dyn FieldIter<'_, R::TypeId>,
        _types: &R,
        out: &mut Bytes,
    ) -> Result<(), EncodeError> {
        out.extend_from_slice(&self.0);
        Ok(())
    }
}

impl Payload for RawPayload {
    type CallData = RawFields;

    fn pallet_name(&self) -> &str {
        &self.pallet_name
    }
    fn call_name(&self) -> &str {
        &self.call_name
    }
    fn call_data(&self) -> &RawFields {
        &self.field_bytes
    }
    fn validation_details(&self) -> Option<ValidationDetails<'_>> {
        None
    }
}

impl RawPayload {
    pub fn from_bytes(metadata: &Metadata, bytes: &[u8]) -> Result<Self, Error> {
        let pallet = metadata
            .pallet_by_call_index(bytes[0])
            .ok_or(Error::PalletNotFound)?;
        let call_variant = pallet
            .call_variant_by_index(bytes[1])
            .ok_or(Error::CallNotFound)?;

        Ok(Self {
            pallet_name: pallet.name().to_string(),
            call_name: call_variant.name.clone(),
            field_bytes: RawFields(bytes[2..].to_vec()),
        })
    }
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("Pallet not found")]
    PalletNotFound,
    #[error("Call not found")]
    CallNotFound,
}

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 by reviewing the tx().call_data and tx().from_bytes entry points, then compare them with the RawPayload and Payload implementation shown in the issue. Check how metadata resolves pallet and call indexes and how transaction signing consumes Payload. Done means a supported path exists for decoding exported raw call data and signing and submitting it later, with coverage for the demonstrated flow.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
api, blockchain
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.