1
// Copyright (C) Moondance Labs Ltd.
2
// This file is part of Tanssi.
3

            
4
// Tanssi 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
// Tanssi 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 Tanssi.  If not, see <http://www.gnu.org/licenses/>
16

            
17
//! Helper functions to convert from `ContainerChainGenesisData` to JSON values and back
18

            
19
use {
20
    crate::{ContainerChainGenesisData, ContainerChainGenesisDataItem, Properties},
21
    cumulus_primitives_core::ParaId,
22
};
23

            
24
pub type ContainerChainGenesisDataResult =
25
    Result<(ParaId, ContainerChainGenesisData, Vec<Vec<u8>>), String>;
26

            
27
/// Reads a raw ChainSpec file stored in `path`, and returns its `ParaId` and
28
/// a `ContainerChainGenesisData` that can be used to recreate the ChainSpec later.
29
pub fn container_chain_genesis_data_from_path(path: &str) -> ContainerChainGenesisDataResult {
30
    // Read raw chainspec file
31
    let raw_chainspec_str = std::fs::read_to_string(path)
32
        .map_err(|_e| format!("ChainSpec for container chain not found at {:?}", path))?;
33

            
34
    container_chain_genesis_data_from_str(&raw_chainspec_str)
35
}
36

            
37
pub fn container_chain_genesis_data_from_str(
38
    raw_chainspec_str: &str,
39
) -> ContainerChainGenesisDataResult {
40
    let raw_chainspec_json: serde_json::Value =
41
        serde_json::from_str(raw_chainspec_str).map_err(|e| e.to_string())?;
42

            
43
    container_chain_genesis_data_from_json(&raw_chainspec_json)
44
}
45

            
46
pub fn container_chain_genesis_data_from_json(
47
    raw_chainspec_json: &serde_json::Value,
48
) -> ContainerChainGenesisDataResult {
49
    // TODO: we are manually parsing a json file here, maybe we can leverage the existing
50
    // chainspec deserialization code.
51
    // TODO: this bound checking may panic, but that shouldn't be too dangerous because this
52
    // function is only used by the `build-spec` command.
53
    let para_id: u32 = u32::try_from(raw_chainspec_json["para_id"].as_u64().unwrap()).unwrap();
54
    let name: String = raw_chainspec_json["name"].as_str().unwrap().to_owned();
55
    let id: String = raw_chainspec_json["id"].as_str().unwrap().to_owned();
56
    let fork_id: Option<String> = raw_chainspec_json["fork_id"].as_str().map(|x| x.to_owned());
57
    let genesis_raw_top_json = &raw_chainspec_json["genesis"]["raw"]["top"];
58
    let storage = storage_from_chainspec_json(genesis_raw_top_json)?;
59
    let properties_json = &raw_chainspec_json["properties"];
60
    let properties = properties_from_chainspec_json(properties_json);
61
    let boot_nodes: Vec<serde_json::Value> =
62
        raw_chainspec_json["bootNodes"].as_array().unwrap().clone();
63
    let boot_nodes: Vec<Vec<u8>> = boot_nodes
64
        .into_iter()
65
        .map(|x| {
66
            let bytes = x.as_str().unwrap().as_bytes();
67
            bytes.to_vec()
68
        })
69
        .collect();
70

            
71
    Ok((
72
        para_id.into(),
73
        ContainerChainGenesisData {
74
            storage,
75
            name: name.into(),
76
            id: id.into(),
77
            fork_id: fork_id.map(|x| x.into()),
78
            extensions: vec![],
79
            properties,
80
        },
81
        boot_nodes,
82
    ))
83
}
84

            
85
pub fn storage_from_chainspec_json(
86
    genesis_raw_top_json: &serde_json::Value,
87
) -> Result<Vec<ContainerChainGenesisDataItem>, String> {
88
    let genesis_data_map = genesis_raw_top_json
89
        .as_object()
90
        .ok_or("genesis.raw.top is not an object".to_string())?;
91

            
92
    let mut genesis_data_vec = Vec::with_capacity(genesis_data_map.len());
93

            
94
    for (key, value) in genesis_data_map {
95
        let key_hex = key
96
            .strip_prefix("0x")
97
            .ok_or("key does not start with 0x".to_string())?;
98
        let value = value.as_str().ok_or("value is not a string".to_string())?;
99
        let value_hex = value
100
            .strip_prefix("0x")
101
            .ok_or("value does not start with 0x".to_string())?;
102

            
103
        let key_bytes = hex::decode(key_hex).map_err(|e| e.to_string())?;
104
        let value_bytes = hex::decode(value_hex).map_err(|e| e.to_string())?;
105

            
106
        genesis_data_vec.push((key_bytes, value_bytes).into());
107
    }
108

            
109
    // This sort is just to make the UI a bit easier to follow,
110
    // sorting the storage is not a requirement.
111
    // Maybe it is not even needed if the `genesis_data_map` iterator is ordered.
112
    // Unstable sort is fine because this was created by iterating over a map,
113
    // so it won't have two equal keys
114
    genesis_data_vec.sort_unstable();
115

            
116
    Ok(genesis_data_vec)
117
}
118

            
119
/// Read `TokenMetadata` from a JSON value. The value is expected to be a map.
120
/// In case of error, the default `TokenMetadata` is returned.
121
pub fn properties_from_chainspec_json(properties_json: &serde_json::Value) -> Properties {
122
    let mut properties: Properties = Properties::default();
123
    if let Some(x) = properties_json
124
        .get("ss58Format")
125
        .and_then(|x| u32::try_from(x.as_u64()?).ok())
126
        .or_else(|| {
127
            log::warn!(
128
                "Failed to read properties.ss58Format from container chain chain spec, using default value instead. Invalid value was: {:?}",
129
                properties_json.get("ss58Format")
130
            );
131

            
132
            None
133
        })
134
    {
135
        properties.token_metadata.ss58_format = x;
136
    }
137
    if let Some(x) = properties_json
138
        .get("tokenDecimals")
139
        .and_then(|x: &serde_json::Value| u32::try_from(x.as_u64()?).ok()).or_else(|| {
140
            log::warn!(
141
                "Failed to read properties.tokenDecimals from container chain chain spec, using default value instead. Invalid value was: {:?}",
142
                properties_json.get("tokenDecimals")
143
            );
144

            
145
            None
146
        })
147
    {
148
        properties.token_metadata.token_decimals = x;
149
    }
150
    if let Some(x) = properties_json.get("tokenSymbol").and_then(|x| {
151
        let xs = x.as_str()?;
152
        let xv: Vec<u8> = xs.to_string().into();
153

            
154
        xv.try_into().ok()
155
    }).or_else(|| {
156
        log::warn!(
157
            "Failed to read properties.tokenSymbol from container chain chain spec, using default value instead. Invalid value was: {:?}",
158
            properties_json.get("tokenSymbol")
159
        );
160

            
161
        None
162
    }) {
163
        properties.token_metadata.token_symbol = x;
164
    }
165
    if let Some(x) = properties_json.get("isEthereum").and_then(|x| {
166
        x.as_bool()
167
    }).or_else(|| {
168
        log::warn!(
169
            "Failed to read properties.isEthereum from container chain chain spec, using default value instead. Invalid value was: {:?}",
170
            properties_json.get("isEthereum")
171
        );
172

            
173
        None
174
    }) {
175
        properties.is_ethereum = x;
176
    }
177

            
178
    properties
179
}
180

            
181
pub fn properties_to_map(
182
    properties: &Properties,
183
) -> Result<serde_json::Map<String, serde_json::Value>, String> {
184
    // TODO: we can just derive Serialize for genesis_data.properties instead of this hack,
185
    // just ensure that the field names match. And "tokenSymbol" must be a string, in the struct
186
    // it is defined as a Vec<u8>.
187
    let properties = vec![
188
        (
189
            "ss58Format",
190
            serde_json::Value::from(properties.token_metadata.ss58_format),
191
        ),
192
        (
193
            "tokenDecimals",
194
            serde_json::Value::from(properties.token_metadata.token_decimals),
195
        ),
196
        (
197
            "tokenSymbol",
198
            serde_json::Value::from(
199
                String::from_utf8(properties.token_metadata.token_symbol.to_vec())
200
                    .map_err(|e| format!("tokenSymbol is not valid UTF8: {}", e))?,
201
            ),
202
        ),
203
        (
204
            "isEthereum",
205
            serde_json::Value::from(properties.is_ethereum),
206
        ),
207
    ]
208
    .into_iter()
209
    .map(|(k, v)| (k.to_string(), v))
210
    .collect();
211

            
212
    Ok(properties)
213
}
214

            
215
#[cfg(test)]
216
mod tests {
217
    use sp_core::ConstU32;
218

            
219
    use super::*;
220

            
221
    fn expected_container_chain_genesis_data() -> ContainerChainGenesisData {
222
        let para_id = 2000;
223

            
224
        ContainerChainGenesisData {
225
            storage: vec![(b"code".to_vec(), vec![1, 2, 3, 4, 5, 6]).into()],
226
            name: format!("Container Chain {}", para_id).into(),
227
            id: format!("container-chain-{}", para_id).into(),
228
            fork_id: None,
229
            extensions: vec![],
230
            properties: Default::default(),
231
        }
232
    }
233

            
234
    fn expected_string() -> &'static str {
235
        // TODO: this should be improved:
236
        // * name should be a string "Container Chain 2000"
237
        // * id should be a string
238
        // * token_symbol should be a string
239
        // * storage should be a map:
240
        //   "storage": { "0x636f6465": "0x010203040506" }
241
        r#"{
242
            "storage": [
243
              {
244
                "key": "0x636f6465",
245
                "value": "0x010203040506"
246
              }
247
            ],
248
            "name": "0x436f6e7461696e657220436861696e2032303030",
249
            "id": "0x636f6e7461696e65722d636861696e2d32303030",
250
            "fork_id": null,
251
            "extensions": "0x",
252
            "properties": {
253
              "token_metadata": {
254
                "token_symbol": [
255
                  85,
256
                  78,
257
                  73,
258
                  84
259
                ],
260
                "ss58_format": 42,
261
                "token_decimals": 12
262
              },
263
              "is_ethereum": false
264
            }
265
        }"#
266
    }
267

            
268
    #[test]
269
    fn test_serde_serialize() {
270
        let x = expected_container_chain_genesis_data();
271
        let xv = serde_json::to_value(x).unwrap();
272
        // Regenerate expected string using
273
        //println!("{}", serde_json::to_string_pretty(&x).unwrap());
274
        let expected = expected_string();
275
        let ev: serde_json::Value = serde_json::from_str(expected).unwrap();
276
        assert_eq!(xv, ev);
277
    }
278

            
279
    #[test]
280
    fn test_serde_deserialize() {
281
        let expected = expected_container_chain_genesis_data();
282
        let s = expected_string();
283
        let x: ContainerChainGenesisData = serde_json::from_str(s).unwrap();
284
        assert_eq!(x, expected);
285
    }
286
}