10

I'm trying to generate an extrinsic offline based on this example. It worked, but the signed transaction is different.

Call Data

enter image description here

As you can see in the output the Call Hash is the same.

Signed Transaction

enter image description here

My code

use serde::{Serialize, Deserialize};
use sp_keyring::AccountKeyring;
use sp_runtime::{MultiAddress, MultiSignature, generic::Era};
use parity_scale_codec::{ Compact, Encode };

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SystemSyncState {
    current_block: u32,
    starting_block: u32,
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct SystemSyncStateSuccess {
    jsonrpc: String,
    result: SystemSyncState,
    id: u8,
}


#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct SystemAccountNextIndexSuccess {
    jsonrpc: String,
    result: u32,
    id: u8,
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct ChainGetBlockHashSuccess {
    jsonrpc: String,
    result: String,
    id: u8,
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum StateGetRuntimeVersionApis {
    Int(u32),
    Str(String),
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct StateGetRuntimeVersion {
    apis: Vec<[StateGetRuntimeVersionApis;2]>,
    authoring_version: u32,
    impl_name: String,
    impl_version: u32,
    spec_name: String,
    spec_version: u32,
    transaction_version: u32,
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct StateGetRuntimeVersionSuccess {
    jsonrpc: String,
    result: StateGetRuntimeVersion,
    id: u8,
}

#[derive(Serialize)]
struct NodeRequest {
    id: u8,
    jsonrpc: String,
    method: String,
    params: Vec<String>,
}
pub struct StorageKeyPrefix(Vec<u8>);

async fn get_nonce(account: &sp_runtime::AccountId32) -> u32 {

    let client = reqwest::Client::new();
    let req = NodeRequest {
        id: 1,
        jsonrpc: "2.0".to_string(),
        method: "system_accountNextIndex".to_string(),
        params: vec![account.to_string()],
    };

    let res = client
        .post("http://localhost:9933")
        .json(&serde_json::json!(req))
        .send()
        .await;

    match res {
        Ok(res) => {
            match res.status() {
                reqwest::StatusCode::OK => {
                    match res.json::<SystemAccountNextIndexSuccess>().await {
                        Ok(parsed) => {
                            // println!("NONCE {:#?}", parsed.result);
                            parsed.result
                        },
                        Err(e) => {
                            println!("ERR {:?}", e);
                            0
                        },
                    }
                },
                _ => {
                    0
                }
            }
        },
        Err(_e) => {
            0
        }
    }
}

async fn get_genesis_hash() -> [u8; 32] {
    let client = reqwest::Client::new();
    let req = NodeRequest {
        id: 1,
        jsonrpc: "2.0".to_string(),
        method: "chain_getBlockHash".to_string(),
        params: vec!["0".to_string()]
    };

    let res = client
        .post("http://localhost:9933")
        .json(&serde_json::json!(req))
        .send()
        .await;

    match res {
        Ok(res) => {
            match res.status() {
                reqwest::StatusCode::OK => {
                    match res.json::<ChainGetBlockHashSuccess>().await {
                        Ok(parsed) => {
                            println!("GENESIS HASH {:#?}", parsed.result);
                            sp_core::hashing::sha2_256(parsed.result.as_bytes())
                        },
                        Err(e) => {
                            println!("ERR {:?}", e);
                            sp_core::hashing::sha2_256(&[0])
                        },
                    }
                },
                _ => {
                    sp_core::hashing::sha2_256(&[0])
                }
            }
        },
        Err(_e) => {
            sp_core::hashing::sha2_256(&[0])
        }
    }
}

/// Fetch runtime information from the node.
async fn get_runtime_version() -> Result<StateGetRuntimeVersion, Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();
    let req = NodeRequest {
        id: 1,
        jsonrpc: "2.0".to_string(),
        method: "state_getRuntimeVersion".to_string(),
        params: vec![]
    };

    let res = client
        .post("http://localhost:9933")
        .json(&serde_json::json!(req))
        .send()
        .await;

    match res {
        Ok(res) => {
            match res.status() {
                reqwest::StatusCode::OK => {
                    match res.json::<StateGetRuntimeVersionSuccess>().await {
                        Ok(parsed) => {
                            Ok(parsed.result)
                        },
                        Err(e) => Err(String::from(e.to_string()).into())
                    }
                },
                _ => {
                    Err(String::from("Something went wrong").into())
                }
            }
        },
        Err(e) => Err(String::from(e.to_string()).into())
    }
}

fn encode_extrinsic<S: Encode ,C: Encode>(signature: Option<S>, call: C) -> Vec<u8> {
    let mut tmp: Vec<u8> = vec![];

    const EXTRINSIC_VERSION: u8 = 4;
    match signature.as_ref() {
        Some(s) => {
            tmp.push(EXTRINSIC_VERSION | 0b1000_0000);
            s.encode_to(&mut tmp);
        },
        None => {
            tmp.push(EXTRINSIC_VERSION & 0b0111_1111);
        },
    }

    call.encode_to(&mut tmp);

    let compact_len = Compact(tmp.len() as u32);

    let mut output: Vec<u8> = vec![];
    compact_len.encode_to(&mut output);
    output.extend(tmp);

    output
}


#[async_std::main]
async fn main() {

    let storage_key = "4d4c14c40d1b7ecb942455794693fa68";
   
    let pallet_index = 8;
    let method_index = 0;
    let call_index: [u8; 2] = [pallet_index, method_index];
    let address: Vec<u8> = storage_key.as_bytes().to_vec().encode();
    let owner = [0];
    let call_data  = [call_index.to_vec(), address, owner.to_vec()].concat();

    let call_hex = hex::encode(call_data.clone());
    println!("CALL HEX {:?}", call_hex);
    let hash = sp_core::blake2_256(call_data.as_ref());
    println!("CALL HASH {:?}", hex::encode(hash));

    let from = AccountKeyring::Alice.to_account_id();
    let alice_nonce = get_nonce(&from).await;
    let runtime_version = get_runtime_version().await.unwrap();
    let genesis_hash = get_genesis_hash().await;

    let extra = (
        Era::Immortal,
        Compact(alice_nonce),
        Compact(0u128)
    );

    let additional = (
        runtime_version.spec_version,
        runtime_version.transaction_version,
        genesis_hash,
        genesis_hash,
    );


    let signature = {
        let full_unsigned_payload = (&call_data, &extra, &additional);
        let full_unsigned_payload_scale_bytes = full_unsigned_payload.encode();

        if call_data.len() > 256 {
            AccountKeyring::Alice.sign(&sp_core::blake2_256(&full_unsigned_payload_scale_bytes)[..])
        } else {
            AccountKeyring::Alice.sign(&full_unsigned_payload_scale_bytes)
        }
    };


    let signature_to_encode = Some((
        MultiAddress::Id::<_,u32>(from),
        MultiSignature::Sr25519(signature),
        extra
    ));

    let payload_scale_encoded = encode_extrinsic(
        signature_to_encode,
        call_data
    );
    let payload_hex = format!("0x{}", hex::encode(&payload_scale_encoded));
    println!("PAYLOAD HEX {:?}", payload_hex);
    println!("TARGET BYTES {:?}", hex::decode("29028400d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01264459d771430ea6f806df70c10f91c783cb31ee8007751a4906fa92ddf70313431871e435f7fbddfb3974ece95c82f3d59cb8abe98d78f47cc150ae99c1b089001400080080346434633134633430643162376563623934323435353739343639336661363800").unwrap());
    println!("RESULT BYTES {:?}", payload_scale_encoded);
}

The output

CALL HEX "080080346434633134633430643162376563623934323435353739343639336661363800"
CALL HASH "9f7038a86de92cd63b1d3edce42730ffeca300c6e590e1a457e675add24ff706"
GENESIS HASH "0xeba2ea8340d05a42a50ddd5c8cbf5ac91de372cff7c4785bcab4ebbdf388584f"

PAYLOAD HEX "0x2d028400d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d01268cb1fc4beb651a7ca1b0944fe22070e702bfb9ba455391a25f5fbefc2cd22b4b4defc1df9c8b946f9be936a0b823f339fd5eed23755dfd4cca467b595be38900140090080080346434633134633430643162376563623934323435353739343639336661363800"

TARGET BYTES [41, 2, 132, 0, 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125, 1, 38, 68, 89, 215, 113, 67, 14, 166, 248, 6, 223, 112, 193, 15, 145, 199, 131, 203, 49, 238, 128, 7, 117, 26, 73, 6, 250, 146, 221, 247, 3, 19, 67, 24, 113, 228, 53, 247, 251, 221, 251, 57, 116, 236, 233, 92, 130, 243, 213, 156, 184, 171, 233, 141, 120, 244, 124, 193, 80, 174, 153, 193, 176, 137, 0, 20, 0, 8, 0, 128, 52, 100, 52, 99, 49, 52, 99, 52, 48, 100, 49, 98, 55, 101, 99, 98, 57, 52, 50, 52, 53, 53, 55, 57, 52, 54, 57, 51, 102, 97, 54, 56, 0]
RESULT BYTES [45, 2, 132, 0, 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125, 1, 38, 140, 177, 252, 75, 235, 101, 26, 124, 161, 176, 148, 79, 226, 32, 112, 231, 2, 191, 185, 186, 69, 83, 145, 162, 95, 95, 190, 252, 44, 210, 43, 75, 77, 239, 193, 223, 156, 139, 148, 111, 155, 233, 54, 160, 184, 35, 243, 57, 253, 94, 237, 35, 117, 93, 253, 76, 202, 70, 123, 89, 91, 227, 137, 0, 20, 0, 144, 8, 0, 128, 52, 100, 52, 99, 49, 52, 99, 52, 48, 100, 49, 98, 55, 101, 99, 98, 57, 52, 50, 52, 53, 53, 55, 57, 52, 54, 57, 51, 102, 97, 54, 56, 0]

The "Result Bytes" is the byte array that I generate. The "Target Bytes" array is generated by the Polkadot web client. Call Hex and Call Hash are correct and match the ones from the Polkadot web client.

Query result

If I try to execute the result Payload Hex I get an error:

enter image description here

Bytes difference

I've highlighted the bytes that differ:

TARGET BYTES [41, 2, 132, 0, 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125, 1, 38, 68, 89, 215, 113, 67, 14, 166, 248, 6, 223, 112, 193, 15, 145, 199, 131, 203, 49, 238, 128, 7, 117, 26, 73, 6, 250, 146, 221, 247, 3, 19, 67, 24, 113, 228, 53, 247, 251, 221, 251, 57, 116, 236, 233, 92, 130, 243, 213, 156, 184, 171, 233, 141, 120, 244, 124, 193, 80, 174, 153, 193, 176, 137, 0, 20, 0, 8, 0, 128, 52, 100, 52, 99, 49, 52, 99, 52, 48, 100, 49, 98, 55, 101, 99, 98, 57, 52, 50, 52, 53, 53, 55, 57, 52, 54, 57, 51, 102, 97, 54, 56, 0]

RESULT BYTES [45, 2, 132, 0, 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125, 1, 38, 140, 177, 252, 75, 235, 101, 26, 124, 161, 176, 148, 79, 226, 32, 112, 231, 2, 191, 185, 186, 69, 83, 145, 162, 95, 95, 190, 252, 44, 210, 43, 75, 77, 239, 193, 223, 156, 139, 148, 111, 155, 233, 54, 160, 184, 35, 243, 57, 253, 94, 237, 35, 117, 93, 253, 76, 202, 70, 123, 89, 91, 227, 137, 0, 20, 0, 144, 8, 0, 128, 52, 100, 52, 99, 49, 52, 99, 52, 48, 100, 49, 98, 55, 101, 99, 98, 57, 52, 50, 52, 53, 53, 55, 57, 52, 54, 57, 51, 102, 97, 54, 56, 0]

Would appreciate any help on finding the reasons of the difference in results. Thanks!

Update

This is the last version of the code

use serde::{Serialize, Deserialize};
use sp_keyring::AccountKeyring;
use sp_runtime::{MultiAddress, MultiSignature, generic::Era};
use parity_scale_codec::{ Compact, Encode };

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SystemSyncState {
    current_block: u32,
    starting_block: u32,
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct SystemSyncStateSuccess {
    jsonrpc: String,
    result: SystemSyncState,
    id: u8,
}


#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct SystemAccountNextIndexSuccess {
    jsonrpc: String,
    result: u32,
    id: u8,
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct ChainGetBlockHashSuccess {
    jsonrpc: String,
    result: String,
    id: u8,
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum StateGetRuntimeVersionApis {
    Int(u32),
    Str(String),
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct StateGetRuntimeVersion {
    apis: Vec<[StateGetRuntimeVersionApis;2]>,
    authoring_version: u32,
    impl_name: String,
    impl_version: u32,
    spec_name: String,
    spec_version: u32,
    transaction_version: u32,
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct StateGetRuntimeVersionSuccess {
    jsonrpc: String,
    result: StateGetRuntimeVersion,
    id: u8,
}

#[derive(Serialize)]
struct NodeRequest {
    id: u8,
    jsonrpc: String,
    method: String,
    params: Vec<String>,
}
pub struct StorageKeyPrefix(Vec<u8>);

async fn get_nonce(account: &sp_runtime::AccountId32) -> u32 {

    let client = reqwest::Client::new();
    let req = NodeRequest {
        id: 1,
        jsonrpc: "2.0".to_string(),
        method: "system_accountNextIndex".to_string(),
        params: vec![account.to_string()],
    };

    let res = client
        .post("http://localhost:9933")
        .json(&serde_json::json!(req))
        .send()
        .await;

    match res {
        Ok(res) => {
            match res.status() {
                reqwest::StatusCode::OK => {
                    match res.json::<SystemAccountNextIndexSuccess>().await {
                        Ok(parsed) => {
                            // println!("NONCE {:#?}", parsed.result);
                            parsed.result
                        },
                        Err(e) => {
                            println!("ERR {:?}", e);
                            0
                        },
                    }
                },
                _ => {
                    0
                }
            }
        },
        Err(_e) => {
            0
        }
    }
}

async fn get_genesis_hash() -> [u8; 32] {
    let client = reqwest::Client::new();
    let req = NodeRequest {
        id: 1,
        jsonrpc: "2.0".to_string(),
        method: "chain_getBlockHash".to_string(),
        params: vec!["0".to_string()]
    };

    let res = client
        .post("http://localhost:9933")
        .json(&serde_json::json!(req))
        .send()
        .await;

    match res {
        Ok(res) => {
            match res.status() {
                reqwest::StatusCode::OK => {
                    match res.json::<ChainGetBlockHashSuccess>().await {
                        Ok(parsed) => {
                            println!("GENESIS HASH {:#?}", parsed.result);
                            sp_core::hashing::sha2_256(parsed.result.as_bytes())
                        },
                        Err(e) => {
                            println!("ERR {:?}", e);
                            sp_core::hashing::sha2_256(&[0])
                        },
                    }
                },
                _ => {
                    sp_core::hashing::sha2_256(&[0])
                }
            }
        },
        Err(_e) => {
            sp_core::hashing::sha2_256(&[0])
        }
    }
}

async fn get_runtime_version() -> Result<StateGetRuntimeVersion, Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();
    let req = NodeRequest {
        id: 1,
        jsonrpc: "2.0".to_string(),
        method: "state_getRuntimeVersion".to_string(),
        params: vec![]
    };

    let res = client
        .post("http://localhost:9933")
        .json(&serde_json::json!(req))
        .send()
        .await;

    match res {
        Ok(res) => {
            match res.status() {
                reqwest::StatusCode::OK => {
                    match res.json::<StateGetRuntimeVersionSuccess>().await {
                        Ok(parsed) => {
                            Ok(parsed.result)
                        },
                        Err(e) => Err(String::from(e.to_string()).into())
                    }
                },
                _ => {
                    Err(String::from("Something went wrong").into())
                }
            }
        },
        Err(e) => Err(String::from(e.to_string()).into())
    }
}

fn encode_extrinsic<S: Encode ,C: Encode>(signature: Option<S>, call: C) -> Vec<u8> {
    let mut tmp: Vec<u8> = vec![];

    const EXTRINSIC_VERSION: u8 = 4;
    match signature.as_ref() {
        Some(s) => {
            tmp.push(EXTRINSIC_VERSION | 0b1000_0000);
            s.encode_to(&mut tmp);
        },
        None => {
            tmp.push(EXTRINSIC_VERSION & 0b0111_1111);
        },
    }

    call.encode_to(&mut tmp);

    let compact_len = Compact(tmp.len() as u32);

    let mut output: Vec<u8> = vec![];
    compact_len.encode_to(&mut output);
    output.extend(tmp);

    output
}


#[async_std::main]
async fn main() {

    let storage_key = "4d4c14c40d1b7ecb942455794693fa68";

    let pallet_index: u8 = 8;
    let method_index: u8 = 0;

    let address = storage_key.as_bytes().to_owned();
    let owner: [u8; 1] = [0];
    let call_data = (
        pallet_index,
        method_index,
        address,
        owner
    );

    let from = AccountKeyring::Alice.to_account_id();
    let alice_nonce = get_nonce(&from).await;
    let runtime_version = get_runtime_version().await.unwrap();
    let genesis_hash = get_genesis_hash().await;

    let extra = (
        Era::Immortal,
        Compact(alice_nonce),
        Compact(0u128)
    );

    let additional = (
        runtime_version.spec_version,
        runtime_version.transaction_version,
        genesis_hash,
        genesis_hash,
    );


    let signature = {
        let full_unsigned_payload = (&call_data, &extra, &additional);
        let full_unsigned_payload_scale_bytes = full_unsigned_payload.encode();

        if full_unsigned_payload_scale_bytes.len() > 256 {
            AccountKeyring::Alice.sign(&sp_core::blake2_256(&full_unsigned_payload_scale_bytes)[..])
        } else {
            AccountKeyring::Alice.sign(&full_unsigned_payload_scale_bytes)
        }
    };

    let signature_to_encode = Some((
        MultiAddress::Id::<_,u32>(from),
        MultiSignature::Sr25519(signature),
        extra
    ));

    let payload_scale_encoded = encode_extrinsic(
        signature_to_encode,
        call_data
    );
    let payload_hex = format!("0x{}", hex::encode(&payload_scale_encoded));
    println!("TARGET HEX {:?}", "0x29028400d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d014c6d14a111568aa33472f6061d31763a0acc15fe32a53f5cd7c0e411bcf7a272636de5498a328c34087140f329780afd9aac3e6ed94213e9215e979771173c87001400080080346434633134633430643162376563623934323435353739343639336661363800");
    println!("RESULT HEX {:?}", payload_hex);


If I understand correctly the only problem right now is the incorrect signature.

enter image description here

Bytes difference

TARGET BYTES [41, 2, 132, 0, 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125, 1, 76, 109, 20, 161, 17, 86, 138, 163, 52, 114, 246, 6, 29, 49, 118, 58, 10, 204, 21, 254, 50, 165, 63, 92, 215, 192, 228, 17, 188, 247, 162, 114, 99, 109, 229, 73, 138, 50, 140, 52, 8, 113, 64, 243, 41, 120, 10, 253, 154, 172, 62, 110, 217, 66, 19, 233, 33, 94, 151, 151, 113, 23, 60, 135, 0, 20, 0, 8, 0, 128, 52, 100, 52, 99, 49, 52, 99, 52, 48, 100, 49, 98, 55, 101, 99, 98, 57, 52, 50, 52, 53, 53, 55, 57, 52, 54, 57, 51, 102, 97, 54, 56, 0]

RESULT BYTES [41, 2, 132, 0, 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125, 1, 154, 207, 39, 54, 83, 45, 188, 252, 156, 12, 35, 43, 195, 150, 79, 241, 36, 120, 168, 73, 139, 81, 80, 155, 228, 224, 9, 41, 214, 157, 87, 38, 237, 169, 110, 227, 254, 140, 230, 192, 147, 148, 160, 32, 252, 169, 237, 183, 74, 105, 32, 63, 77, 36, 200, 207, 76, 121, 217, 250, 176, 135, 97, 134, 0, 20, 0, 8, 0, 128, 52, 100, 52, 99, 49, 52, 99, 52, 48, 100, 49, 98, 55, 101, 99, 98, 57, 52, 50, 52, 53, 53, 55, 57, 52, 54, 57, 51, 102, 97, 54, 56, 0]

3
  • 1
    Perhaps related to github.com/paritytech/subxt/issues/484 in that you might like subxt when it's able to do offline tx.
    – Nuke
    Mar 21, 2022 at 22:26
  • Yes, that is the reason why I still do not use it. I was also confused about the need to manually download node metadata first in order to use lib. Mar 22, 2022 at 17:36
  • How else could you possibly correctly form arbitrary substrate chain transactions without getting the metadata? As there is not uniform standard, why the scale-info pallet is powerful: allows chain devs to have an auto gen api (mostly) just by using a few macros in the runtime. For end users, it allows to use any substrate chain with minimal customization
    – Nuke
    Mar 24, 2022 at 20:44

2 Answers 2

10

The problem is the way you encode the call data. Since your call_data in main is a vector, when you encode it into the temporary byte vector in encode_extrinsic, SCALE first appends the length of the vector per specification. You can see the result of this as the excess byte 144 in your "result bytes" at the end of that greyed-out section, right before the module index byte 8.

So inside encode_extrinsic, instead of doing:

call.encode_to(&mut tmp);

you should just append the contents of call to tmp. You can do it by replacing encode_extrinsic with something similar to:

fn encode_extrinsic<S: Encode>(
    signature: Option<S>,
    call: Vec<u8>,
) -> Vec<u8> {
    let mut tmp: Vec<u8> = vec![];

    const EXTRINSIC_VERSION: u8 = 4;
    match signature.as_ref() {
        Some(s) => {
            tmp.push(EXTRINSIC_VERSION | 0b1000_0000);
            s.encode_to(&mut tmp);
        },
        None => {
            tmp.push(EXTRINSIC_VERSION & 0b0111_1111);
        },
    }

    tmp.extend(call);

    let compact_len = Compact(tmp.len() as u32);

    let mut output: Vec<u8> = vec![];
    compact_len.encode_to(&mut output);
    output.extend(tmp);

    output
}

This should also fix the difference in the first byte, which is caused by this excess byte.

Rest of the difference is the signature, which I think is expected.

NOTE: The difference between your code and the Parity example is that the latter uses a tuple for the call data.

EDIT:

There's another bit that differs in your code from the example. You're checking whether call_data is longer than 256 bytes, but you should be checking whether full_unsigned_payload_scale_bytes is so when you're signing the payload:

#[async_std::main]
async fn main() {

    // ...

    let signature = {
        // ...
        if full_unsigned_payload_scale_bytes.len() > 256 {
            AccountKeyring::Alice.sign(&sp_core::blake2_256(&full_unsigned_payload_scale_bytes)[..])
        } else {
            AccountKeyring::Alice.sign(&full_unsigned_payload_scale_bytes)
        }
    };
    // ...
}

This may fix the signature issue if full_unsigned_payload_scale_bytes ends up longer than 256 bytes.

3
  • 1
    Thank you very much for your help. Thanks to you, I've figured out how to fix the first byte. The signature is still not valid though (both with tuple and Vec). Mar 22, 2022 at 12:16
  • 1
    No problem! There's one more issue with your code that differs from the Parity example, added an edit to the end of the answer, might fix it. Good luck @AmirBoziev.
    – kukabi
    Mar 22, 2022 at 12:58
  • Thanks, appreciate it @kukabi Mar 22, 2022 at 16:10
2

Working code:

use serde::{Serialize, Deserialize};
use std::str::FromStr;
use sp_keyring::AccountKeyring;
use sp_runtime::{MultiAddress, MultiSignature, generic::Era};
use parity_scale_codec::{ Compact, Encode };
use sp_core::H256;


#[allow(dead_code)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct SystemSyncState {
    current_block: u32,
    starting_block: u32,
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct SystemSyncStateSuccess {
    jsonrpc: String,
    result: SystemSyncState,
    id: u8,
}


#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct SystemAccountNextIndexSuccess {
    jsonrpc: String,
    result: u32,
    id: u8,
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct ChainGetBlockHashSuccess {
    jsonrpc: String,
    result: String,
    id: u8,
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum StateGetRuntimeVersionApis {
    Int(u32),
    Str(String),
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct StateGetRuntimeVersion {
    apis: Vec<[StateGetRuntimeVersionApis;2]>,
    authoring_version: u32,
    impl_name: String,
    impl_version: u32,
    spec_name: String,
    spec_version: u32,
    transaction_version: u32,
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct StateGetRuntimeVersionSuccess {
    jsonrpc: String,
    result: StateGetRuntimeVersion,
    id: u8,
}

#[derive(Serialize)]
struct NodeRequest {
    id: u8,
    jsonrpc: String,
    method: String,
    params: Vec<String>,
}
pub struct StorageKeyPrefix(Vec<u8>);

async fn get_nonce(account: &sp_runtime::AccountId32) -> Result<u32, Box<dyn std::error::Error>> {

    let client = reqwest::Client::new();
    let req = NodeRequest {
        id: 1,
        jsonrpc: "2.0".to_string(),
        method: "system_accountNextIndex".to_string(),
        params: vec![account.to_string()],
    };

    let res = client
        .post("http://localhost:9933")
        .json(&serde_json::json!(req))
        .send()
        .await;

    match res {
        Ok(res) => {
            match res.status() {
                reqwest::StatusCode::OK => {
                    match res.json::<SystemAccountNextIndexSuccess>().await {
                        Ok(parsed) => {
                            Ok(parsed.result)
                        },
                        Err(e) => Err(String::from(e.to_string()).into())
                    }
                },
                _ => Err(String::from("Something went wrong").into())
            }
        },
        Err(e) => Err(String::from(e.to_string()).into())
    }
}

async fn get_genesis_hash() -> Result<H256, Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();
    let req = NodeRequest {
        id: 1,
        jsonrpc: "2.0".to_string(),
        method: "chain_getBlockHash".to_string(),
        params: vec!["0".to_string()]
    };

    let res = client
        .post("http://localhost:9933")
        .json(&serde_json::json!(req))
        .send()
        .await;

    match res {
        Ok(res) => {
            match res.status() {
                reqwest::StatusCode::OK => {
                    match res.json::<ChainGetBlockHashSuccess>().await {
                        Ok(parsed) => {
                            let genesis_hash = parsed.result.replace("0x", "");
                            Ok(sp_core::H256::from_str(genesis_hash.as_ref()).unwrap())
                        },
                        Err(e) => Err(String::from(e.to_string()).into())
                    }
                },
                _ => Err(String::from("Something went wrong").into())
            }
        },
        Err(e) => Err(String::from(e.to_string()).into())
    }
}

async fn get_runtime_version() -> Result<StateGetRuntimeVersion, Box<dyn std::error::Error>> {
    let client = reqwest::Client::new();
    let req = NodeRequest {
        id: 1,
        jsonrpc: "2.0".to_string(),
        method: "state_getRuntimeVersion".to_string(),
        params: vec![]
    };

    let res = client
        .post("http://localhost:9933")
        .json(&serde_json::json!(req))
        .send()
        .await;

    match res {
        Ok(res) => {
            match res.status() {
                reqwest::StatusCode::OK => {
                    match res.json::<StateGetRuntimeVersionSuccess>().await {
                        Ok(parsed) => {
                            Ok(parsed.result)
                        },
                        Err(e) => Err(String::from(e.to_string()).into())
                    }
                },
                _ => Err(String::from("Something went wrong").into())
            }
        },
        Err(e) => Err(String::from(e.to_string()).into())
    }
}

#[async_std::main]
async fn main() {
    println!("TARGET HEX {:?}", "0x29028400d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d013ce459f22bd4e8aac926c28ef632ca398216cb735f6c738243ccd0f0215aa20fd00fb01c5951d4a15959ae6e94af4dff0884d84878eed913e5e558419e607284002400080080346434633134633430643162376563623934323435353739343639336661363800");
    let xt = create_extrinsic_call().await;
    let xt_hex = hex::encode(&xt);
    println!("RESULT HEX {:?}", format!("0x{}", xt_hex));
}


#[allow(dead_code)]
async fn create_storage_key() -> String {

    let client = reqwest::Client::new();
    let req = NodeRequest {
        id: 1,
        jsonrpc: "2.0".to_string(),
        method: "system_syncState".to_string(),
        params: vec![],
    };

    let res = client
        .post("http://localhost:9933")
        .json(&serde_json::json!(req))
        .send()
        .await;

    match res {
        Ok(res) => {
            match res.status() {
                reqwest::StatusCode::OK => {
                    match res.json::<SystemSyncStateSuccess>().await {
                        Ok(_parsed) => {
                            // println!("BLOCK {:?}", parsed.result.current_block);
                            // self.height = Some(parsed.result.current_block);
                            // self.connection_status = ConnectionStatus::Success;
                        },
                        Err(e) => {
                            println!("ERR {:?}", e)
                        },
                    }
                },
                _ => {}
            }
        },
        Err(_e) => {}
    }

    let module_name = "Nolik";
    let module_name_twox = sp_core::twox_128(module_name.as_bytes());
    let module_name_hex = hex::encode(module_name_twox.clone());
    println!("MODULE {:?}", module_name_hex);

    let method_name = "AddressOwners";
    let method_name_twox = sp_core::twox_128(method_name.as_bytes());
    let method_name_hex = hex::encode(method_name_twox.clone());
    println!("METHOD {:?}", method_name_hex);

    let storage_key = "4d4c14c40d1b7ecb942455794693fa68";
    let storage_key_twox = sp_core::twox_64(storage_key.as_bytes().encode().as_ref());
    let storage_key_hex = hex::encode(storage_key_twox.clone());
    println!("TWOX64 {:?}", storage_key_hex);

    let twox_64_concat: Vec<u8> = storage_key_twox
        .iter()
        .chain::<&[u8]>(storage_key.as_bytes().encode().as_ref())
        .cloned()
        .collect();

    let twox_64_concat_hex = hex::encode(twox_64_concat.clone());
    println!("TWOX64CONCAT {:?}", twox_64_concat_hex);

    let key = format!("0x{}{}{}", module_name_hex, method_name_hex, twox_64_concat_hex);
    key
}

async fn create_extrinsic_call() -> Vec<u8> {

    let alice = AccountKeyring::Alice.to_account_id();
    let alice_nonce = get_nonce(&alice).await.unwrap();
    let runtime_version = get_runtime_version().await.unwrap();
    let genesis_hash = get_genesis_hash().await.unwrap();

    let pallet_index: u8 = 8;
    let method_index: u8 = 0;
    let call_index: [u8; 2] = [pallet_index, method_index];
    // extrinsic parameter
    let address: Vec<u8> = "3e341291aa132c446f752265fee146bb".as_bytes().into();
    // extrinsic parameter
    let owner: [u8; 1] = [0];

    let call: Vec<u8>  = [call_index.to_vec(), address.encode().clone(), owner.to_vec()].concat();


    let extra = (
        Era::Immortal,
        Compact(alice_nonce),
        Compact(0u128),
    );

    let additional = (
        runtime_version.spec_version,
        runtime_version.transaction_version,
        genesis_hash,
        genesis_hash,
    );

   let call_tup = (
       pallet_index,
       method_index,
       address.clone(),
       owner
   );

    let payload = (
        &call_tup,
        &extra,
        &additional
    );

    let signature = payload.using_encoded(|payload| AccountKeyring::Alice.sign(&payload));
    let extrinsic = {
        let mut encoded_inner = Vec::new();

        (0b1000_0000 + 4u8).encode_to(&mut encoded_inner);

        MultiAddress::Id::<_, u32>(alice.clone()).encode_to(&mut encoded_inner);
        MultiSignature::from(signature).encode_to(&mut encoded_inner);
        extra.encode_to(&mut encoded_inner);
        encoded_inner.extend(&call);

        let len = Compact(encoded_inner.len() as u32);
        let mut encoded = Vec::new();
        len.encode_to(&mut encoded);
        encoded.extend(&encoded_inner);
        encoded
    };

    extrinsic
}

The output generates a Call Hex which looks like this: 0x29028400d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d0142faddae706311cd9113a391aa9fc9827d23a3d66010f08592e217effd7dae38c8c1461b3aed62a1e89c91c3b986dcf187ab04c626ed32971ddfd04d53968a82003800080080336533343132393161613133326334343666373532323635666565313436626200

Now it can be used to send an RPC Request enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.