contract_extrinsics/
rpc.rs

1// Copyright (C) Use Ink (UK) Ltd.
2// This file is part of cargo-contract.
3//
4// cargo-contract is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// cargo-contract is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with cargo-contract.  If not, see <http://www.gnu.org/licenses/>.
16
17use std::str::FromStr;
18
19use contract_transcode::AccountId32;
20use subxt::{
21    backend::rpc::{
22        RawValue,
23        RpcClient,
24        RpcParams,
25    },
26    ext::scale_value::{
27        stringify::{
28            from_str_custom,
29            ParseError,
30        },
31        Value,
32    },
33};
34
35use crate::url_to_string;
36use anyhow::{
37    anyhow,
38    bail,
39    Result,
40};
41
42pub struct RawParams(Option<Box<RawValue>>);
43
44impl RawParams {
45    /// Creates a new `RawParams` instance from a slice of string parameters.
46    /// Returns a `Result` containing the parsed `RawParams` or an error if parsing fails.
47    pub fn new(params: &[String]) -> Result<Self> {
48        let mut str_parser = from_str_custom();
49        str_parser = str_parser.add_custom_parser(custom_hex_parse);
50        str_parser = str_parser.add_custom_parser(custom_ss58_parse);
51
52        let value_params = params
53            .iter()
54            .map(|e| str_parser.parse(e).0)
55            .collect::<Result<Vec<_>, ParseError>>()
56            .map_err(|e| anyhow::anyhow!("Method parameters parsing failed: {e}"))?;
57
58        let params = match value_params.is_empty() {
59            true => None,
60            false => {
61                value_params
62                    .iter()
63                    .try_fold(RpcParams::new(), |mut v, e| {
64                        v.push(e)?;
65                        Ok(v)
66                    })
67                    .map_err(|e: subxt::Error| {
68                        anyhow::anyhow!("Building method parameters failed: {e}")
69                    })?
70                    .build()
71            }
72        };
73
74        Ok(Self(params))
75    }
76}
77
78pub struct RpcRequest(RpcClient);
79
80impl RpcRequest {
81    /// Creates a new `RpcRequest` instance.
82    pub async fn new(url: &url::Url) -> Result<Self> {
83        let rpc = RpcClient::from_url(url_to_string(url)).await?;
84        Ok(Self(rpc))
85    }
86
87    /// Performs a raw RPC call with the specified method and parameters.
88    /// Returns a `Result` containing the raw RPC call result or an error if the call
89    /// fails.
90    pub async fn raw_call<'a>(
91        &'a self,
92        method: &'a str,
93        params: RawParams,
94    ) -> Result<Box<RawValue>> {
95        let methods = self.get_supported_methods().await?;
96        if !methods.iter().any(|e| e == method) {
97            bail!(
98                "Method not found, supported methods: {}",
99                methods.join(", ")
100            );
101        }
102        self.0
103            .request_raw(method, params.0)
104            .await
105            .map_err(|e| anyhow!("Raw RPC call failed: {e}"))
106    }
107
108    /// Retrieves the supported RPC methods.
109    /// Returns a `Result` containing a vector of supported RPC methods or an error if the
110    /// call fails.
111    async fn get_supported_methods(&self) -> Result<Vec<String>> {
112        let result = self
113            .0
114            .request_raw("rpc_methods", None)
115            .await
116            .map_err(|e| anyhow!("Rpc call 'rpc_methods' failed: {e}"))?;
117
118        let result_value: serde_json::Value = serde_json::from_str(result.get())?;
119
120        let methods = result_value
121            .get("methods")
122            .and_then(|v| v.as_array())
123            .ok_or_else(|| anyhow!("Methods field parsing failed!"))?;
124
125        // Exclude unupported methods using pattern matching
126        let patterns = ["watch", "unstable", "subscribe"];
127        let filtered_methods: Vec<String> = methods
128            .iter()
129            .filter_map(|v| v.as_str().map(String::from))
130            .filter(|s| {
131                patterns
132                    .iter()
133                    .all(|&pattern| !s.to_lowercase().contains(pattern))
134            })
135            .collect();
136
137        Ok(filtered_methods)
138    }
139}
140
141/// Parse hex to string
142fn custom_hex_parse(s: &mut &str) -> Option<Result<Value<()>, ParseError>> {
143    if !s.starts_with("0x") {
144        return None
145    }
146
147    let end_idx = s
148        .find(|c: char| !c.is_ascii_alphanumeric())
149        .unwrap_or(s.len());
150    let hex = &s[..end_idx];
151    *s = &s[end_idx..];
152    Some(Ok(Value::string(hex.to_string())))
153}
154
155/// Parse ss58 address to string
156fn custom_ss58_parse(s: &mut &str) -> Option<Result<Value<()>, ParseError>> {
157    let end_idx = s
158        .find(|c: char| !c.is_ascii_alphanumeric())
159        .unwrap_or(s.len());
160    let account = AccountId32::from_str(&s[..end_idx]).ok()?;
161
162    *s = &s[end_idx..];
163    Some(Ok(Value::string(format!("0x{}", hex::encode(account.0)))))
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    fn assert_raw_params_value(input: &[&str], expected: &str) {
170        let input = input.iter().map(|e| e.to_string()).collect::<Vec<String>>();
171        let raw_params = RawParams::new(&input).expect("Raw param shall be created");
172        let expected = expected
173            .chars()
174            .filter(|&c| !c.is_whitespace())
175            .collect::<String>();
176        assert_eq!(raw_params.0.unwrap().get(), expected);
177    }
178
179    #[test]
180    fn parse_ss58_works() {
181        let expected = r#"["0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d","sr25"]"#;
182        let input = &[
183            "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
184            "\"sr25\"",
185        ];
186        assert_raw_params_value(input, expected);
187    }
188
189    #[test]
190    fn parse_seq_works() {
191        let expected = r#"[[1,"0x1234",true]]"#;
192        let input = &["(1, 0x1234, true)"];
193        assert_raw_params_value(input, expected);
194    }
195
196    #[test]
197    fn parse_map_works() {
198        let expected = r#"[{
199            "hello": true,
200            "a": 4,
201            "b": "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d",
202            "c": "test"
203        }]"#;
204        let input = &["{hello: true, a: 4, b: \
205        5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY, c: \"test\"}"];
206        assert_raw_params_value(input, expected);
207    }
208}