subxt_utils_fetchmetadata/
url.rs

1// Copyright 2019-2024 Parity Technologies (UK) Ltd.
2// This file is dual-licensed as Apache-2.0 or GPL-3.0.
3// see LICENSE for license details.
4
5//! Fetch metadata from a URL.
6
7use crate::Error;
8use codec::{Decode, Encode};
9use jsonrpsee::{
10    core::client::ClientT, http_client::HttpClientBuilder, rpc_params, ws_client::WsClientBuilder,
11};
12
13pub use url::Url;
14
15/// The metadata version that is fetched from the node.
16#[derive(Default, Debug, Clone, Copy)]
17pub enum MetadataVersion {
18    /// Latest stable version of the metadata.
19    #[default]
20    Latest,
21    /// Fetch a specified version of the metadata.
22    Version(u32),
23    /// Latest unstable version of the metadata.
24    Unstable,
25}
26
27// Note: Implementation needed for the CLI tool.
28impl std::str::FromStr for MetadataVersion {
29    type Err = String;
30
31    fn from_str(input: &str) -> Result<Self, Self::Err> {
32        match input {
33            "unstable" => Ok(MetadataVersion::Unstable),
34            "latest" => Ok(MetadataVersion::Latest),
35            version => {
36                let num: u32 = version
37                    .parse()
38                    .map_err(|_| format!("Invalid metadata version specified {version:?}"))?;
39
40                Ok(MetadataVersion::Version(num))
41            }
42        }
43    }
44}
45
46/// Returns the metadata bytes from the provided URL.
47pub async fn from_url(url: Url, version: MetadataVersion) -> Result<Vec<u8>, Error> {
48    let bytes = match url.scheme() {
49        "http" | "https" => fetch_metadata_http(url, version).await,
50        "ws" | "wss" => fetch_metadata_ws(url, version).await,
51        invalid_scheme => Err(Error::InvalidScheme(invalid_scheme.to_owned())),
52    }?;
53
54    Ok(bytes)
55}
56
57/// Returns the metadata bytes from the provided URL, blocking the current thread.
58pub fn from_url_blocking(url: Url, version: MetadataVersion) -> Result<Vec<u8>, Error> {
59    tokio_block_on(from_url(url, version))
60}
61
62// Block on some tokio runtime for sync contexts
63fn tokio_block_on<T, Fut: std::future::Future<Output = T>>(fut: Fut) -> T {
64    tokio::runtime::Builder::new_multi_thread()
65        .enable_all()
66        .build()
67        .unwrap()
68        .block_on(fut)
69}
70
71async fn fetch_metadata_ws(url: Url, version: MetadataVersion) -> Result<Vec<u8>, Error> {
72    let client = WsClientBuilder::default()
73        .request_timeout(std::time::Duration::from_secs(180))
74        .max_buffer_capacity_per_subscription(4096)
75        .build(url)
76        .await?;
77
78    fetch_metadata(client, version).await
79}
80
81async fn fetch_metadata_http(url: Url, version: MetadataVersion) -> Result<Vec<u8>, Error> {
82    let client = HttpClientBuilder::default()
83        .request_timeout(std::time::Duration::from_secs(180))
84        .build(url)?;
85
86    fetch_metadata(client, version).await
87}
88
89/// The innermost call to fetch metadata:
90async fn fetch_metadata(client: impl ClientT, version: MetadataVersion) -> Result<Vec<u8>, Error> {
91    const UNSTABLE_METADATA_VERSION: u32 = u32::MAX;
92
93    // Fetch available metadata versions. If error, revert to legacy metadata code.
94    async fn fetch_available_versions(
95        client: &impl ClientT,
96    ) -> Result<Vec<u32>, Error> {
97        let res: String = client
98            .request("state_call", rpc_params!["Metadata_metadata_versions", "0x"])
99            .await?;
100        let raw_bytes = hex::decode(res.trim_start_matches("0x"))?;
101        Decode::decode(&mut &raw_bytes[..]).map_err(Into::into)
102    }
103
104    // Fetch metadata using the "new" state_call interface
105    async fn fetch_inner(
106        client: &impl ClientT,
107        version: MetadataVersion,
108        supported_versions: Vec<u32>,
109    ) -> Result<Vec<u8>, Error> {
110        // Return the version the user wants if it's supported:
111        let version = match version {
112            MetadataVersion::Latest => *supported_versions
113                .iter()
114                .filter(|&&v| v != UNSTABLE_METADATA_VERSION)
115                .max()
116                .ok_or_else(|| Error::Other("No valid metadata versions returned".to_string()))?,
117            MetadataVersion::Unstable => {
118                if supported_versions.contains(&UNSTABLE_METADATA_VERSION) {
119                    UNSTABLE_METADATA_VERSION
120                } else {
121                    return Err(Error::Other(
122                        "The node does not have an unstable metadata version available".to_string(),
123                    ));
124                }
125            }
126            MetadataVersion::Version(version) => {
127                if supported_versions.contains(&version) {
128                    version
129                } else {
130                    return Err(Error::Other(format!(
131                        "The node does not have metadata version {version} available"
132                    )));
133                }
134            }
135        };
136
137        let bytes = version.encode();
138        let version: String = format!("0x{}", hex::encode(&bytes));
139
140        // Fetch the metadata at that version:
141        let metadata_string: String = client
142            .request(
143                "state_call",
144                rpc_params!["Metadata_metadata_at_version", &version],
145            )
146            .await?;
147        // Decode the metadata.
148        let metadata_bytes = hex::decode(metadata_string.trim_start_matches("0x"))?;
149        let metadata: Option<frame_metadata::OpaqueMetadata> =
150            Decode::decode(&mut &metadata_bytes[..])?;
151        let Some(metadata) = metadata else {
152            return Err(Error::Other(format!(
153                "The node does not have metadata version {version} available"
154            )));
155        };
156        Ok(metadata.0)
157    }
158
159    // Fetch metadata using the "old" state_call interface
160    async fn fetch_inner_legacy(
161        client: &impl ClientT,
162    ) -> Result<Vec<u8>, Error> {
163        // Fetch the metadata.
164        let metadata_string: String = client
165            .request("state_call", rpc_params!["Metadata_metadata", "0x"])
166            .await?;
167
168        // Decode the metadata.
169        let metadata_bytes = hex::decode(metadata_string.trim_start_matches("0x"))?;
170        let metadata: frame_metadata::OpaqueMetadata = Decode::decode(&mut &metadata_bytes[..])?;
171        Ok(metadata.0)
172    }
173
174    match fetch_available_versions(&client).await {
175        Ok(supported_versions) => {
176            fetch_inner(&client, version, supported_versions).await
177        },
178        Err(e) => {
179            // The "new" interface failed. if the user is asking for V14 or the "latest"
180            // metadata then try the legacy interface instead. Else, just return the
181            // reason for failure.
182            if matches!(version, MetadataVersion::Version(14) | MetadataVersion::Latest) {
183                fetch_inner_legacy(&client).await
184            } else {
185                Err(e)
186            }
187        }
188    }
189}