Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 116 additions & 3 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Options:
Track on which to submit the referendum
--at <AT>
Optional: Enact at a particular block number
--at-date <AT_DATE>
Optional: Enact at a particular wall clock time (format: DD-MM-YYThhmm, e.g. 25-05-21T0800)
Comment thread
joepetrowski marked this conversation as resolved.
--after <AFTER>
Optional: Enact after a given number of blocks
--output-len-limit <OUTPUT_LEN_LIMIT>
Expand Down
77 changes: 77 additions & 0 deletions src/functions.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
use crate::{kusama_relay, polkadot_relay};
use chrono::{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.
Expand All @@ -13,3 +29,64 @@ pub(crate) fn get_proposal_bytes(proposal: String) -> Vec<u8> {
return hex::decode(contents.as_str().trim_start_matches("0x")).expect("Valid proposal")
}
}

/// Convert a wall clock time string to a block number
Comment thread
eugypalu marked this conversation as resolved.
Outdated
/// Format: DD-MM-YYThhmm (e.g. 25-05-21T0800)
pub(crate) async fn wall_clock_to_block_number(
time_str: &str,
network: &str,
) -> Result<u32, TimeConversionError> {
// 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",
Comment thread
joepetrowski marked this conversation as resolved.
Outdated
_ => return Err(TimeConversionError("Invalid network".to_string())),
};

let api = OnlineClient::<PolkadotConfig>::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 = 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)))?
.ok_or_else(|| TimeConversionError("Failed to get timestamp".to_string()))?;

// Calculate time difference in seconds
let target_timestamp = utc_dt.timestamp_millis() 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)
}
31 changes: 23 additions & 8 deletions src/submit_referendum.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::functions::wall_clock_to_block_number;
use crate::*;
use clap::Parser as ClapParser;
use std::fs;
Expand All @@ -22,6 +23,10 @@ pub(crate) struct ReferendumArgs {
#[clap(long = "at")]
at: Option<u32>,

/// Optional: Enact at a particular wall clock time (format: DD-MM-YYThhmm, e.g. 25-05-21T0800)
Comment thread
eugypalu marked this conversation as resolved.
Outdated
#[clap(long = "at-date")]
at_date: Option<String>,

/// Optional: Enact after a given number of blocks.
#[clap(long = "after")]
after: Option<u32>,
Expand All @@ -42,15 +47,15 @@ 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.
deliver_output(proposal_details, calls);
}

// 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::*;
Expand Down Expand Up @@ -94,17 +99,27 @@ 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 <block>` or `--after <blocks>`.\n");
println!("Specify an enactment time with `--at <block>`, `--at-date <time>` or `--after <blocks>`.\n");
Comment thread
eugypalu marked this conversation as resolved.
Outdated
After(10)
},
(Some(_), Some(_)) => {
(Some(_), Some(_), _) => {
panic!("\nBoth `--at` and `--at-date` provided. You can only use one.\n");
},
(Some(_), _, Some(_)) | (_, Some(_), Some(_)) => {
panic!("\nBoth `At` and `After` dispatch times provided. You can only use one.\n");
},
(Some(at), None) => At(at),
(None, Some(after)) => After(after),
(Some(at), None, None) => At(at),
(None, Some(at_date), None) => {
let block = wall_clock_to_block_number(&at_date, &prefs.network)
.await
.expect("Failed to convert wall clock time to block number");
println!("\nConverted wall clock time {} to block number {}\n", at_date, block);
At(block)
},
(None, None, Some(after)) => After(after),
};

let output_len_limit = if let Some(input) = prefs.output_len_limit { input } else { 1_000 };
Expand Down
3 changes: 2 additions & 1 deletion src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ impl Network {
PolkadotAssetHub
| PolkadotCollectives
| PolkadotBridgeHub
| PolkadotPeople | PolkadotCoretime
| PolkadotPeople
| PolkadotCoretime
)
}
}
Expand Down