From f55f85f3278ba4cc423316c7b6ea23149d82893f Mon Sep 17 00:00:00 2001 From: Eugenio Paluello Date: Fri, 6 Jun 2025 10:40:13 +0200 Subject: [PATCH 1/9] --at time in wall clock time --- Cargo.lock | 119 ++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + README.md | 2 +- src/functions.rs | 69 +++++++++++++++++++++++ src/submit_referendum.rs | 23 ++++++-- src/types.rs | 3 +- 6 files changed, 207 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5988e14..079d48f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,6 +64,21 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.18" @@ -970,6 +985,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "cipher" version = "0.4.4" @@ -2271,6 +2300,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -3096,6 +3149,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" name = "opengov-cli" version = "0.1.0" dependencies = [ + "chrono", "clap", "hex", "parity-scale-codec", @@ -5733,14 +5787,55 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + [[package]] name = "windows-registry" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ - "windows-result", - "windows-strings", + "windows-result 0.2.0", + "windows-strings 0.1.0", "windows-targets", ] @@ -5753,16 +5848,34 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-strings" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-targets", ] +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 1f0f44d..067ee27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,4 @@ hex = "0.4.3" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } clap = { version = "4.4.18", features = ["derive", "cargo"] } reqwest = "0.12.3" +chrono = "0.4.41" diff --git a/README.md b/README.md index c0c1545..dedd5d6 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Options: -t, --track Track on which to submit the referendum --at - Optional: Enact at a particular block number + Optional: Enact at a particular wall clock time (format: DD-MM-YYThhmm, e.g. 25-05-21T0800) --after Optional: Enact after a given number of blocks --output-len-limit diff --git a/src/functions.rs b/src/functions.rs index 3981e58..5d5e1c9 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -1,4 +1,19 @@ +use chrono::{DateTime, Duration, Local, NaiveDateTime, TimeZone, Utc}; use std::fs; +use subxt::{OnlineClient, PolkadotConfig}; + +const BLOCK_TIME_SECONDS: u64 = 6; // Polkadot/Kusama block time in seconds + +#[derive(Debug)] +pub struct TimeConversionError(String); + +impl std::fmt::Display for TimeConversionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for TimeConversionError {} // Check what the user entered for the proposal. If it is just call data, return it back. Otherwise, // we expect a path to a file that contains the call data. Read that in and return it. @@ -13,3 +28,57 @@ pub(crate) fn get_proposal_bytes(proposal: String) -> Vec { return hex::decode(contents.as_str().trim_start_matches("0x")).expect("Valid proposal") } } + +/// Convert a wall clock time string to a block number +/// Format: DD-MM-YYThhmm (e.g. 25-05-21T0800) +pub(crate) async fn wall_clock_to_block_number( + time_str: &str, + network: &str, +) -> Result { + // Parse the input time string + let naive_dt = NaiveDateTime::parse_from_str(time_str, "%d-%m-%yT%H%M") + .map_err(|e| TimeConversionError(format!("Invalid time format: {}", e)))?; + + // Convert to UTC + let local_dt = Local + .from_local_datetime(&naive_dt) + .single() + .ok_or_else(|| TimeConversionError("Invalid local time".to_string()))?; + let utc_dt = local_dt.with_timezone(&Utc); + + // Get current block number and timestamp + let url = match network.to_lowercase().as_str() { + "polkadot" => "wss://polkadot-rpc.dwellir.com:443", + "kusama" => "wss://kusama-rpc.dwellir.com:443", + _ => return Err(TimeConversionError("Invalid network".to_string())), + }; + + let api = OnlineClient::::from_url(url) + .await + .map_err(|e| TimeConversionError(format!("Failed to connect to node: {}", e)))?; + + let current_block = api + .blocks() + .at_latest() + .await + .map_err(|e| TimeConversionError(format!("Failed to get latest block: {}", e)))?; + + let current_block_number = current_block.number(); + let current_timestamp = current_block + .timestamp() + .await + .map_err(|e| TimeConversionError(format!("Failed to get block timestamp: {}", e)))?; + + // Calculate time difference in seconds + let target_timestamp = utc_dt.timestamp() as u64; + let time_diff = if target_timestamp > current_timestamp { + target_timestamp - current_timestamp + } else { + return Err(TimeConversionError("Target time is in the past".to_string())); + }; + + // Calculate block difference + let block_diff = (time_diff + BLOCK_TIME_SECONDS - 1) / BLOCK_TIME_SECONDS; + + Ok(current_block_number + block_diff as u32) +} diff --git a/src/submit_referendum.rs b/src/submit_referendum.rs index 511d130..d530ddd 100644 --- a/src/submit_referendum.rs +++ b/src/submit_referendum.rs @@ -1,3 +1,4 @@ +use crate::functions::wall_clock_to_block_number; use crate::*; use clap::Parser as ClapParser; use std::fs; @@ -18,9 +19,9 @@ pub(crate) struct ReferendumArgs { #[clap(long = "track", short)] track: String, - /// Optional: Enact at a particular block number. + /// Optional: Enact at a particular block number or wall clock time (format: DD-MM-YYThhmm, e.g. 25-05-21T0800) #[clap(long = "at")] - at: Option, + at: Option, /// Optional: Enact after a given number of blocks. #[clap(long = "after")] @@ -42,7 +43,7 @@ pub(crate) struct ReferendumArgs { // The sub-command's "main" function. pub(crate) async fn submit_referendum(prefs: ReferendumArgs) { // Find out what the user wants to do. - let proposal_details = parse_inputs(prefs); + let proposal_details = parse_inputs(prefs).await; // Generate the calls necessary. let calls = generate_calls(&proposal_details).await; // Tell the user what to do. @@ -50,7 +51,7 @@ pub(crate) async fn submit_referendum(prefs: ReferendumArgs) { } // Parse the CLI inputs and return a typed struct with all the details needed. -fn parse_inputs(prefs: ReferendumArgs) -> ProposalDetails { +async fn parse_inputs(prefs: ReferendumArgs) -> ProposalDetails { use DispatchTimeWrapper::*; use NetworkTrack::*; use Output::*; @@ -103,7 +104,19 @@ fn parse_inputs(prefs: ReferendumArgs) -> ProposalDetails { (Some(_), Some(_)) => { panic!("\nBoth `At` and `After` dispatch times provided. You can only use one.\n"); }, - (Some(at), None) => At(at), + (Some(at), None) => { + // Try to parse as block number first + if let Ok(block) = at.parse::() { + At(block) + } else { + // If not a block number, try to parse as wall clock time + let block = wall_clock_to_block_number(&at, &prefs.network) + .await + .expect("Failed to convert wall clock time to block number"); + println!("\nConverted wall clock time {} to block number {}\n", at, block); + At(block) + } + }, (None, Some(after)) => After(after), }; diff --git a/src/types.rs b/src/types.rs index baa72a5..a4dbe0f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -127,7 +127,8 @@ impl Network { PolkadotAssetHub | PolkadotCollectives | PolkadotBridgeHub - | PolkadotPeople | PolkadotCoretime + | PolkadotPeople + | PolkadotCoretime ) } } From d8a9ff2d13be3a8fc88febdfbf8a5b53dacf18a3 Mon Sep 17 00:00:00 2001 From: Eugenio Paluello Date: Mon, 9 Jun 2025 08:51:16 +0200 Subject: [PATCH 2/9] at-date option --- README.md | 2 ++ src/functions.rs | 16 ++++++++++++---- src/submit_referendum.rs | 40 +++++++++++++++++++++------------------- 3 files changed, 35 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index dedd5d6..a1822af 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,8 @@ Options: -t, --track Track on which to submit the referendum --at + Optional: Enact at a particular block number + --at-date Optional: Enact at a particular wall clock time (format: DD-MM-YYThhmm, e.g. 25-05-21T0800) --after Optional: Enact after a given number of blocks diff --git a/src/functions.rs b/src/functions.rs index 5d5e1c9..d2c8cf8 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -1,4 +1,5 @@ -use chrono::{DateTime, Duration, Local, NaiveDateTime, TimeZone, Utc}; +use crate::{kusama_relay, polkadot_relay}; +use chrono::{Local, NaiveDateTime, TimeZone, Utc}; use std::fs; use subxt::{OnlineClient, PolkadotConfig}; @@ -64,10 +65,17 @@ pub(crate) async fn wall_clock_to_block_number( .map_err(|e| TimeConversionError(format!("Failed to get latest block: {}", e)))?; let current_block_number = current_block.number(); - let current_timestamp = current_block - .timestamp() + let current_timestamp = api + .storage() + .at(current_block.hash()) + .fetch(&match network.to_lowercase().as_str() { + "polkadot" => polkadot_relay::storage().timestamp().now(), + "kusama" => kusama_relay::storage().timestamp().now(), + _ => return Err(TimeConversionError("Invalid network".to_string())), + }) .await - .map_err(|e| TimeConversionError(format!("Failed to get block timestamp: {}", e)))?; + .map_err(|e| TimeConversionError(format!("Failed to get block timestamp: {}", e)))? + .ok_or_else(|| TimeConversionError("Failed to get timestamp".to_string()))?; // Calculate time difference in seconds let target_timestamp = utc_dt.timestamp() as u64; diff --git a/src/submit_referendum.rs b/src/submit_referendum.rs index d530ddd..ca92cc5 100644 --- a/src/submit_referendum.rs +++ b/src/submit_referendum.rs @@ -19,9 +19,13 @@ pub(crate) struct ReferendumArgs { #[clap(long = "track", short)] track: String, - /// Optional: Enact at a particular block number or wall clock time (format: DD-MM-YYThhmm, e.g. 25-05-21T0800) + /// Optional: Enact at a particular block number. #[clap(long = "at")] - at: Option, + at: Option, + + /// Optional: Enact at a particular wall clock time (format: DD-MM-YYThhmm, e.g. 25-05-21T0800) + #[clap(long = "at-date")] + at_date: Option, /// Optional: Enact after a given number of blocks. #[clap(long = "after")] @@ -95,29 +99,27 @@ async fn parse_inputs(prefs: ReferendumArgs) -> ProposalDetails { _ => panic!("`network` must be `polkadot` or `kusama`"), }; - let dispatch = match (prefs.at, prefs.after) { - (None, None) => { + let dispatch = match (prefs.at, prefs.at_date, prefs.after) { + (None, None, None) => { println!("\nNo enactment time specified. Defaulting to `After(10)`."); - println!("Specify an enactment time with `--at ` or `--after `.\n"); + println!("Specify an enactment time with `--at `, `--at-date