Skip to content

Commit

Permalink
feat(gcli): introduce command config (#3909)
Browse files Browse the repository at this point in the history
  • Loading branch information
clearloop authored Apr 26, 2024
1 parent 184ebb3 commit f103875
Show file tree
Hide file tree
Showing 11 changed files with 241 additions and 504 deletions.
8 changes: 3 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 2 additions & 5 deletions gcli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@ gsdk.workspace = true
gclient.workspace = true
anyhow.workspace = true
async-trait.workspace = true
base64.workspace = true
colored.workspace = true
color-eyre.workspace = true
dirs.workspace = true
env_logger.workspace = true
futures.workspace = true
gmeta.workspace = true
gear-core.workspace = true
gear-core-errors.workspace = true
Expand All @@ -31,18 +29,17 @@ jsonrpsee = { workspace = true, features = [ "http-client", "ws-client" ] }
keyring.workspace = true
lazy_static.workspace = true
log.workspace = true
nacl.workspace = true
scale-info.workspace = true
schnorrkel.workspace = true
serde.workspace = true
serde_json.workspace = true
clap = { workspace = true, features = ["std", "derive"] }
thiserror.workspace = true
tokio = { workspace = true, features = [ "full" ] }
whoami.workspace = true
reqwest = { workspace = true, default-features = false, features = [ "json", "rustls-tls" ] }
etc.workspace = true
runtime-primitives.workspace = true
url = { workspace = true, features = ["serde"] }
toml.workspace = true
# TODO: use wasmi from workspace (#3214)
wasmi = { version = "0.30.0", features = ["std"] }

Expand Down
29 changes: 28 additions & 1 deletion gcli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
use clap::Parser;
use color_eyre::{eyre::eyre, Result};
use env_logger::{Builder, Env};
use gclient::{ext::sp_core, GearApi};
use gclient::{
ext::sp_core::{self, crypto::Ss58Codec, sr25519::Pair, Pair as _},
GearApi,
};
use gring::Keyring;
use gsdk::Api;

Expand Down Expand Up @@ -87,9 +90,33 @@ pub trait App: Parser + Sync {
None
}

/// Get the address of the primary key
fn ss58_address(&self) -> String {
gring::cmd::Command::store()
.and_then(Keyring::load)
.and_then(|mut s| s.primary())
.map(|k| k.address)
.unwrap_or(
Pair::from_string("//Alice", None)
.expect("Alice always works")
.public()
.to_ss58check(),
)
}

/// Exec program from the parsed arguments.
async fn exec(&self) -> anyhow::Result<()>;

/// Get gear api without signing in with password.
async fn api(&self) -> anyhow::Result<GearApi> {
let endpoint = self.endpoint().clone();
let timeout = self.timeout();
Api::new_with_timeout(endpoint.as_deref(), Some(timeout))
.await
.map(Into::into)
.map_err(Into::into)
}

/// Get signer.
async fn signer(&self) -> anyhow::Result<GearApi> {
let endpoint = self.endpoint().clone();
Expand Down
172 changes: 172 additions & 0 deletions gcli/src/cmd/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// This file is part of Gear.
//
// Copyright (C) 2024 Gear Technologies Inc.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

use anyhow::{anyhow, Result};
use clap::{builder::PossibleValue, Parser, ValueEnum};
use colored::Colorize;
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, fmt, fs, path::PathBuf};
use url::Url;

const CONFIG_PATH: &str = ".config/vara/config.toml";

/// Gear command line configuration
#[derive(Clone, Debug, Parser)]
pub struct Config {
/// Config actions
#[clap(subcommand)]
pub action: Action,
}

impl Config {
/// NOTE: currently just a simple wrapper for [`ConfigSettings::write`]
/// since we just have one config option.
pub fn exec(&self) -> Result<()> {
match &self.action {
Action::Set(s) => {
s.write(None)?;
println!("{s}");
}
// prints the whole config atm.
Action::Get { url: _ } => {
let settings = ConfigSettings::read(None)?;
println!("{settings}");
}
}

Ok(())
}
}

/// Gear command client config settings
#[derive(Clone, Debug, Parser, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct ConfigSettings {
/// URL for Vara's JSON RPC or moniker
#[clap(short, long, name = "URL_OR_MONIKER")]
pub url: Network,
}

impl ConfigSettings {
fn config() -> Result<PathBuf> {
dirs::home_dir()
.map(|h| h.join(CONFIG_PATH))
.ok_or_else(|| anyhow!("Could not find config.toml from ${{HOME}}/{CONFIG_PATH}"))
}

/// Read the config from disk
pub fn read(path: Option<PathBuf>) -> Result<ConfigSettings> {
let conf = path.unwrap_or(Self::config()?);
toml::from_str(&fs::read_to_string(conf)?).map_err(Into::into)
}

/// Write the whole settings to disk
///
/// NOTE: this method should be updated as well once
/// there are more options in the settings.
pub fn write(&self, path: Option<PathBuf>) -> Result<()> {
let conf = path.unwrap_or(Self::config()?);
if let Some(parent) = conf.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}

fs::write(conf, toml::to_string_pretty(self)?).map_err(Into::into)
}
}

impl fmt::Display for ConfigSettings {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", "RPC URL".bold(), self.url.clone().as_ref())
}
}

/// Config action
#[derive(Clone, Debug, Parser, PartialEq, Eq)]
pub enum Action {
/// Set a config setting
Set(ConfigSettings),
/// Get current config settings
Get {
/// Get the rpc url from the current config settings.
#[clap(short, long)]
url: bool,
},
}

/// Vara networks
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Network {
/// Vara main network
#[default]
Mainnet,
/// Vara development network
Testnet,
/// Localhost endpoint
Localhost,
/// Customized vara network
Custom(Url),
}

impl AsRef<str> for Network {
fn as_ref(&self) -> &str {
match self {
Self::Mainnet => "wss://rpc.vara.network:443",
Self::Testnet => "wss://testnet.vara.network:443",
Self::Localhost => "ws://localhost:9944",
Self::Custom(url) => url.as_str(),
}
}
}

impl ToString for Network {
fn to_string(&self) -> String {
self.as_ref().into()
}
}

impl ValueEnum for Network {
fn value_variants<'a>() -> &'a [Self] {
&[Network::Mainnet, Network::Testnet, Network::Localhost]
}

fn to_possible_value(&self) -> Option<PossibleValue> {
match self {
Self::Mainnet => Some(PossibleValue::new("mainnet")),
Self::Testnet => Some(PossibleValue::new("testnet")),
Self::Localhost => Some(PossibleValue::new("localhost")),
Self::Custom(_) => None,
}
}

fn from_str(input: &str, ignore_case: bool) -> Result<Self, String> {
let input = if ignore_case {
Cow::Owned(input.to_lowercase())
} else {
Cow::Borrowed(input)
};

Ok(match input.as_ref() {
"mainnet" => Self::Mainnet,
"testnet" => Self::Testnet,
"localhost" => Self::Localhost,
input => Self::Custom(Url::parse(input).map_err(|_| input.to_string())?),
})
}
}
14 changes: 6 additions & 8 deletions gcli/src/cmd/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,11 @@ pub struct Info {
}

impl Info {
/// execute command transfer
/// Execute command transfer
pub async fn exec(&self, app: &impl App) -> Result<()> {
let signer = app.signer().await?;
let mut address = self
.address
.clone()
.unwrap_or_else(|| signer.account_id().to_ss58check());
let api = app.api().await?;
let mut address = self.address.clone().unwrap_or_else(|| app.ss58_address());

if address.starts_with("//") {
address = Pair::from_string(&address, None)
.expect("Parse development address failed")
Expand All @@ -70,8 +68,8 @@ impl Info {

let acc = AccountId32::from_ss58check(&address)?;
match self.action {
Action::Balance => Self::balance(signer, acc).await,
Action::Mailbox { count } => Self::mailbox(signer, acc, count).await,
Action::Balance => Self::balance(api, acc).await,
Action::Mailbox { count } => Self::mailbox(api, acc, count).await,
}
}

Expand Down
27 changes: 22 additions & 5 deletions gcli/src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! commands
use crate::App;
use clap::Parser;

pub mod claim;
pub mod config;
pub mod create;
pub mod info;
pub mod new;
Expand All @@ -33,9 +32,21 @@ pub mod upload;
pub mod wallet;

pub use self::{
claim::Claim, create::Create, info::Info, new::New, program::Program, reply::Reply, send::Send,
transfer::Transfer, update::Update, upload::Upload, wallet::Wallet,
claim::Claim,
config::{Config, ConfigSettings},
create::Create,
info::Info,
new::New,
program::Program,
reply::Reply,
send::Send,
transfer::Transfer,
update::Update,
upload::Upload,
wallet::Wallet,
};
use crate::App;
use clap::Parser;

/// All SubCommands of gear command line interface.
#[derive(Clone, Debug, Parser)]
Expand All @@ -44,6 +55,7 @@ pub enum Command {
Create(Create),
Info(Info),
New(New),
Config(Config),
#[clap(subcommand)]
Program(Program),
Reply(Reply),
Expand All @@ -59,6 +71,7 @@ impl Command {
/// Execute the command.
pub async fn exec(&self, app: &impl App) -> anyhow::Result<()> {
match self {
Command::Config(config) => config.exec()?,
Command::New(new) => new.exec().await?,
Command::Program(program) => program.exec(app).await?,
Command::Update(update) => update.exec().await?,
Expand Down Expand Up @@ -128,7 +141,11 @@ impl App for Opt {
}

fn endpoint(&self) -> Option<String> {
self.endpoint.clone()
if self.endpoint.is_some() {
return self.endpoint.clone();
}

ConfigSettings::read(None).ok().map(|c| c.url.to_string())
}

fn passwd(&self) -> Option<String> {
Expand Down
Loading

0 comments on commit f103875

Please sign in to comment.