Skip to content

Commit ed366bf

Browse files
author
yxf
committed
basic implementations
1 parent 6aee646 commit ed366bf

File tree

9 files changed

+701
-1
lines changed

9 files changed

+701
-1
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@ Cargo.lock
88

99
# These are backup files generated by rustfmt
1010
**/*.rs.bk
11+
12+
13+
#Added by cargo
14+
15+
/target

Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "subwallet"
3+
version = "0.1.0"
4+
authors = ["yxf <[email protected]>"]
5+
edition = "2018"
6+
7+
8+
[dependencies]
9+
clap = "2.33.0"
10+
rustbreak = { version = "2.0.0-rc3", features=["bin_enc"] }
11+
serde = { version = "1.0", features = ["derive"] }
12+
tiny-bip39 = "0.7"
13+
serde_json = { version = "1.0" }
14+
rpassword = "4.0.1"
15+
hex = "0.4.0"
16+
rand = "0.7.2"
17+
dirs = "2.0"
18+
19+
sodalite = "0.3.0"
20+
tweetnacl = "0.2.1"
21+
schnorrkel = { version = "0.9.1", features = ["preaudit_deprecated", "u64_backend"], default-features = false }
22+
23+
# sp-core = { version = "2.0.0-alpha.8" }
24+
sp-core = { path = "../substrate/primitives/core" }
25+
26+
blake2-rfc = { version = "0.2.18" }

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
# subwallet
2-
A simple Command Line Interface Wallet for Polkadot/Substrate.
2+
3+
A simple Command Line Interface Wallet for Polkadot/Substrate.
4+
5+
<img src="https://raw.githubusercontent.com/w3f/Open-Grants-Program/master/src/web3%20foundation%20grants_black.jpg" width="300px">
6+

src/command.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use clap::{App, SubCommand, Arg};
2+
3+
pub fn get_app<'a, 'b>() -> App<'a, 'b> {
4+
App::new("subwallet")
5+
.author("yxf <[email protected]>")
6+
.about("A simple Command Line Interface wallet for Polkadot/Substrate.")
7+
.version(env!("CARGO_PKG_VERSION"))
8+
.subcommands(vec![
9+
SubCommand::with_name("getnewaddress")
10+
.about("Generate a new address. if label is specified, the address will be associated with label")
11+
.arg(Arg::with_name("label")
12+
.help("The label name for the address to be linked to.")
13+
.required(true)
14+
).args_from_usage("
15+
-e, --ed25519 'Use Ed25519/BIP39 cryptography'
16+
-k, --secp256k1 'Use SECP256k1/ECDSA/BIP39 cryptography'
17+
-s, --sr25519 'Use Schnorr/Ristretto x25519/BIP39 cryptography'
18+
"),
19+
SubCommand::with_name("listaddresses")
20+
.about("Return the list of addresses"),
21+
22+
SubCommand::with_name("restore")
23+
.about("Restore address from json file")
24+
.args_from_usage("
25+
<file> 'The filename with path'
26+
"),
27+
SubCommand::with_name("backup")
28+
.about("Backup specified address to local json file")
29+
.args_from_usage("
30+
<label> 'Address or label to backup'
31+
<path> 'The destination directory or file'
32+
"),
33+
])
34+
}

src/crypto.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
2+
pub use sp_core::{
3+
crypto::{set_default_ss58_version, Ss58AddressFormat, Ss58Codec, Derive },
4+
ed25519, sr25519, ecdsa, Pair, Public,
5+
};
6+
7+
use blake2_rfc::blake2b::{ Blake2b, Blake2bResult };
8+
9+
pub trait Crypto: Sized {
10+
type Pair: Pair<Public = Self::Public>;
11+
type Public: Public + Ss58Codec + AsRef<[u8]> + std::hash::Hash;
12+
13+
fn pair_from_secret_slice(slice: &[u8]) -> Result<Self::Pair, ()>;
14+
15+
fn crypto_type() -> &'static str;
16+
}
17+
18+
pub struct Ed25519;
19+
20+
impl Crypto for Ed25519 {
21+
type Pair = ed25519::Pair;
22+
type Public = ed25519::Public;
23+
24+
fn pair_from_secret_slice(slice: &[u8]) -> Result<Self::Pair, ()> {
25+
// if slice.len() != SEC_LENGTH {
26+
// return Err(())
27+
// }
28+
let mut secret_key = [0u8; 32];
29+
secret_key.copy_from_slice(&slice[..32]);
30+
Ok(Self::Pair::from_seed_slice(&secret_key[..]).unwrap())
31+
}
32+
33+
fn crypto_type() -> &'static str { "ed25519" }
34+
}
35+
36+
37+
pub struct Ecdsa;
38+
struct EcdsaPublic([u8; 32]);
39+
impl Derive for EcdsaPublic {}
40+
41+
impl Default for EcdsaPublic {
42+
fn default() -> Self {
43+
EcdsaPublic([0u8; 32])
44+
}
45+
}
46+
47+
impl AsRef<[u8]> for EcdsaPublic {
48+
fn as_ref(&self) -> &[u8] {
49+
&self.0[..]
50+
}
51+
}
52+
impl AsMut<[u8]> for EcdsaPublic {
53+
fn as_mut(&mut self) -> &mut [u8] {
54+
&mut self.0[..]
55+
}
56+
}
57+
58+
impl Crypto for Ecdsa {
59+
type Pair = ecdsa::Pair;
60+
type Public = ecdsa::Public;
61+
62+
fn pair_from_secret_slice(slice: &[u8]) -> Result<Self::Pair, ()> {
63+
let mut secret_key = [0u8; 32];
64+
secret_key.copy_from_slice(&slice[..32]);
65+
let pair = Self::Pair::from_seed_slice(&secret_key[..]).unwrap();
66+
Ok(pair)
67+
}
68+
fn crypto_type() -> &'static str { "ecdsa" }
69+
}
70+
71+
impl Ecdsa {
72+
// Return `https://github.com/polkadot-js` compatible address.
73+
pub fn to_js_ss58check<P: Pair>(pair: &P) -> String {
74+
let raw = pair.public().to_raw_vec();
75+
let hash = Self::prehash(&raw[..]);
76+
let mut raw = [0u8; 32];
77+
raw.copy_from_slice(&hash.as_bytes()[..]);
78+
let public = EcdsaPublic(raw);
79+
public.to_ss58check()
80+
}
81+
82+
fn prehash(data: &[u8]) -> Blake2bResult {
83+
let mut context = Blake2b::new(32);
84+
context.update(data);
85+
context.finalize()
86+
}
87+
}
88+
89+
90+
pub struct Sr25519;
91+
impl Crypto for Sr25519 {
92+
type Pair = sr25519::Pair;
93+
type Public = sr25519::Public;
94+
95+
fn pair_from_secret_slice(slice: &[u8]) -> Result<Self::Pair, ()> {
96+
Ok(Self::Pair::from_seed_slice(&slice[..]).unwrap())
97+
}
98+
99+
fn crypto_type() -> &'static str { "sr25519" }
100+
}
101+
102+

src/keystore.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
use std::fs;
2+
use serde::{Serialize, Deserialize};
3+
use serde_json::Value;
4+
use crate::crypto::*;
5+
use crate::pkcs8;
6+
7+
#[derive(Clone, Debug, Serialize, Deserialize)]
8+
pub struct Encoding {
9+
pub content: Vec<String>,
10+
pub r#type: String,
11+
pub version: String,
12+
}
13+
14+
#[derive(Clone, Debug, Serialize, Deserialize)]
15+
pub struct Keystore {
16+
pub address: String,
17+
pub encoded: String,
18+
pub encoding: Encoding,
19+
pub meta: Value,
20+
}
21+
22+
23+
impl Keystore {
24+
pub fn parse_from_file(path: String) -> Result<Self, ()> {
25+
let data = fs::read_to_string(path).map_err(|_| () )?;
26+
let keystore: Self = serde_json::from_str(&data).map_err( |_| () )?;
27+
Ok(keystore)
28+
}
29+
30+
pub fn crypto(&self) -> String {
31+
self.encoding.content[1].clone()
32+
}
33+
34+
pub fn label(&self) -> String {
35+
self.meta["name"].as_str().unwrap_or("").to_string()
36+
}
37+
38+
pub fn genesis_hash(&self) -> String {
39+
self.meta["genesisHash"].as_str().unwrap_or("").to_string()
40+
}
41+
42+
pub fn when_created(&self) -> u64 {
43+
self.meta["whenCreated"].as_u64().unwrap_or(0u64)
44+
}
45+
46+
pub fn to_json(&self) -> String {
47+
serde_json::to_string(&self).unwrap()
48+
}
49+
50+
pub fn encoded_bytes(&self) -> Vec<u8> {
51+
let encoded = if self.encoded.starts_with("0x") {
52+
&self.encoded[2..]
53+
} else {
54+
&self.encoded
55+
};
56+
hex::decode(encoded).unwrap_or(vec![])
57+
}
58+
59+
pub fn into_pair<T: Crypto>(&self, password: Option<String>) -> Result<T::Pair, ()> {
60+
let encoded = self.encoded_bytes();
61+
if encoded.is_empty() {
62+
return Err(())
63+
}
64+
match pkcs8::decode(&encoded[..], password) {
65+
Ok((_, secret_key)) => {
66+
T::pair_from_secret_slice(&secret_key[..])
67+
},
68+
Err(_) => Err(())
69+
}
70+
}
71+
}
72+
73+
74+
75+
76+

src/main.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
2+
mod keystore;
3+
mod crypto;
4+
mod command;
5+
mod wallet;
6+
mod pkcs8;
7+
8+
use sp_core::crypto::{Ss58AddressFormat, set_default_ss58_version};
9+
use std::path::Path;
10+
use std::fs;
11+
use keystore::Keystore;
12+
use crypto::*;
13+
use wallet::*;
14+
15+
fn main() {
16+
let mut app = command::get_app();
17+
let matches = app.clone().get_matches();
18+
set_default_ss58_version(Ss58AddressFormat::PolkadotAccount);
19+
let store = WalletStore::init(None);
20+
21+
match matches.subcommand() {
22+
("getnewaddress", Some(matches)) => {
23+
let label = matches.value_of("label").unwrap();
24+
let mut address = if matches.is_present("ed25519") {
25+
Address::generate::<Ed25519>()
26+
} else if matches.is_present("secp256k1") {
27+
Address::generate::<Ecdsa>()
28+
} else {
29+
Address::generate::<Sr25519>()
30+
};
31+
32+
address.label = label.to_string();
33+
store.save(address.clone());
34+
println!("{}", address.addr);
35+
}
36+
("listaddresses", Some(_)) => {
37+
let addresses = store.read_all();
38+
for address in addresses {
39+
address.print();
40+
}
41+
}
42+
("restore", Some(matches)) => {
43+
let file = matches.value_of("file").unwrap();
44+
45+
let keystore = match Keystore::parse_from_file(file.to_string()) {
46+
Ok(keystore) => keystore,
47+
Err(_) => {
48+
println!("Failed to parse keystore file");
49+
return
50+
}
51+
};
52+
53+
let password = rpassword::read_password_from_tty(Some("Password: ")).ok();
54+
if let Ok(address) = Address::from_keystore(keystore, password) {
55+
store.save(address.clone());
56+
println!("Address `{}` is restored", address.addr);
57+
} else {
58+
println!("Failed to recover address");
59+
return
60+
}
61+
}
62+
("backup", Some(matches)) => {
63+
let label = matches.value_of("label").unwrap();
64+
let file = matches.value_of("path").unwrap();
65+
66+
let address = match store.read(label) {
67+
Some(address) => address,
68+
None => {
69+
println!("`{}` related address does not exist.", label);
70+
return
71+
}
72+
};
73+
74+
let path = Path::new(file);
75+
let full_path = if path.ends_with("/") || path.is_dir() { // dir
76+
let file_name = format!("{}.json", address.addr.as_str());
77+
let mut path = path.to_path_buf();
78+
path.push(file_name);
79+
path
80+
} else { // file
81+
path.to_path_buf()
82+
};
83+
84+
if full_path.exists() {
85+
eprintln!("File `{}` aleady exists", full_path.to_str().unwrap());
86+
return
87+
}
88+
89+
let password = rpassword::read_password_from_tty(Some("Type password to encrypt seed: ")).ok();
90+
91+
let keystore = address.into_keystore(password);
92+
93+
if let Err(e) = fs::write(full_path.clone(), keystore.to_json()) {
94+
println!("Failed to write to file: {:?}", e);
95+
} else {
96+
println!("Address `{}` is backed up to file `{}`", address.addr, full_path.to_str().unwrap());
97+
}
98+
}
99+
_ => {
100+
app.print_help().unwrap();
101+
println!();
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)