Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • master
1 result

Target

Select target project
  • tuxmain/gcli
1 result
Select Git revision
  • license
  • master
2 results
Show changes
Commits on Source (13)
[alias]
rr = "run --release --"
\ No newline at end of file
This diff is collapsed.
...@@ -9,14 +9,14 @@ description = "A command line client written in Rust that use Duniter GVA API." ...@@ -9,14 +9,14 @@ description = "A command line client written in Rust that use Duniter GVA API."
[dependencies] [dependencies]
anyhow = "1.0.32" anyhow = "1.0.32"
graphql_client = "0.9.0" dubp-client = { git = "https://git.duniter.org/libs/dubp-rs-client-lib", branch = "master", features = ["blocking"], default-features = false }
reqwest = { version = "0.10.9", features = ["blocking", "json"] } #dubp-client= { path = "../dubp-rs-client-lib", features = ["blocking"], default-features = false }
read_input = "0.8.4"
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"
[dev-dependencies] [dev-dependencies]
dubp-client = { git = "https://git.duniter.org/libs/dubp-rs-client-lib", branch = "master", features = ["blocking", "mock"], default-features = false }
#dubp-client= { path = "../dubp-rs-client-lib", features = ["blocking", "mock"], default-features = false }
mockall = "0.8.0" mockall = "0.8.0"
[build-dependencies]
duniter-gva-gql = { git = "https://git.duniter.org/nodes/typescript/duniter", branch = "dev" }
#duniter-gva-gql = { path = "../duniter/rust-libs/modules/gva/gql" }
...@@ -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 :)
......
fn main() {
let schema_sdl = duniter_gva_gql::get_schema_definition();
std::fs::write("gql/gva_schema.gql", schema_sdl.as_bytes())
.expect("Fail to write gva schema in file");
println!("cargo:rerun-if-changed=build.rs");
}
...@@ -17,7 +17,7 @@ skip-tree = [ ...@@ -17,7 +17,7 @@ skip-tree = [
[licenses] [licenses]
unlicensed = "deny" unlicensed = "deny"
# We want really high confidence when inferring licenses from text # We want really high confidence when inferring licenses from text
confidence-threshold = 0.92 confidence-threshold = 0.8
allow = [ allow = [
"AGPL-3.0", "AGPL-3.0",
"Apache-2.0", "Apache-2.0",
......
query Balance($script: String!, $withUd: Boolean!) {
balance(script: $script) {
amount
}
currentUd @include(if: $withUd) {
amount
}
}
query CurrentUd {
currentUd {
amount
}
}
query MembersCount {
currentBlock {
membersCount
}
}
...@@ -15,10 +15,13 @@ ...@@ -15,10 +15,13 @@
pub mod balance; pub mod balance;
pub mod current_ud; pub mod current_ud;
pub mod idty;
pub mod members_count; pub mod members_count;
pub mod wallet;
use crate::*; use crate::*;
#[allow(clippy::large_enum_variant)]
#[derive(StructOpt)] #[derive(StructOpt)]
pub(crate) enum Command { pub(crate) enum Command {
/// Get account balance /// Get account balance
...@@ -29,6 +32,13 @@ pub(crate) enum Command { ...@@ -29,6 +32,13 @@ pub(crate) enum Command {
}, },
/// Get current UD value /// Get current UD value
CurrentUd, CurrentUd,
/// Get identity
Idty { pubkey: String },
/// 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,
},
} }
...@@ -15,36 +15,46 @@ ...@@ -15,36 +15,46 @@
use crate::*; use crate::*;
pub(crate) fn balance<W: Write>( pub(crate) fn balance<C: GvaClient, W: Write>(
client: &Client, gva_client: &C,
out: &mut W, out: &mut W,
pubkey_or_script: &str, pubkey_or_script: &str,
ud_unit: bool, ud_unit: bool,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let request_body = Balance::build_query(balance::Variables { let pubkey_or_script = PubkeyOrScript::from_str(pubkey_or_script)?;
script: pubkey_or_script.to_owned(),
with_ud: ud_unit,
});
let balance::ResponseData { let req_time = Instant::now();
balance: balance::BalanceBalance { amount }, if let Some(AccountBalance {
current_ud: current_ud_opt, amount,
} = client.send_gql_query(&request_body)?; ud_amount_opt,
}) = gva_client.account_balance(&pubkey_or_script, ud_unit)?
{
println!(
"The server responded in {} ms.",
req_time.elapsed().as_millis()
);
if let Some(balance::BalanceCurrentUd { amount: ud_amount }) = current_ud_opt { if let Some(ud_amount) = ud_amount_opt {
writeln!( writeln!(
out, out,
"The balance of account '{}' is {:.2} UDĞ1 !", "The balance of account '{}' is {:.2} UDĞ1 !",
pubkey_or_script, pubkey_or_script.to_string(),
amount as f64 / ud_amount as f64, amount.amount() as f64 / ud_amount.amount() as f64,
)?; )?;
} else { } else {
writeln!( writeln!(
out, out,
"The balance of account '{}' is {}.{:02} Ğ1 !", "The balance of account '{}' is {}.{:02} Ğ1 !",
pubkey_or_script, pubkey_or_script.to_string(),
amount / 100, amount.amount() / 100,
amount % 100 amount.amount() % 100
)?;
}
} else {
writeln!(
out,
"Account '{}' not exist !",
pubkey_or_script.to_string(),
)?; )?;
} }
Ok(()) Ok(())
...@@ -53,43 +63,43 @@ pub(crate) fn balance<W: Write>( ...@@ -53,43 +63,43 @@ pub(crate) fn balance<W: Write>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use dubp_client::wallet::prelude::SourceAmount;
#[test] #[test]
fn test_balance() -> anyhow::Result<()> { fn test_balance() -> anyhow::Result<()> {
let mut client = Client::default(); let mut client = MockGvaClient::default();
client client.expect_account_balance().returning(|_, _| {
.expect_send_gql_query::<graphql_client::QueryBody<balance::Variables>, _>() Ok(Some(AccountBalance {
.returning(|_| { amount: SourceAmount::new(2046, 0),
Ok(balance::ResponseData { ud_amount_opt: None,
balance: balance::BalanceBalance { amount: 2_046 }, }))
current_ud: None,
})
}); });
let mut out = Vec::new(); let mut out = Vec::new();
balance(&client, &mut out, "toto", false)?; balance(&client, &mut out, "toto", false)?;
let output = std::str::from_utf8(&out)?; let output = std::str::from_utf8(&out)?;
assert_eq!(output, "The balance of account 'toto' is 20.46 Ğ1 !\n"); assert_eq!(output, "The balance of account 'SIG(toto)' is 20.46 Ğ1 !\n");
Ok(()) Ok(())
} }
#[test] #[test]
fn test_balance_with_ud_unit() -> anyhow::Result<()> { fn test_balance_with_ud_unit() -> anyhow::Result<()> {
let mut client = Client::default(); let mut client = MockGvaClient::default();
client client.expect_account_balance().returning(|_, _| {
.expect_send_gql_query::<graphql_client::QueryBody<balance::Variables>, _>() Ok(Some(AccountBalance {
.returning(|_| { amount: SourceAmount::new(2_046, 0),
Ok(balance::ResponseData { ud_amount_opt: Some(SourceAmount::new(1_023, 0)),
balance: balance::BalanceBalance { amount: 2_046 }, }))
current_ud: Some(balance::BalanceCurrentUd { amount: 1_023 }),
})
}); });
let mut out = Vec::new(); let mut out = Vec::new();
balance(&client, &mut out, "toto", true)?; balance(&client, &mut out, "toto", true)?;
let output = std::str::from_utf8(&out)?; let output = std::str::from_utf8(&out)?;
assert_eq!(output, "The balance of account 'toto' is 2.00 UDĞ1 !\n"); assert_eq!(
output,
"The balance of account 'SIG(toto)' is 2.00 UDĞ1 !\n"
);
Ok(()) Ok(())
} }
......
...@@ -15,15 +15,19 @@ ...@@ -15,15 +15,19 @@
use crate::*; use crate::*;
pub(crate) fn current_ud<W: Write>(client: &Client, out: &mut W) -> anyhow::Result<()> { pub(crate) fn current_ud<C: GvaClient, W: Write>(
let request_body = CurrentUd::build_query(current_ud::Variables); gva_client: &C,
out: &mut W,
if let current_ud::ResponseData { ) -> anyhow::Result<()> {
current_ud: Some(current_ud::CurrentUdCurrentUd { amount }), let req_time = Instant::now();
} = client.send_gql_query(&request_body)? if let Some(current_ud) = gva_client.current_ud()? {
{ println!(
let int_part = amount / 100; "The server responded in {} ms.",
let dec_part = amount % 100; req_time.elapsed().as_millis()
);
let int_part = current_ud / 100;
let dec_part = current_ud % 100;
writeln!( writeln!(
out, out,
"The current UD value is {}.{:02} Ğ1 !", "The current UD value is {}.{:02} Ğ1 !",
...@@ -42,14 +46,8 @@ mod tests { ...@@ -42,14 +46,8 @@ mod tests {
#[test] #[test]
fn test_current_ud() -> anyhow::Result<()> { fn test_current_ud() -> anyhow::Result<()> {
let mut client = Client::default(); let mut client = MockGvaClient::default();
client client.expect_current_ud().returning(|| Ok(Some(1_023)));
.expect_send_gql_query::<graphql_client::QueryBody<current_ud::Variables>, _>()
.returning(|_| {
Ok(current_ud::ResponseData {
current_ud: Some(current_ud::CurrentUdCurrentUd { amount: 1_023 }),
})
});
let mut out = Vec::new(); let mut out = Vec::new();
current_ud(&client, &mut out)?; current_ud(&client, &mut out)?;
let output = std::str::from_utf8(&out)?; let output = std::str::from_utf8(&out)?;
......
// 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::*;
pub(crate) fn idty<C: GvaClient, W: Write>(
gva_client: &C,
out: &mut W,
pubkey: &str,
) -> anyhow::Result<()> {
let pubkey = PublicKey::from_base58(pubkey)?;
let req_time = Instant::now();
if let Some(Idty {
is_member,
username,
..
}) = gva_client.idty_by_pubkey(pubkey)?
{
println!(
"The server responded in {} ms.",
req_time.elapsed().as_millis()
);
writeln!(out, "Found identity for pubkey:")?;
writeln!(out, "username: {}", username)?;
writeln!(out, "is_member: {}", is_member)?;
} else {
writeln!(out, "No identity for pubkey {}", pubkey)?;
}
Ok(())
}
...@@ -15,12 +15,16 @@ ...@@ -15,12 +15,16 @@
use crate::*; use crate::*;
pub(crate) fn members_count<W: Write>(client: &Client, out: &mut W) -> anyhow::Result<()> { pub(crate) fn members_count<C: GvaClient, W: Write>(
let request_body = MembersCount::build_query(members_count::Variables); gva_client: &C,
out: &mut W,
let members_count::ResponseData { ) -> anyhow::Result<()> {
current_block: members_count::MembersCountCurrentBlock { members_count }, let req_time = Instant::now();
} = client.send_gql_query(&request_body)?; let members_count = gva_client.members_count()?;
println!(
"The server responded in {} ms.",
req_time.elapsed().as_millis()
);
writeln!( writeln!(
out, out,
...@@ -39,16 +43,8 @@ mod tests { ...@@ -39,16 +43,8 @@ mod tests {
#[test] #[test]
fn test_member_count() -> anyhow::Result<()> { fn test_member_count() -> anyhow::Result<()> {
let mut client = Client::default(); let mut client = MockGvaClient::default();
client client.expect_members_count().returning(|| Ok(10_000));
.expect_send_gql_query::<graphql_client::QueryBody<members_count::Variables>, _>()
.returning(|_| {
Ok(members_count::ResponseData {
current_block: members_count::MembersCountCurrentBlock {
members_count: 10_000,
},
})
});
let mut out = Vec::new(); let mut out = Vec::new();
members_count(&client, &mut out)?; members_count(&client, &mut out)?;
let output = std::str::from_utf8(&out)?; let output = std::str::from_utf8(&out)?;
......
// 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 dubp_client::{
crypto::{
bases::b58::ToBase58,
dewif::{
read_dewif_file_content, write_dewif_v4_content, Currency, ExpectedCurrency,
G1_CURRENCY,
},
keys::{
ed25519::bip32::{KeyPair, PrivateDerivationPath},
KeyPair as _, KeyPairEnum,
},
mnemonic::{mnemonic_to_seed, Language, Mnemonic, MnemonicType},
utils::U31,
},
wallet::prelude::SourceAmount,
};
#[allow(clippy::large_enum_variant)]
#[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
Pubkey {
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(short, long, default_value = "en")]
lang: Language,
#[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,
},
/// send money
Pay {
account_index: u32,
#[structopt(short, long)]
amount: f64,
#[structopt(short, long)]
base: Option<u64>,
#[structopt(short, long)]
comment: Option<String>,
#[structopt(long)]
dewif: Option<String>,
#[structopt(long, parse(from_os_str))]
dewif_file: Option<PathBuf>,
#[structopt(short, long, default_value = "en")]
lang: Language,
#[structopt(long)]
password_stdin: bool,
#[structopt(short, long)]
recipient: PkOrScript,
/// UD units
#[structopt(short, long)]
ud_units: bool,
/// Auto confirm (for non-interactive use)
#[structopt(short, long)]
yes: bool,
},
}
pub(crate) fn wallet<C: GvaClient, W: Write>(
gva_client: &C,
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::Pubkey {
account_index,
address_index,
dewif,
dewif_file,
external,
lang,
password_stdin,
} => {
let keypair = get_master_keypair(dewif, dewif_file, lang, password_stdin)?;
let derivation_path = match account_index % 3 {
0 => PrivateDerivationPath::transparent(U31::new(account_index)?)?,
1 => todo!(),
2 => PrivateDerivationPath::opaque(
U31::new(account_index)?,
external,
if let Some(address_index) = address_index {
Some(U31::new(address_index)?)
} else {
None
},
)?,
_ => unreachable!(),
};
let derived_keypair = keypair.derive(derivation_path);
writeln!(out, "{}", derived_keypair.public_key().to_base58())?;
Ok(())
}
WalletCommand::Pay {
account_index,
amount,
base,
comment,
dewif,
dewif_file,
lang,
password_stdin,
recipient,
ud_units,
yes,
} => {
let recipient = recipient.0;
let keypair = get_master_keypair(dewif, dewif_file, lang, password_stdin)?;
let (amount, amount_str) = if ud_units {
(Amount::Uds(amount), format!("{:.2} UDğ1", amount))
} else {
(
Amount::Cents(SourceAmount::new(
(amount * 100.0).round() as i64,
base.unwrap_or_default() as i64,
)),
format!("{:.2} Ğ1", amount),
)
};
match account_index % 3 {
0 => {
let trasparent_keypair = keypair.derive(PrivateDerivationPath::transparent(
U31::new(account_index)?,
)?);
let confirm = if yes {
true
} else {
let res = input()
.msg(format!(
"Send {} to {} from transparent account {}? [y/N]",
amount_str,
recipient.to_string(),
trasparent_keypair.public_key().to_base58()
))
.default('N')
.get();
res == 'y' || res == 'Y'
};
if confirm {
let req_time = Instant::now();
let payment_result = gva_client.simple_payment(
amount,
&trasparent_keypair.generate_signator(),
recipient,
comment,
None,
)?;
if let PaymentResult::Errors(errors) = payment_result {
writeln!(out, "All or part of the payment failed, errors: \n")?;
for error in errors {
writeln!(out, "- {:?}", error)?;
}
todo!()
} else {
let duration = req_time.elapsed();
writeln!(
out,
"Payment succesfully processed in {}.{} ms.",
duration.as_millis(),
duration.subsec_micros() % 1_000
)?;
}
}
Ok(())
}
1 => todo!(),
2 => todo!(),
_ => unreachable!(),
}
}
}
}
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_master_keypair(
dewif_opt: Option<String>,
dewif_file_opt: Option<PathBuf>,
lang: Language,
password_stdin: bool,
) -> anyhow::Result<KeyPair> {
if dewif_opt.is_some() || dewif_file_opt.is_some() {
let dewif_content = get_dewif_content(dewif_opt, dewif_file_opt)?;
get_master_keypair_from_dewif(dewif_content, password_stdin)
} else {
let mnemonic_phrase = rpassword::prompt_password_stdout("Mnemonic: ")?;
let mnemonic = Mnemonic::from_phrase(mnemonic_phrase, lang)?;
Ok(KeyPair::from_seed(mnemonic_to_seed(&mnemonic)))
}
}
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_master_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(KeyPairEnum::Bip32Ed25519(keypair)) = keypairs.next() {
Ok(keypair)
} else {
Err(anyhow::Error::msg("DEWIF corrupted"))
}
}
...@@ -14,50 +14,21 @@ ...@@ -14,50 +14,21 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::*; use crate::*;
use dubp_client::{
crypto::keys::PublicKey as _,
documents_parser::{wallet_script_from_str, TextParseError},
};
#[cfg_attr(test, allow(dead_code))] pub struct PkOrScript(pub(crate) WalletScriptV10);
pub(crate) struct Client {
inner: reqwest::blocking::Client,
server_url: String,
}
#[cfg_attr(test, mockall::automock, allow(dead_code))]
impl Client {
pub(crate) fn new(server_url: String) -> Self {
Client {
inner: reqwest::blocking::Client::new(),
server_url,
}
}
pub(crate) fn send_gql_query<
Req: 'static + serde::Serialize,
ResData: 'static + serde::de::DeserializeOwned,
>(
&self,
request_body: &Req,
) -> anyhow::Result<ResData> {
let request = self.inner.post(&self.server_url).json(request_body);
let start_time = Instant::now(); impl FromStr for PkOrScript {
let response = request.send()?; type Err = TextParseError;
let req_duration = Instant::now().duration_since(start_time);
println!("The server responded in {} ms.", req_duration.as_millis());
let mut gql_response: Response<ResData> = response.json()?; fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(errors) = gql_response.errors.take() { if let Ok(pubkey) = PublicKey::from_base58(s) {
print_server_errors(errors); Ok(Self(WalletScriptV10::single_sig(pubkey)))
Err(anyhow::Error::msg(""))
} else if let Some(data) = gql_response.data {
Ok(data)
} else { } else {
Err(anyhow::Error::msg("server response contains no data")) Ok(Self(wallet_script_from_str(s)?))
} }
} }
} }
fn print_server_errors(errors: Vec<graphql_client::Error>) {
println!("Server errors:");
for error in errors {
println!("{}", error);
}
}
...@@ -24,35 +24,35 @@ ...@@ -24,35 +24,35 @@
unused_import_braces unused_import_braces
)] )]
mod client;
mod commands; mod commands;
mod inputs;
#[cfg(not(test))]
use crate::client::Client;
#[cfg(test)]
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 crate::inputs::PkOrScript;
use graphql_client::GraphQLQuery; use commands::{
use graphql_client::Response; balance::balance, current_ud::current_ud, idty::idty, members_count::members_count,
use std::io::Write; wallet::wallet,
use std::time::Instant; };
#[cfg(test)]
use dubp_client::MockGvaClient;
use dubp_client::{
crypto::keys::{ed25519::PublicKey, PublicKey as _},
wallet::prelude::*,
AccountBalance, Amount, GvaClient, Idty, NaiveGvaClient, PaymentResult, PubkeyOrScript,
};
use read_input::prelude::*;
use std::{
env::var_os,
fs::File,
io::{BufReader, Read, Write},
path::PathBuf,
str::FromStr,
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";
#[derive(Debug, Clone, Copy, GraphQLQuery)]
#[graphql(schema_path = "gql/gva_schema.gql", query_path = "gql/gva_queries.gql")]
pub struct Balance;
#[derive(Debug, Clone, Copy, GraphQLQuery)]
#[graphql(schema_path = "gql/gva_schema.gql", query_path = "gql/gva_queries.gql")]
pub struct CurrentUd;
#[derive(Debug, Clone, Copy, GraphQLQuery)]
#[graphql(schema_path = "gql/gva_schema.gql", query_path = "gql/gva_queries.gql")]
pub struct MembersCount;
#[derive(StructOpt)] #[derive(StructOpt)]
#[structopt(name = "rust-gva-client", about = "Client use GVA API of Duniter.")] #[structopt(name = "rust-gva-client", about = "Client use GVA API of Duniter.")]
struct CliArgs { struct CliArgs {
...@@ -66,16 +66,18 @@ struct CliArgs { ...@@ -66,16 +66,18 @@ struct CliArgs {
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
let cli_args = CliArgs::from_args(); let cli_args = CliArgs::from_args();
let client = Client::new(cli_args.server); let gva_client = NaiveGvaClient::new(&cli_args.server)?;
let mut out = std::io::stdout(); let mut out = std::io::stdout();
match cli_args.command { match cli_args.command {
Command::Balance { Command::Balance {
pubkey_or_script, pubkey_or_script,
ud_unit, ud_unit,
} => balance(&client, &mut out, &pubkey_or_script, ud_unit)?, } => balance(&gva_client, &mut out, &pubkey_or_script, ud_unit)?,
Command::CurrentUd => current_ud(&client, &mut out)?, Command::CurrentUd => current_ud(&gva_client, &mut out)?,
Command::MembersCount => members_count(&client, &mut out)?, Command::Idty { pubkey } => idty(&gva_client, &mut out, &pubkey)?,
Command::MembersCount => members_count(&gva_client, &mut out)?,
Command::Wallet { command } => wallet(&gva_client, &mut out, command)?,
} }
Ok(()) Ok(())
} }