Skip to content
Snippets Groups Projects
Commit b7a2fb66 authored by Éloïs's avatar Éloïs
Browse files

feat: add wallet command

parent 07bacf59
No related branches found
No related tags found
No related merge requests found
Pipeline #10966 failed
...@@ -25,6 +25,38 @@ version = "0.2.3" ...@@ -25,6 +25,38 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
[[package]]
name = "aes"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7001367fde4c768a19d1029f0a8be5abd9308e1119846d5bd9ad26297b8faf5"
dependencies = [
"aes-soft",
"aesni",
"block-cipher",
]
[[package]]
name = "aes-soft"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4925647ee64e5056cf231608957ce7c81e12d6d6e316b9ce1404778cc1d35fa7"
dependencies = [
"block-cipher",
"byteorder",
"opaque-debug 0.2.3",
]
[[package]]
name = "aesni"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d050d39b0b7688b3a3254394c3e30a9d66c41dcf9b05b0e2dbdc623f6505d264"
dependencies = [
"block-cipher",
"opaque-debug 0.2.3",
]
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.4.7" version = "0.4.7"
...@@ -330,6 +362,15 @@ dependencies = [ ...@@ -330,6 +362,15 @@ dependencies = [
"generic-array 0.14.4", "generic-array 0.14.4",
] ]
[[package]]
name = "block-cipher"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa136449e765dc7faa244561ccae839c394048667929af599b5d931ebe7b7f10"
dependencies = [
"generic-array 0.14.4",
]
[[package]] [[package]]
name = "block-padding" name = "block-padding"
version = "0.1.5" version = "0.1.5"
...@@ -574,6 +615,12 @@ dependencies = [ ...@@ -574,6 +615,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "cryptoxide"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46212f5d1792f89c3e866fb10636139464060110c568edd7f73ab5e9f736c26d"
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.10.2" version = "0.10.2"
...@@ -886,11 +933,16 @@ version = "0.43.2" ...@@ -886,11 +933,16 @@ version = "0.43.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5612539417a1204863d10dda1cb313fe7a34da8596d30fa93da8af125c928abb" checksum = "5612539417a1204863d10dda1cb313fe7a34da8596d30fa93da8af125c928abb"
dependencies = [ dependencies = [
"aes",
"arrayvec",
"base64 0.12.3", "base64 0.12.3",
"blake3", "blake3",
"bs58", "bs58",
"byteorder", "byteorder",
"cryptoxide",
"ed25519-bip32",
"getrandom 0.1.15", "getrandom 0.1.15",
"once_cell",
"ring", "ring",
"serde", "serde",
"thiserror", "thiserror",
...@@ -898,6 +950,15 @@ dependencies = [ ...@@ -898,6 +950,15 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "ed25519-bip32"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8827180a2b511141fbe49141e50b31a8d542465e0fb572f81f36feea2addfe92"
dependencies = [
"cryptoxide",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.6.1" version = "1.6.1"
...@@ -1182,9 +1243,11 @@ version = "0.1.0" ...@@ -1182,9 +1243,11 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"duniter-gva-gql", "duniter-gva-gql",
"dup-crypto",
"graphql_client", "graphql_client",
"mockall", "mockall",
"reqwest", "reqwest",
"rpassword",
"serde", "serde",
"structopt", "structopt",
] ]
...@@ -2245,6 +2308,16 @@ dependencies = [ ...@@ -2245,6 +2308,16 @@ dependencies = [
"winapi 0.3.9", "winapi 0.3.9",
] ]
[[package]]
name = "rpassword"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb"
dependencies = [
"libc",
"winapi 0.3.9",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.18" version = "0.1.18"
......
...@@ -9,8 +9,10 @@ description = "A command line client written in Rust that use Duniter GVA API." ...@@ -9,8 +9,10 @@ description = "A command line client written in Rust that use Duniter GVA API."
[dependencies] [dependencies]
anyhow = "1.0.32" anyhow = "1.0.32"
dup-crypto = { version = "0.43.2", features = [ "bip32-ed25519", "dewif", "mnemonic"] }
graphql_client = "0.9.0" graphql_client = "0.9.0"
reqwest = { version = "0.10.9", features = ["blocking", "json"] } reqwest = { version = "0.10.9", features = ["blocking", "json"] }
rpassword = "5.0.1"
serde = { version = "1.0.105", features = ["derive"] } serde = { version = "1.0.105", features = ["derive"] }
structopt = "0.3.18" structopt = "0.3.18"
......
...@@ -2,6 +2,89 @@ ...@@ -2,6 +2,89 @@
A simple command line client written in Rust that use Duniter GVA API. A simple command line client written in Rust that use Duniter GVA API.
## Use
### Generate new wallet
Use command `wallet gen`:
```bash
gcli wallet gen
Mnemonic: sibling pelican sense stereo plastic helmet book expand tube whale census multiply
Password:
DEWIF: AAAABAAAAAEMGzFNV+j80uJkBuAfazfFWZfz0ARlA5T/Ea3ZQTo7zjTiALR/jkf/pcBLFlsqtERCLozLcRP8Q74qj1NSFZ79Fw==
```
You can generate a mnemonic only with option `--mnemonic-only`:
```bash
gcli wallet gen --mnemonic-only
sibling pelican sense stereo plastic helmet book expand tube whale census multiply
```
You can choose another language, change mnemonic length, etc. See `wallet gen --help` for details.
#### Non-Interactive
You can choose a password in a non-interactive way with option `--password-stdin`:
```bash
echo "1234" | gcli wallet gen --password-stdin
Mnemonic: sibling pelican sense stereo plastic helmet book expand tube whale census multiply
DEWIF: AAAABAAAAAEMGzFNV+j80uJkBuAfazfFWZfz0ARlA5T/Ea3ZQTo7zjTiALR/jkf/pcBLFlsqtERCLozLcRP8Q74qj1NSFZ79Fw==
```
### Import a wallet form mnemonic
```bash
gcli wallet import
Mnemonic: sibling pelican sense stereo plastic helmet book expand tube whale census multiply
Password:
DEWIF: AAAABAAAAAEMGzFNV+j80uJkBuAfazfFWZfz0ARlA5T/Ea3ZQTo7zjTiALR/jkf/pcBLFlsqtERCLozLcRP8Q74qj1NSFZ79Fw==
```
You can choose another language, and other parameters. See `wallet import --help` for details.
#### Non-Interactive
You can choose a mnemonic and password in a non-interactive way with environment variable `MNEMONIC` and option `--password-stdin`:
```bash
echo "1234" | MNEMONIC="sibling pelican sense stereo plastic helmet book expand tube whale census multiply" gcli wallet import --password-stdin
DEWIF: AAAABAAAAAEMGzFNV+j80uJkBuAfazfFWZfz0ARlA5T/Ea3ZQTo7zjTiALR/jkf/pcBLFlsqtERCLozLcRP8Q74qj1NSFZ79Fw==
```
### Get a public key (=address)
To obtain the public key of the sub-account whose derivation number is `3`:
```bash
gcli wallet get-pubkey --dewif AAAABAAAAAEMGzFNV+j80uJkBuAfazfFWZfz0ARlA5T/Ea3ZQTo7zjTiALR/jkf/pcBLFlsqtERCLozLcRP8Q74qj1NSFZ79Fw== 3
Password:
2S7wAUzaXfG3Z1SWNULqBotnUb9QbucmeuVkqVL4X8FB
```
To obtain the master external* public key of the sub-account whose derivation number is `3`:
```bash
gcli wallet get-pubkey --external --dewif AAAABAAAAAEMGzFNV+j80uJkBuAfazfFWZfz0ARlA5T/Ea3ZQTo7zjTiALR/jkf/pcBLFlsqtERCLozLcRP8Q74qj1NSFZ79Fw== 3
Password:
9KV3gJFcsEH98FQhF3vPW4YG15VGRybHZuzAeD3oVicx
```
* The master external public key allows you to generate all payment receipt addresses by public key derivation. This is the key to be configured on your server to generate the payment addresses that will be provided to your customers.
You can also obtain an external or internal address in particular by its derivation index, see `wallet get-pubkey --help` for details.
#### Non-Interactive
You can enter your password in a non-interactive way with option `--password-stdin`:
```bash
echo "1234" | gcli wallet get-pubkey --dewif AAAABAAAAAEMGzFNV+j80uJkBuAfazfFWZfz0ARlA5T/Ea3ZQTo7zjTiALR/jkf/pcBLFlsqtERCLozLcRP8Q74qj1NSFZ79Fw== 3
2S7wAUzaXfG3Z1SWNULqBotnUb9QbucmeuVkqVL4X8FB
```
## Contribute ## Contribute
Contributions are welcome :) Contributions are welcome :)
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
pub mod balance; pub mod balance;
pub mod current_ud; pub mod current_ud;
pub mod members_count; pub mod members_count;
pub mod wallet;
use crate::*; use crate::*;
...@@ -31,4 +32,9 @@ pub(crate) enum Command { ...@@ -31,4 +32,9 @@ pub(crate) enum Command {
CurrentUd, CurrentUd,
/// Get current number of WoT members /// Get current number of WoT members
MembersCount, MembersCount,
/// Create or update a wallet
Wallet {
#[structopt(subcommand)]
command: wallet::WalletCommand,
},
} }
// Copyright (C) 2020 Éloïs SANCHEZ.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::*;
use dup_crypto::keys::{ed25519::bip32::KeyPair, KeyPair as _};
use dup_crypto::mnemonic::{mnemonic_to_seed, Language, Mnemonic, MnemonicType};
use dup_crypto::{
bases::b58::ToBase58,
dewif::{
read_dewif_file_content, write_dewif_v4_content, Currency, ExpectedCurrency, G1_CURRENCY,
},
keys::ed25519::bip32::DerivationIndex,
};
#[derive(StructOpt)]
pub enum WalletCommand {
/// Generate a wallet
Gen {
#[structopt(short, long, default_value = "en")]
lang: Language,
#[structopt(long, default_value = "12")]
log_n: u8,
#[structopt(short, long)]
mnemonic_only: bool,
#[structopt(long)]
password_stdin: bool,
/// Mnemonic word count
#[structopt(short, long, parse(try_from_str = parse_mnemonic_len), default_value = "12")]
word_count: MnemonicType,
},
/// Get a public key
GetPubkey {
account_index: u32,
address_index: Option<u32>,
#[structopt(long)]
dewif: Option<String>,
#[structopt(long, parse(from_os_str))]
dewif_file: Option<PathBuf>,
#[structopt(short, long)]
external: bool,
#[structopt(long)]
password_stdin: bool,
},
/// Import a wallet from mnemonic
Import {
#[structopt(short, long, default_value = "en")]
lang: Language,
#[structopt(long, default_value = "12")]
log_n: u8,
#[structopt(long)]
password_stdin: bool,
},
}
pub(crate) fn wallet<W: Write>(out: &mut W, command: WalletCommand) -> anyhow::Result<()> {
match command {
WalletCommand::Gen {
lang,
mnemonic_only,
log_n,
password_stdin,
word_count,
} => {
let mnemonic = Mnemonic::new(word_count, lang)
.map_err(|_| anyhow::Error::msg("unspecified rand error"))?;
if mnemonic_only {
writeln!(out, "{}", mnemonic.phrase())?;
} else {
writeln!(out, "Mnemonic: {}", mnemonic.phrase())?;
gen_dewif(out, log_n, mnemonic, password_stdin)?;
}
Ok(())
}
WalletCommand::Import {
lang,
log_n,
password_stdin,
} => {
let mnemonic_phrase = if let Some(mnemonic_env) = var_os("MNEMONIC") {
mnemonic_env
.into_string()
.map_err(|_| anyhow::Error::msg("invalid utf8 string"))?
} else {
rpassword::prompt_password_stdout("Mnemonic: ")?
};
let mnemonic = Mnemonic::from_phrase(mnemonic_phrase, lang)?;
gen_dewif(out, log_n, mnemonic, password_stdin)?;
Ok(())
}
WalletCommand::GetPubkey {
account_index,
address_index,
dewif,
dewif_file,
external,
password_stdin,
} => {
let dewif_content = get_dewif_content(dewif, dewif_file)?;
let keypair = get_keypair_from_dewif(dewif_content, password_stdin)?;
let account_keypair = keypair.derive(DerivationIndex::hard(account_index)?);
if external {
let external_keypair = account_keypair.derive(DerivationIndex::hard(0)?);
if let Some(address_index) = address_index {
let address_keypair =
external_keypair.derive(DerivationIndex::soft(address_index)?);
writeln!(out, "{}", address_keypair.public_key().to_base58())?;
} else {
writeln!(out, "{}", external_keypair.public_key().to_base58())?;
}
} else if let Some(address_index) = address_index {
let internal_keypair = account_keypair.derive(DerivationIndex::hard(1)?);
let address_keypair =
internal_keypair.derive(DerivationIndex::soft(address_index)?);
writeln!(out, "{}", address_keypair.public_key().to_base58())?;
} else {
writeln!(out, "{}", account_keypair.public_key().to_base58())?;
}
Ok(())
}
}
}
fn parse_mnemonic_len(s: &str) -> anyhow::Result<MnemonicType> {
Ok(MnemonicType::for_word_count(s.parse()?)?)
}
fn gen_dewif<W: Write>(
out: &mut W,
log_n: u8,
mnemonic: Mnemonic,
password_stdin: bool,
) -> anyhow::Result<()> {
let seed = mnemonic_to_seed(&mnemonic);
let keypair = KeyPair::from_seed(seed.clone());
let password = if password_stdin {
rpassword::read_password()?
} else {
rpassword::prompt_password_stdout("Password: ")?
};
//println!("TMP DEBUG: password={}", password);
let dewif = write_dewif_v4_content(
Currency::from(G1_CURRENCY),
log_n,
&password,
&keypair.public_key(),
seed,
);
writeln!(out, "DEWIF: {}", dewif)?;
Ok(())
}
fn get_dewif_content(
dewif_opt: Option<String>,
dewif_file_opt: Option<PathBuf>,
) -> anyhow::Result<String> {
if let Some(dewif_env) = var_os("DEWIF") {
dewif_env
.into_string()
.map_err(|_| anyhow::Error::msg("invalid utf8 string"))
} else if let Some(dewif_file_env) = var_os("DEWIF_FILE") {
let mut buf_reader = BufReader::new(File::open(dewif_file_env)?);
let mut contents = String::new();
buf_reader.read_to_string(&mut contents)?;
Ok(contents)
} else if let Some(dewif) = dewif_opt {
Ok(dewif)
} else if let Some(dewif_file) = dewif_file_opt {
let mut buf_reader = BufReader::new(File::open(dewif_file)?);
let mut contents = String::new();
buf_reader.read_to_string(&mut contents)?;
Ok(contents)
} else {
Err(anyhow::Error::msg("no DEWIF provided"))
}
}
fn get_keypair_from_dewif(dewif_content: String, password_stdin: bool) -> anyhow::Result<KeyPair> {
let password = if password_stdin {
rpassword::read_password()?
} else {
rpassword::prompt_password_stdout("Password: ")?
};
let mut keypairs = read_dewif_file_content(
ExpectedCurrency::Specific(Currency::from(G1_CURRENCY)),
&dewif_content,
&password,
)?;
if let Some(dup_crypto::keys::KeyPairEnum::Bip32Ed25519(keypair)) = keypairs.next() {
Ok(keypair)
} else {
Err(anyhow::Error::msg("DEWIF corrupted"))
}
}
...@@ -32,11 +32,18 @@ use crate::client::Client; ...@@ -32,11 +32,18 @@ use crate::client::Client;
#[cfg(test)] #[cfg(test)]
use crate::client::MockClient as Client; use crate::client::MockClient as Client;
use crate::commands::Command; use crate::commands::Command;
use commands::{balance::balance, current_ud::current_ud, members_count::members_count}; use commands::{
balance::balance, current_ud::current_ud, members_count::members_count, wallet::wallet,
};
use graphql_client::GraphQLQuery; use graphql_client::GraphQLQuery;
use graphql_client::Response; use graphql_client::Response;
use std::io::Write; use std::{
use std::time::Instant; env::var_os,
fs::File,
io::{BufReader, Read, Write},
path::PathBuf,
time::Instant,
};
use structopt::StructOpt; use structopt::StructOpt;
const DEFAULT_GVA_SERVER: &str = "https://g1.librelois.fr/gva"; const DEFAULT_GVA_SERVER: &str = "https://g1.librelois.fr/gva";
...@@ -76,6 +83,7 @@ fn main() -> anyhow::Result<()> { ...@@ -76,6 +83,7 @@ fn main() -> anyhow::Result<()> {
} => balance(&client, &mut out, &pubkey_or_script, ud_unit)?, } => balance(&client, &mut out, &pubkey_or_script, ud_unit)?,
Command::CurrentUd => current_ud(&client, &mut out)?, Command::CurrentUd => current_ud(&client, &mut out)?,
Command::MembersCount => members_count(&client, &mut out)?, Command::MembersCount => members_count(&client, &mut out)?,
Command::Wallet { command } => wallet(&mut out, command)?,
} }
Ok(()) Ok(())
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment