diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock
index c861209f68..c852d480ed 100644
--- a/substrate/Cargo.lock
+++ b/substrate/Cargo.lock
@@ -2476,6 +2476,7 @@ dependencies = [
"srml-offences 1.0.0",
"srml-session 2.0.0",
"srml-staking 2.0.0",
+ "srml-staking-reward-curve 2.0.0",
"srml-sudo 2.0.0",
"srml-support 2.0.0",
"srml-system 2.0.0",
@@ -2599,9 +2600,10 @@ dependencies = [
[[package]]
name = "num-bigint"
-version = "0.2.2"
+version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
+ "autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -2621,7 +2623,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"autocfg 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
- "num-bigint 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-bigint 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -4288,6 +4290,7 @@ dependencies = [
"srml-authorship 0.1.0",
"srml-balances 2.0.0",
"srml-session 2.0.0",
+ "srml-staking-reward-curve 2.0.0",
"srml-support 2.0.0",
"srml-system 2.0.0",
"srml-timestamp 2.0.0",
@@ -4296,6 +4299,25 @@ dependencies = [
"substrate-primitives 2.0.0",
]
+[[package]]
+name = "srml-staking-reward-curve"
+version = "2.0.0"
+dependencies = [
+ "proc-macro-crate 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "sr-primitives 2.0.0",
+ "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "srml-staking-reward-curve-test"
+version = "2.0.0"
+dependencies = [
+ "sr-primitives 2.0.0",
+ "srml-staking-reward-curve 2.0.0",
+]
+
[[package]]
name = "srml-sudo"
version = "2.0.0"
@@ -4751,7 +4773,7 @@ dependencies = [
"futures-timer 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
"merlin 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "num-bigint 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num-bigint 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"num-rational 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"parity-scale-codec 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -6720,7 +6742,7 @@ dependencies = [
"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
"checksum nohash-hasher 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0d138afcce92d219ccb6eb53d9b1e8a96ac0d633cfd3c53cd9856d96d1741bb8"
"checksum nom 4.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
-"checksum num-bigint 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "57450397855d951f1a41305e54851b1a7b8f5d2e349543a02a2effe25459f718"
+"checksum num-bigint 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f9c3f34cdd24f334cb265d9bf8bfa8a241920d026916785747a92f0e55541a1a"
"checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09"
"checksum num-rational 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f2885278d5fe2adc2f75ced642d52d879bffaceb5a2e0b1d4309ffdfb239b454"
"checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32"
diff --git a/substrate/Cargo.toml b/substrate/Cargo.toml
index 6bec85759b..684363c227 100644
--- a/substrate/Cargo.toml
+++ b/substrate/Cargo.toml
@@ -95,6 +95,8 @@ members = [
"srml/scored-pool",
"srml/session",
"srml/staking",
+ "srml/staking/reward-curve",
+ "srml/staking/reward-curve/test",
"srml/sudo",
"srml/system",
"srml/timestamp",
diff --git a/substrate/core/sr-primitives/src/curve.rs b/substrate/core/sr-primitives/src/curve.rs
new file mode 100644
index 0000000000..447c57ee32
--- /dev/null
+++ b/substrate/core/sr-primitives/src/curve.rs
@@ -0,0 +1,164 @@
+// Copyright 2019 Parity Technologies (UK) Ltd.
+// This file is part of Substrate.
+
+// Substrate 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.
+
+// Substrate 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 Substrate. If not, see .
+
+//! Provides some utilities to define a piecewise linear function.
+
+use crate::{Perbill, traits::{SimpleArithmetic, SaturatedConversion}};
+use core::ops::Sub;
+
+/// Piecewise Linear function in [0, 1] -> [0, 1].
+#[cfg_attr(feature = "std", derive(Debug))]
+#[derive(PartialEq, Eq)]
+pub struct PiecewiseLinear<'a> {
+ /// Array of points. Must be in order from the lowest abscissas to the highest.
+ pub points: &'a [(Perbill, Perbill)]
+}
+
+fn abs_sub + Clone>(a: N, b: N) -> N where {
+ a.clone().max(b.clone()) - a.min(b)
+}
+
+impl<'a> PiecewiseLinear<'a> {
+ /// Compute `f(n/d)*d` with `n <= d`. This is useful to avoid loss of precision.
+ pub fn calculate_for_fraction_times_denominator(&self, n: N, d: N) -> N where
+ N: SimpleArithmetic + Clone
+ {
+ let n = n.min(d.clone());
+
+ if self.points.len() == 0 {
+ return N::zero()
+ }
+
+ let next_point_index = self.points.iter()
+ .position(|p| n < p.0 * d.clone());
+
+ let (prev, next) = if let Some(next_point_index) = next_point_index {
+ if let Some(previous_point_index) = next_point_index.checked_sub(1) {
+ (self.points[previous_point_index], self.points[next_point_index])
+ } else {
+ // There is no previous points, take first point ordinate
+ return self.points.first().map(|p| p.1).unwrap_or_else(Perbill::zero) * d
+ }
+ } else {
+ // There is no next points, take last point ordinate
+ return self.points.last().map(|p| p.1).unwrap_or_else(Perbill::zero) * d
+ };
+
+ let delta_y = multiply_by_rational_saturating(
+ abs_sub(n.clone(), prev.0 * d.clone()),
+ abs_sub(next.1.into_parts(), prev.1.into_parts()),
+ // Must not saturate as prev abscissa > next abscissa
+ next.0.into_parts().saturating_sub(prev.0.into_parts()),
+ );
+
+ // If both substration are same sign then result is positive
+ if (n > prev.0 * d.clone()) == (next.1.into_parts() > prev.1.into_parts()) {
+ (prev.1 * d).saturating_add(delta_y)
+ // Otherwise result is negative
+ } else {
+ (prev.1 * d).saturating_sub(delta_y)
+ }
+ }
+}
+
+// Compute value * p / q.
+// This is guaranteed not to overflow on whatever values nor lose precision.
+// `q` must be superior to zero.
+fn multiply_by_rational_saturating(value: N, p: u32, q: u32) -> N
+ where N: SimpleArithmetic + Clone
+{
+ let q = q.max(1);
+
+ // Mul can saturate if p > q
+ let result_divisor_part = (value.clone() / q.into()).saturating_mul(p.into());
+
+ let result_remainder_part = {
+ let rem = value % q.into();
+
+ // Fits into u32 because q is u32 and remainder < q
+ let rem_u32 = rem.saturated_into::();
+
+ // Multiplication fits into u64 as both term are u32
+ let rem_part = rem_u32 as u64 * p as u64 / q as u64;
+
+ // Can saturate if p > q
+ rem_part.saturated_into::()
+ };
+
+ // Can saturate if p > q
+ result_divisor_part.saturating_add(result_remainder_part)
+}
+
+#[test]
+fn test_multiply_by_rational_saturating() {
+ use std::convert::TryInto;
+
+ let div = 100u32;
+ for value in 0..=div {
+ for p in 0..=div {
+ for q in 1..=div {
+ let value: u64 = (value as u128 * u64::max_value() as u128 / div as u128)
+ .try_into().unwrap();
+ let p = (p as u64 * u32::max_value() as u64 / div as u64)
+ .try_into().unwrap();
+ let q = (q as u64 * u32::max_value() as u64 / div as u64)
+ .try_into().unwrap();
+
+ assert_eq!(
+ multiply_by_rational_saturating(value, p, q),
+ (value as u128 * p as u128 / q as u128)
+ .try_into().unwrap_or(u64::max_value())
+ );
+ }
+ }
+ }
+}
+
+#[test]
+fn test_calculate_for_fraction_times_denominator() {
+ use std::convert::TryInto;
+
+ let curve = PiecewiseLinear {
+ points: &[
+ (Perbill::from_parts(0_000_000_000), Perbill::from_parts(0_500_000_000)),
+ (Perbill::from_parts(0_500_000_000), Perbill::from_parts(1_000_000_000)),
+ (Perbill::from_parts(1_000_000_000), Perbill::from_parts(0_000_000_000)),
+ ]
+ };
+
+ pub fn formal_calculate_for_fraction_times_denominator(n: u64, d: u64) -> u64 {
+ if n <= Perbill::from_parts(0_500_000_000) * d.clone() {
+ n + d / 2
+ } else {
+ (d as u128 * 2 - n as u128 * 2).try_into().unwrap()
+ }
+ }
+
+ let div = 100u32;
+ for d in 0..=div {
+ for n in 0..=d {
+ let d: u64 = (d as u128 * u64::max_value() as u128 / div as u128)
+ .try_into().unwrap();
+ let n: u64 = (n as u128 * u64::max_value() as u128 / div as u128)
+ .try_into().unwrap();
+
+ let res = curve.calculate_for_fraction_times_denominator(n, d);
+ let expected = formal_calculate_for_fraction_times_denominator(n, d);
+
+ assert!(abs_sub(res, expected) <= 1);
+ }
+ }
+}
diff --git a/substrate/core/sr-primitives/src/lib.rs b/substrate/core/sr-primitives/src/lib.rs
index c3f47f29c4..81e73033b2 100644
--- a/substrate/core/sr-primitives/src/lib.rs
+++ b/substrate/core/sr-primitives/src/lib.rs
@@ -47,6 +47,7 @@ pub mod testing;
pub mod weights;
pub mod traits;
+pub mod curve;
pub mod generic;
pub mod transaction_validity;
diff --git a/substrate/node/runtime/Cargo.toml b/substrate/node/runtime/Cargo.toml
index 554d9561a3..8b1f775cea 100644
--- a/substrate/node/runtime/Cargo.toml
+++ b/substrate/node/runtime/Cargo.toml
@@ -42,6 +42,7 @@ membership = { package = "srml-membership", path = "../../srml/membership", defa
offences = { package = "srml-offences", path = "../../srml/offences", default-features = false }
session = { package = "srml-session", path = "../../srml/session", default-features = false, features = ["historical"] }
staking = { package = "srml-staking", path = "../../srml/staking", default-features = false }
+srml-staking-reward-curve = { path = "../../srml/staking/reward-curve"}
sudo = { package = "srml-sudo", path = "../../srml/sudo", default-features = false }
support = { package = "srml-support", path = "../../srml/support", default-features = false }
system = { package = "srml-system", path = "../../srml/system", default-features = false }
diff --git a/substrate/node/runtime/src/lib.rs b/substrate/node/runtime/src/lib.rs
index f6955d2bd0..bee5e7fc22 100644
--- a/substrate/node/runtime/src/lib.rs
+++ b/substrate/node/runtime/src/lib.rs
@@ -35,7 +35,10 @@ use client::{
block_builder::api::{self as block_builder_api, InherentData, CheckInherentsResult},
runtime_api as client_api, impl_runtime_apis
};
-use sr_primitives::{ApplyResult, impl_opaque_keys, generic, create_runtime_str, key_types};
+use sr_primitives::{
+ Permill, Perbill, ApplyResult, impl_opaque_keys, generic, create_runtime_str, key_types
+};
+use sr_primitives::curve::PiecewiseLinear;
use sr_primitives::transaction_validity::TransactionValidity;
use sr_primitives::weights::Weight;
use sr_primitives::traits::{
@@ -57,7 +60,6 @@ pub use sr_primitives::BuildStorage;
pub use timestamp::Call as TimestampCall;
pub use balances::Call as BalancesCall;
pub use contracts::Gas;
-pub use sr_primitives::{Permill, Perbill};
pub use support::StorageValue;
pub use staking::StakerStatus;
@@ -82,8 +84,8 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
// and set impl_version to equal spec_version. If only runtime
// implementation changes and behavior does not, then leave spec_version as
// is and increment impl_version.
- spec_version: 159,
- impl_version: 159,
+ spec_version: 160,
+ impl_version: 160,
apis: RUNTIME_API_VERSIONS,
};
@@ -232,9 +234,21 @@ impl session::historical::Trait for Runtime {
type FullIdentificationOf = staking::ExposureOf;
}
+srml_staking_reward_curve::build! {
+ const REWARD_CURVE: PiecewiseLinear<'static> = curve!(
+ min_inflation: 0_025_000,
+ max_inflation: 0_100_000,
+ ideal_stake: 0_500_000,
+ falloff: 0_050_000,
+ max_piece_count: 40,
+ test_precision: 0_005_000,
+ );
+}
+
parameter_types! {
pub const SessionsPerEra: sr_staking_primitives::SessionIndex = 6;
pub const BondingDuration: staking::EraIndex = 24 * 28;
+ pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE;
}
impl staking::Trait for Runtime {
@@ -248,6 +262,7 @@ impl staking::Trait for Runtime {
type SessionsPerEra = SessionsPerEra;
type BondingDuration = BondingDuration;
type SessionInterface = Self;
+ type RewardCurve = RewardCurve;
}
parameter_types! {
diff --git a/substrate/srml/staking/Cargo.toml b/substrate/srml/staking/Cargo.toml
index 092bcfda8e..3700d84c09 100644
--- a/substrate/srml/staking/Cargo.toml
+++ b/substrate/srml/staking/Cargo.toml
@@ -23,6 +23,7 @@ authorship = { package = "srml-authorship", path = "../authorship", default-feat
primitives = { package = "substrate-primitives", path = "../../core/primitives" }
balances = { package = "srml-balances", path = "../balances" }
timestamp = { package = "srml-timestamp", path = "../timestamp" }
+srml-staking-reward-curve = { path = "../staking/reward-curve"}
[features]
equalize = []
diff --git a/substrate/srml/staking/reward-curve/Cargo.toml b/substrate/srml/staking/reward-curve/Cargo.toml
new file mode 100644
index 0000000000..4e254e9512
--- /dev/null
+++ b/substrate/srml/staking/reward-curve/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "srml-staking-reward-curve"
+version = "2.0.0"
+authors = ["Parity Technologies "]
+edition = "2018"
+
+[lib]
+proc-macro = true
+
+[dependencies]
+# sr-api-macros = { path = "../../../core/sr-api-macros" }
+syn = { version = "1.0", features = [ "full", "visit" ] }
+quote = "1.0"
+proc-macro2 = "1.0"
+proc-macro-crate = "0.1.3"
+
+[dev-dependencies]
+sr-primitives = { path = "../../../core/sr-primitives" }
diff --git a/substrate/srml/staking/reward-curve/src/lib.rs b/substrate/srml/staking/reward-curve/src/lib.rs
new file mode 100644
index 0000000000..9ed319be7f
--- /dev/null
+++ b/substrate/srml/staking/reward-curve/src/lib.rs
@@ -0,0 +1,414 @@
+extern crate proc_macro;
+
+mod log;
+
+use log::log2;
+use proc_macro::TokenStream;
+use proc_macro2::{TokenStream as TokenStream2, Span};
+use proc_macro_crate::crate_name;
+use quote::{quote, ToTokens};
+use std::convert::TryInto;
+use syn::parse::{Parse, ParseStream};
+
+/// Accepts a number of expressions to create a instance of PiecewiseLinear which represents the
+/// NPoS curve (as detailed
+/// [here](http://research.web3.foundation/en/latest/polkadot/Token%20Economics/#inflation-model))
+/// for those parameters. Parameters are:
+/// - `min_inflation`: the minimal amount to be rewarded between validators, expressed as a fraction
+/// of total issuance. Known as `I_0` in the literature.
+/// Expressed in millionth, must be between 0 and 1_000_000.
+///
+/// - `max_inflation`: the maximum amount to be rewarded between validators, expressed as a fraction
+/// of total issuance. This is attained only when `ideal_stake` is achieved.
+/// Expressed in millionth, must be between min_inflation and 1_000_000.
+///
+/// - `ideal_stake`: the fraction of total issued tokens that should be actively staked behind
+/// validators. Known as `x_ideal` in the literature.
+/// Expressed in millionth, must be between 0_100_000 and 0_900_000.
+///
+/// - `falloff`: Known as `decay_rate` in the literature. A co-efficient dictating the strength of
+/// the global incentivisation to get the `ideal_stake`. A higher number results in less typical
+/// inflation at the cost of greater volatility for validators.
+/// Expressed in millionth, must be between 0 and 1_000_000.
+///
+/// - `max_piece_count`: The maximum number of pieces in the curve. A greater number uses more
+/// resources but results in higher accuracy.
+/// Must be between 2 and 1_000.
+///
+/// - `test_precision`: The maximum error allowed in the generated test.
+/// Expressed in millionth, must be between 0 and 1_000_000.
+///
+/// # Example
+///
+/// ```
+/// # fn main() {}
+/// use sr_primitives::curve::PiecewiseLinear;
+///
+/// srml_staking_reward_curve::build! {
+/// const I_NPOS: PiecewiseLinear<'static> = curve!(
+/// min_inflation: 0_025_000,
+/// max_inflation: 0_100_000,
+/// ideal_stake: 0_500_000,
+/// falloff: 0_050_000,
+/// max_piece_count: 40,
+/// test_precision: 0_005_000,
+/// );
+/// }
+/// ```
+#[proc_macro]
+pub fn build(input: TokenStream) -> TokenStream {
+ let input = syn::parse_macro_input!(input as INposInput);
+
+ let points = compute_points(&input);
+
+ let declaration = generate_piecewise_linear(points);
+ let test_module = generate_test_module(&input);
+
+ let imports = match crate_name("sr-primitives") {
+ Ok(sr_primitives) => {
+ let ident = syn::Ident::new(&sr_primitives, Span::call_site());
+ quote!( extern crate #ident as _sr_primitives; )
+ },
+ Err(e) => syn::Error::new(Span::call_site(), &e).to_compile_error(),
+ };
+
+ let const_name = input.ident;
+ let const_type = input.typ;
+
+ quote!(
+ const #const_name: #const_type = {
+ #imports
+ #declaration
+ };
+ #test_module
+ ).into()
+}
+
+const MILLION: u32 = 1_000_000;
+
+mod keyword {
+ syn::custom_keyword!(curve);
+ syn::custom_keyword!(min_inflation);
+ syn::custom_keyword!(max_inflation);
+ syn::custom_keyword!(ideal_stake);
+ syn::custom_keyword!(falloff);
+ syn::custom_keyword!(max_piece_count);
+ syn::custom_keyword!(test_precision);
+}
+
+struct INposInput {
+ ident: syn::Ident,
+ typ: syn::Type,
+ min_inflation: u32,
+ ideal_stake: u32,
+ max_inflation: u32,
+ falloff: u32,
+ max_piece_count: u32,
+ test_precision: u32,
+}
+
+struct Bounds {
+ min: u32,
+ min_strict: bool,
+ max: u32,
+ max_strict: bool,
+}
+
+impl Bounds {
+ fn check(&self, value: u32) -> bool {
+ let wrong = (self.min_strict && value <= self.min)
+ || (!self.min_strict && value < self.min)
+ || (self.max_strict && value >= self.max)
+ || (!self.max_strict && value > self.max);
+
+ !wrong
+ }
+}
+
+impl core::fmt::Display for Bounds {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ write!(
+ f,
+ "{}{:07}; {:07}{}",
+ if self.min_strict { "]" } else { "[" },
+ self.min,
+ self.max,
+ if self.max_strict { "[" } else { "]" },
+ )
+ }
+}
+
+fn parse_field(input: ParseStream, bounds: Bounds)
+ -> syn::Result
+{
+ ::parse(&input)?;
+ ::parse(&input)?;
+ let value_lit = syn::LitInt::parse(&input)?;
+ let value: u32 = value_lit.base10_parse()?;
+ if !bounds.check(value) {
+ return Err(syn::Error::new(value_lit.span(), format!(
+ "Invalid {}: {}, must be in {}", Token::default().to_token_stream(), value, bounds,
+ )));
+ }
+
+ Ok(value)
+}
+
+impl Parse for INposInput {
+ fn parse(input: ParseStream) -> syn::Result {
+ let args_input;
+
+ ::parse(&input)?;
+ let ident = ::parse(&input)?;
+ ::parse(&input)?;
+ let typ = ::parse(&input)?;
+ ::parse(&input)?;
+ ::parse(&input)?;
+ ::parse(&input)?;
+ syn::parenthesized!(args_input in input);
+ ::parse(&input)?;
+
+ if !input.is_empty() {
+ return Err(input.error("expected end of input stream, no token expected"));
+ }
+
+ let min_inflation = parse_field::(&args_input, Bounds {
+ min: 0,
+ min_strict: true,
+ max: 1_000_000,
+ max_strict: false,
+ })?;
+ ::parse(&args_input)?;
+ let max_inflation = parse_field::(&args_input, Bounds {
+ min: min_inflation,
+ min_strict: true,
+ max: 1_000_000,
+ max_strict: false,
+ })?;
+ ::parse(&args_input)?;
+ let ideal_stake = parse_field::(&args_input, Bounds {
+ min: 0_100_000,
+ min_strict: false,
+ max: 0_900_000,
+ max_strict: false,
+ })?;
+ ::parse(&args_input)?;
+ let falloff = parse_field::(&args_input, Bounds {
+ min: 0_010_000,
+ min_strict: false,
+ max: 1_000_000,
+ max_strict: false,
+ })?;
+ ::parse(&args_input)?;
+ let max_piece_count = parse_field::(&args_input, Bounds {
+ min: 2,
+ min_strict: false,
+ max: 1_000,
+ max_strict: false,
+ })?;
+ ::parse(&args_input)?;
+ let test_precision = parse_field::(&args_input, Bounds {
+ min: 0,
+ min_strict: false,
+ max: 1_000_000,
+ max_strict: false,
+ })?;
+