merged with fbdf31b879
This commit is contained in:
@@ -35,7 +35,7 @@ default-features = false
|
||||
|
||||
# non-wasm-only dependencies
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
|
||||
version = "1.4"
|
||||
version = "1.19.1"
|
||||
features = ["macros", "rt", "net", "sync", "time"]
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite]
|
||||
|
||||
@@ -83,7 +83,7 @@ impl GatewayClient {
|
||||
) -> Self {
|
||||
GatewayClient {
|
||||
authenticated: false,
|
||||
disabled_credentials_mode: false,
|
||||
disabled_credentials_mode: true,
|
||||
bandwidth_remaining: 0,
|
||||
gateway_address,
|
||||
gateway_identity,
|
||||
@@ -100,8 +100,8 @@ impl GatewayClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_disabled_credentials_mode(&mut self, _disabled_credentials_mode: bool) {
|
||||
self.disabled_credentials_mode = false;
|
||||
pub fn set_disabled_credentials_mode(&mut self, disabled_credentials_mode: bool) {
|
||||
self.disabled_credentials_mode = disabled_credentials_mode;
|
||||
}
|
||||
|
||||
// TODO: later convert into proper builder methods
|
||||
|
||||
@@ -9,8 +9,8 @@ edition = "2021"
|
||||
[dependencies]
|
||||
futures = "0.3"
|
||||
log = "0.4.8"
|
||||
tokio = { version = "1.4", features = ["time", "net", "rt"] }
|
||||
tokio-util = { version = "0.6", features = ["codec"] }
|
||||
tokio = { version = "1.19.1", features = ["time", "net", "rt"] }
|
||||
tokio-util = { version = "0.7.3", features = ["codec"] }
|
||||
|
||||
# internal
|
||||
nymsphinx = {path = "../../nymsphinx" }
|
||||
|
||||
@@ -22,7 +22,7 @@ reqwest = { version = "0.11", features = ["json"] }
|
||||
thiserror = "1"
|
||||
log = "0.4"
|
||||
url = { version = "2.2", features = ["serde"] }
|
||||
tokio = { version = "1.10", features = ["sync", "time"] }
|
||||
tokio = { version = "1.19.1", features = ["sync", "time"] }
|
||||
futures = "0.3"
|
||||
|
||||
coconut-interface = { path = "../../coconut-interface" }
|
||||
@@ -40,7 +40,7 @@ prost = { version = "0.10", default-features = false, optional = true }
|
||||
flate2 = { version = "1.0.20", optional = true }
|
||||
sha2 = { version = "0.9.5", optional = true }
|
||||
itertools = { version = "0.10", optional = true }
|
||||
cosmwasm-std = { version = "1.0.0-beta8", optional = true }
|
||||
cosmwasm-std = { version = "1.0.0", optional = true }
|
||||
execute = { path = "../../execute" }
|
||||
|
||||
ts-rs = "6.1.2"
|
||||
|
||||
@@ -189,23 +189,23 @@ impl NymdClient<SigningNymdClient> {
|
||||
client: SigningNymdClient::connect_with_signer(endpoint, wallet, gas_price)?,
|
||||
client_address: Some(client_address),
|
||||
simulated_gas_multiplier: DEFAULT_SIMULATED_GAS_MULTIPLIER,
|
||||
mixnet_contract_address: DEFAULT_NETWORK
|
||||
mixnet_contract_address: network
|
||||
.mixnet_contract_address()
|
||||
.parse()
|
||||
.expect("Error parsing mixnet contract address"),
|
||||
vesting_contract_address: DEFAULT_NETWORK
|
||||
vesting_contract_address: network
|
||||
.vesting_contract_address()
|
||||
.parse()
|
||||
.expect("Error parsing vesting contract address"),
|
||||
bandwidth_claim_contract_address: DEFAULT_NETWORK
|
||||
bandwidth_claim_contract_address: network
|
||||
.bandwidth_claim_contract_address()
|
||||
.parse()
|
||||
.expect("Error parsing bandwidth claim contract address"),
|
||||
coconut_bandwidth_contract_address: DEFAULT_NETWORK
|
||||
coconut_bandwidth_contract_address: network
|
||||
.coconut_bandwidth_contract_address()
|
||||
.parse()
|
||||
.unwrap(),
|
||||
multisig_contract_address: DEFAULT_NETWORK.multisig_contract_address().parse().unwrap(),
|
||||
multisig_contract_address: network.multisig_contract_address().parse().unwrap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
|
||||
fn template() -> &'static str;
|
||||
|
||||
fn config_file_name() -> String {
|
||||
log::trace!("NymdConfig::config_file_name");
|
||||
"config.toml".to_string()
|
||||
}
|
||||
|
||||
@@ -23,7 +22,6 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
|
||||
|
||||
// default, most probable, implementations; can be easily overridden where required
|
||||
fn default_config_directory(id: Option<&str>) -> PathBuf {
|
||||
log::trace!("NymdConfig::default_config_directory");
|
||||
if let Some(id) = id {
|
||||
Self::default_root_directory().join(id).join("config")
|
||||
} else {
|
||||
@@ -32,7 +30,6 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
|
||||
}
|
||||
|
||||
fn default_data_directory(id: Option<&str>) -> PathBuf {
|
||||
log::trace!("NymdConfig::default_data_path");
|
||||
if let Some(id) = id {
|
||||
Self::default_root_directory().join(id).join("data")
|
||||
} else {
|
||||
@@ -41,7 +38,6 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
|
||||
}
|
||||
|
||||
fn default_config_file_path(id: Option<&str>) -> PathBuf {
|
||||
log::trace!("NymdConfig::default_config_file_path");
|
||||
Self::default_config_directory(id).join(Self::config_file_name())
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = "1.0.0-beta6"
|
||||
cosmwasm-std = "1.0.0"
|
||||
schemars = "0.8"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
|
||||
@@ -7,4 +7,4 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = "1.0.0-beta8"
|
||||
cosmwasm-std = "1.0.0"
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = "1.0.0-beta8"
|
||||
cosmwasm-std = "1.0.0"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_repr = "0.1"
|
||||
|
||||
@@ -460,7 +460,7 @@ impl MixNodeBond {
|
||||
.unwrap_or_default();
|
||||
let operator_reward = self.operator_reward(params);
|
||||
// Total reward has to be the sum of operator and delegator rewards
|
||||
let delegators_reward = node_profit - operator_reward;
|
||||
let delegators_reward = node_profit.saturating_sub(operator_reward);
|
||||
|
||||
Ok(RewardEstimate {
|
||||
total_node_reward: total_node_reward.try_into()?,
|
||||
|
||||
@@ -9,6 +9,6 @@ edition = "2021"
|
||||
cw-utils = { version = "0.13.1" }
|
||||
cw3 = { version = "0.13.1" }
|
||||
cw4 = { version = "0.13.1" }
|
||||
cosmwasm-std = "1.0.0-beta6"
|
||||
cosmwasm-std = "1.0.0"
|
||||
schemars = "0.8"
|
||||
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
|
||||
@@ -6,11 +6,11 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = "1.0.0-beta8"
|
||||
cosmwasm-std = "1.0.0"
|
||||
mixnet-contract-common = { path = "../mixnet-contract" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
schemars = "0.8"
|
||||
cw-storage-plus = "0.13.2"
|
||||
cw-storage-plus = "0.13.4"
|
||||
config = { path = "../../config" }
|
||||
ts-rs = "6.1.2"
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ nymcoconut = { path = "../nymcoconut" }
|
||||
log = "0.4"
|
||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]}
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.4", features = [ "rt-multi-thread", "net", "signal", "fs" ] }
|
||||
tokio = { version = "1.19.1", features = [ "rt-multi-thread", "net", "signal", "fs" ] }
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
|
||||
tokio = { version = "1.4", features = ["rt-multi-thread", "macros"] }
|
||||
tokio = { version = "1.19.1", features = ["rt-multi-thread", "macros"] }
|
||||
@@ -14,8 +14,8 @@ humantime-serde = "1.0"
|
||||
log = "0.4"
|
||||
rand = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.4", features = ["time", "macros", "rt", "net", "io-util"] }
|
||||
tokio-util = { version = "0.6", features = ["codec"] }
|
||||
tokio = { version = "1.19.1", features = ["time", "macros", "rt", "net", "io-util"] }
|
||||
tokio-util = { version = "0.7.3", features = ["codec"] }
|
||||
url = "2.2"
|
||||
|
||||
crypto = { path = "../crypto" }
|
||||
|
||||
@@ -7,6 +7,6 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.4", features = [] }
|
||||
tokio-stream = "0.1" # this one seems to be a thing until `Stream` trait is stabilised in stdlib
|
||||
tokio-util = { version = "0.6", features = ["time"] }
|
||||
tokio = { version = "1.19.1", features = [] }
|
||||
tokio-stream = "0.1.9" # this one seems to be a thing until `Stream` trait is stabilised in stdlib
|
||||
tokio-util = { version = "0.7.3", features = ["time"] }
|
||||
|
||||
@@ -33,5 +33,5 @@ mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract"
|
||||
path = "framing"
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
|
||||
version = "1.4"
|
||||
version = "1.19.1"
|
||||
features = ["sync"]
|
||||
|
||||
@@ -8,7 +8,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.0"
|
||||
tokio-util = { version = "0.6", features = ["codec"] }
|
||||
tokio-util = { version = "0.7.3", features = ["codec"] }
|
||||
|
||||
nymsphinx-types = { path = "../types" }
|
||||
nymsphinx-params = { path = "../params" }
|
||||
|
||||
@@ -8,8 +8,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.0"
|
||||
tokio = { version = "1.4", features = [ "net", "io-util", "sync", "macros", "time", "rt-multi-thread" ] }
|
||||
tokio-util = { version = "0.6", features = [ "io" ] } # reason for getting this guy is to to able to port to tokio 1.X more quickly by being able to use
|
||||
tokio = { version = "1.19.1", features = [ "net", "io-util", "sync", "macros", "time", "rt-multi-thread" ] }
|
||||
tokio-util = { version = "0.7.3", features = [ "io" ] } # reason for getting this guy is to to able to port to tokio 1.X more quickly by being able to use
|
||||
# their `read_buf` [from the util crate] replacement rather than having to rethink/reimplement `AvailableReader` with the new AsyncRead trait definition.
|
||||
# In the long run, the dependency should probably get removed in favour of pure-tokio implementation, but for time being it's fine.
|
||||
futures = "0.3"
|
||||
@@ -18,4 +18,4 @@ socks5-requests = { path = "../requests" }
|
||||
ordered-buffer = { path = "../ordered-buffer" }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = "0.4"
|
||||
tokio-test = "0.4.2"
|
||||
@@ -32,6 +32,26 @@ impl Message {
|
||||
const REQUEST_FLAG: u8 = 0;
|
||||
const RESPONSE_FLAG: u8 = 1;
|
||||
|
||||
pub fn conn_id(&self) -> u64 {
|
||||
match self {
|
||||
Message::Request(req) => match req {
|
||||
Request::Connect(c) => c.conn_id,
|
||||
Request::Send(conn_id, _, _) => *conn_id,
|
||||
},
|
||||
Message::Response(resp) => resp.connection_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
match self {
|
||||
Message::Request(req) => match req {
|
||||
Request::Connect(_) => 0,
|
||||
Request::Send(_, data, _) => data.len(),
|
||||
},
|
||||
Message::Response(resp) => resp.data.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_from_bytes(b: &[u8]) -> Result<Message, MessageError> {
|
||||
if b.is_empty() {
|
||||
return Err(MessageError::NoData);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "statistics"
|
||||
version = "1.0.1"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
reqwest = "0.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
thiserror = "1"
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::StatsError;
|
||||
use crate::StatsMessage;
|
||||
|
||||
pub const DEFAULT_STATISTICS_SERVICE_ADDRESS: &str = "127.0.0.1";
|
||||
pub const DEFAULT_STATISTICS_SERVICE_PORT: u16 = 8090;
|
||||
|
||||
pub const STATISTICS_SERVICE_VERSION: &str = "/v1";
|
||||
pub const STATISTICS_SERVICE_API_STATISTICS: &str = "statistic";
|
||||
|
||||
pub fn build_statistics_request_bytes(msg: StatsMessage) -> Result<Vec<u8>, StatsError> {
|
||||
let json_msg = msg.to_json()?;
|
||||
|
||||
let req = reqwest::Request::new(
|
||||
reqwest::Method::POST,
|
||||
reqwest::Url::parse(&format!(
|
||||
"http://{}:{}/{}/{}",
|
||||
DEFAULT_STATISTICS_SERVICE_ADDRESS,
|
||||
DEFAULT_STATISTICS_SERVICE_PORT,
|
||||
STATISTICS_SERVICE_VERSION,
|
||||
STATISTICS_SERVICE_API_STATISTICS
|
||||
))
|
||||
.unwrap(),
|
||||
);
|
||||
let data = format!(
|
||||
"{} {} {:?}\n\
|
||||
Content-Type: application/json\n\
|
||||
Content-Length: {}\n\n\
|
||||
{}\n",
|
||||
req.method().as_str(),
|
||||
req.url().as_str(),
|
||||
req.version(),
|
||||
json_msg.len(),
|
||||
json_msg
|
||||
);
|
||||
|
||||
Ok(data.into_bytes())
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum StatsError {
|
||||
#[error("Serde JSON error: {0}")]
|
||||
SerdeJsonError(#[from] serde_json::Error),
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use error::StatsError;
|
||||
|
||||
pub mod api;
|
||||
pub mod error;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct StatsMessage {
|
||||
pub stats_data: Vec<StatsServiceData>,
|
||||
pub interval_seconds: u32,
|
||||
pub timestamp: String,
|
||||
}
|
||||
|
||||
impl StatsMessage {
|
||||
pub fn to_json(&self) -> Result<String, StatsError> {
|
||||
Ok(serde_json::to_string(self)?)
|
||||
}
|
||||
|
||||
pub fn from_json(s: &str) -> Result<Self, StatsError> {
|
||||
Ok(serde_json::from_str(s)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct StatsServiceData {
|
||||
pub requested_service: String,
|
||||
pub request_bytes: u32,
|
||||
pub response_bytes: u32,
|
||||
}
|
||||
|
||||
impl StatsServiceData {
|
||||
pub fn new(requested_service: String, request_bytes: u32, response_bytes: u32) -> Self {
|
||||
StatsServiceData {
|
||||
requested_service,
|
||||
request_bytes,
|
||||
response_bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "task"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal"] }
|
||||
tokio-util = { version = "0.7.3", features = ["codec"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal", "test-util", "macros"] }
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod shutdown;
|
||||
|
||||
pub use shutdown::{ShutdownListener, ShutdownNotifier};
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::sync::watch::{self, error::SendError};
|
||||
|
||||
const SHUTDOWN_TIMER_SECS: u64 = 5;
|
||||
|
||||
/// Used to notify other tasks to gracefully shutdown
|
||||
#[derive(Debug)]
|
||||
pub struct ShutdownNotifier {
|
||||
notify_tx: watch::Sender<()>,
|
||||
notify_rx: Option<watch::Receiver<()>>,
|
||||
}
|
||||
|
||||
impl Default for ShutdownNotifier {
|
||||
fn default() -> Self {
|
||||
let (notify_tx, notify_rx) = watch::channel(());
|
||||
Self {
|
||||
notify_tx,
|
||||
notify_rx: Some(notify_rx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ShutdownNotifier {
|
||||
pub fn subscribe(&self) -> ShutdownListener {
|
||||
ShutdownListener::new(
|
||||
self.notify_rx
|
||||
.as_ref()
|
||||
.expect("Unable to subscribe to shutdown notifier that is already shutdown")
|
||||
.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn signal_shutdown(&self) -> Result<(), SendError<()>> {
|
||||
self.notify_tx.send(())
|
||||
}
|
||||
|
||||
pub async fn wait_for_shutdown(&mut self) {
|
||||
if let Some(notify_rx) = self.notify_rx.take() {
|
||||
drop(notify_rx);
|
||||
}
|
||||
|
||||
tokio::select! {
|
||||
_ = self.notify_tx.closed() => {
|
||||
log::info!("All registered tasks succesfully shutdown");
|
||||
},
|
||||
_ = tokio::signal::ctrl_c() => {
|
||||
log::info!("Forcing shutdown");
|
||||
}
|
||||
_ = tokio::time::sleep(Duration::from_secs(SHUTDOWN_TIMER_SECS)) => {
|
||||
log::info!("Timout reached, forcing shutdown");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Listen for shutdown notifications
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ShutdownListener {
|
||||
shutdown: bool,
|
||||
notify: watch::Receiver<()>,
|
||||
}
|
||||
|
||||
impl ShutdownListener {
|
||||
pub fn new(notify: watch::Receiver<()>) -> ShutdownListener {
|
||||
ShutdownListener {
|
||||
shutdown: false,
|
||||
notify,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_shutdown(&self) -> bool {
|
||||
self.shutdown
|
||||
}
|
||||
|
||||
pub async fn recv(&mut self) {
|
||||
if self.shutdown {
|
||||
return;
|
||||
}
|
||||
let _ = self.notify.changed().await;
|
||||
self.shutdown = true;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn signal_shutdown() {
|
||||
let shutdown = ShutdownNotifier::default();
|
||||
let mut listener = shutdown.subscribe();
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
tokio::select! {
|
||||
_ = listener.recv() => 42,
|
||||
}
|
||||
});
|
||||
|
||||
shutdown.signal_shutdown().unwrap();
|
||||
assert_eq!(task.await.unwrap(), 42);
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,16 @@ impl Node {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Node {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Node(id: {}, owner: {}, stake: {}, location: {}, host: {})",
|
||||
self.identity_key, self.owner, self.stake, self.location, self.host,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl filter::Versioned for Node {
|
||||
fn version(&self) -> String {
|
||||
self.version.clone()
|
||||
|
||||
Reference in New Issue
Block a user