use std::collections::{HashMap, HashSet};
use std::io::Write;
use std::path::Path;
use itertools::Itertools;
use reqwest;
const SLIP_0044_MARKDOWN_URL: &str =
"https://raw.githubusercontent.com/satoshilabs/slips/master/slip-0044.md";
const SLIP_044_MARKDOWN_HEADER: &str = "Coin type | Path component (`coin_type'`) | Symbol | Coin";
#[derive(Debug)]
struct CoinType {
id: u32,
ids: Vec<u32>,
path_component: String,
symbol: Option<String>,
name: String,
original_name: String,
link: Option<String>,
rustdoc_lines: Vec<String>,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let coin_types = reqwest::blocking::get(SLIP_0044_MARKDOWN_URL)?
.text()?
.split("\n")
.skip_while(|&line| line != SLIP_044_MARKDOWN_HEADER)
.skip(2)
.filter_map(|line| {
let columns = line.split('|').collect::<Vec<_>>();
if columns.len() != 4 {
return None;
}
let (original_name, link) = parse_markdown_link(columns[3].trim());
if original_name.is_empty() || original_name == "reserved" {
return None;
}
let name = original_name_to_short(original_name).unwrap();
Some(CoinType {
id: columns[0].trim().parse().ok()?,
ids: vec![],
path_component: columns[1].trim().to_string(),
symbol: Some(columns[2].trim())
.map(prepend_enum)
.map(|symbol| match symbol.as_str() {
"$DAG" => "DAG".to_string(),
symbol => symbol.to_string(),
})
.filter(|symbol| !symbol.is_empty()),
name: name.to_string(),
original_name: original_name.to_string(),
link: link.map(ToString::to_string),
rustdoc_lines: vec![],
})
})
.fold(HashMap::<_, CoinType>::new(), |mut acc, coin_type| {
let id = coin_type.id.clone();
acc.entry((
coin_type.symbol.clone(),
coin_type.name.clone(),
coin_type.original_name.clone(),
))
.or_insert(coin_type)
.ids
.push(id);
acc
})
.into_iter()
.fold(HashMap::<_, Vec<_>>::new(), |mut acc, (_, coin_type)| {
acc.entry(coin_type.name.clone())
.or_default()
.push(coin_type);
acc
})
.into_iter()
.map(|(_, coin_types)| {
let coin_types = if coin_types.len() > 1 {
coin_types
.into_iter()
.map(|coin_type| CoinType {
name: format!(
"{}_{}",
coin_type.name.clone(),
match coin_type.symbol.clone() {
Some(symbol) => symbol,
None => coin_type.ids.clone().into_iter().join("_").to_string(),
}
),
..coin_type
})
.collect()
} else {
coin_types
};
coin_types
.into_iter()
.map(|coin_type| CoinType {
rustdoc_lines: vec![
format!("/// Coin type: {}", coin_type.ids.iter().join(", ")),
if let Some(symbol) = coin_type.symbol.clone() {
format!("/// Symbol: {}", symbol)
} else {
"".to_string()
},
format!("/// Coin: {}", coin_type.original_name),
],
..coin_type
})
.collect::<Vec<_>>()
})
.flatten();
let mut file = std::fs::File::create(
Path::new(file!())
.parent()
.ok_or("can't get first parent")?
.parent()
.ok_or("can't get second parent")?
.join("coin.rs"),
)?;
writeln!(&mut file, "// Code generated by {}; DO NOT EDIT.", file!())?;
writeln!(&mut file, "use crate::coins;")?;
writeln!(&mut file, "coins!(")?;
let mut seen_symbols = HashSet::<String>::new();
for coin_type in coin_types.sorted_by_key(|coin_type| coin_type.id) {
writeln!(
&mut file,
" (
{}
[{}], {}, \"{}\", {}, {}, {},
),",
coin_type
.rustdoc_lines
.into_iter()
.filter(|s| !s.is_empty())
.join("\n ///\n "),
coin_type.ids.into_iter().join(",").to_string(),
coin_type.name,
coin_type.original_name,
match coin_type.link {
Some(ref link) => format!("\"{}\"", link),
None => "".to_string(),
},
match coin_type.symbol {
Some(ref symbol) =>
if seen_symbols.contains(symbol) {
""
} else {
symbol
},
None => "",
}
.to_string(),
match coin_type.symbol.clone() {
Some(ref symbol) =>
if seen_symbols.contains(symbol) {
format!("\"{}\"", symbol)
} else {
seen_symbols.insert(symbol.clone());
"".to_string()
},
None => "".to_string(),
}
.to_string(),
)?;
}
writeln!(&mut file, ");")?;
Ok(())
}
fn parse_markdown_link(input: &str) -> (&str, Option<&str>) {
if input.starts_with('[') {
(
input.splitn(3, &['[', ']'][..]).nth(1).unwrap_or(input),
input
.trim_start_matches(']')
.splitn(3, &['(', ')'][..])
.nth(1),
)
} else {
(input, None)
}
}
fn original_name_to_short(original_name: &str) -> Result<String, String> {
let mut name = original_name.replace(' ', "");
name = name
.split_once('(')
.map_or(name.to_string(), |(name, _)| name.to_string());
name = prepend_enum(&name);
if name.contains(|ch: char| !ch.is_ascii_alphanumeric() && ch != '_') {
let name_match = match name.as_str() {
"Pl^g" => Ok("Plug"),
"BitcoinMatteo'sVision" => Ok("BitcoinMatteosVision"),
"Crypto.orgChain" => Ok("CryptoOrgChain"),
"Cocos-BCX" => Ok("CocosBCX"),
"Capricoin+" => Ok("CapricoinPlus"),
"Seele-N" => Ok("SeeleN"),
"IQ-Cash" => Ok("IQCash"),
"XinFin.Network" => Ok("XinFinNetwork"),
"Unit-e" => Ok("UnitE"),
"HARMONY-ONE" => Ok("HarmonyOne"),
"ThePower.io" => Ok("ThePower"),
"evan.network" => Ok("EvanNetwork"),
"Ether-1" => Ok("EtherOne"),
"æternity" => Ok("aeternity"),
"θ" => Ok("Theta"),
name => Err(format!("unknown original coin name `{}`", name)),
};
name_match.map(|name| name.to_string())
} else {
Ok(name)
}
}
fn prepend_enum(name: &str) -> String {
if name.starts_with(char::is_numeric) {
["_", name].join("")
} else {
name.to_string()
}
}