Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ event-listener = "5.4.2"
# Blockchain deps: align with chain workspace
codec = { package = "parity-scale-codec", version = "3.7", features = ["derive"] }
jsonrpsee = { version = "0.24", features = ["client"] }
# Resolving a metadata type id to its `TypeDef`, to tell a real byte run from a `Vec` of
# structs. subxt re-exports scale-value and scale-decode but not scale-info.
scale-info = { version = "2.11", default-features = false }
sp-core = { version = "39.0.0" }
sp-runtime = { version = "45.0.0" }

Expand Down
101 changes: 101 additions & 0 deletions src/cli/dynamic_decode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//! Helpers for reading chain state that the generated codegen cannot decode.
//!
//! The types in [`crate::chain::quantus_subxt`] are generated from one runtime's
//! metadata. Any storage entry whose type graph reaches a runtime-composed type —
//! `OriginCaller`, `RuntimeCall`, a runtime-local struct — gets a different
//! validation hash on a runtime that composes those differently, and subxt then
//! refuses the read with `Metadata(IncompatibleCodegen)`. That is not a corruption
//! risk (subxt validates before decoding), but it makes the command unusable
//! against any runtime but the one the CLI shipped against.
//!
//! Reading such entries through [`subxt::dynamic`] decodes against the *live*
//! metadata instead. These helpers navigate the resulting [`Value`] by field and
//! variant *name*, so a command keeps working on any runtime that still calls the
//! fields what the pallet upstream calls them.

use crate::error::QuantusError;
use subxt::ext::scale_value::{Composite, Primitive, Value, ValueDef};

/// A dynamically decoded value, as produced by `DecodedValueThunk::to_value`.
pub(crate) type Val = Value<u32>;

/// The variant's name and its payload, or `None` if this is not an enum.
pub(crate) fn variant(v: &Val) -> Option<(&str, &Composite<u32>)> {
match &v.value {
ValueDef::Variant(var) => Some((var.name.as_str(), &var.values)),
_ => None,
}
}

/// The struct/tuple body, or `None` if this is not a composite.
pub(crate) fn composite(v: &Val) -> Option<&Composite<u32>> {
match &v.value {
ValueDef::Composite(c) => Some(c),
_ => None,
}
}

/// Look a field up by name. Unnamed composites have no named fields.
pub(crate) fn field<'a>(c: &'a Composite<u32>, name: &str) -> Option<&'a Val> {
match c {
Composite::Named(fields) => fields.iter().find(|(n, _)| n == name).map(|(_, v)| v),
Composite::Unnamed(_) => None,
}
}

/// Positional access, for tuple variants such as `Ongoing(status)`.
pub(crate) fn nth(c: &Composite<u32>, i: usize) -> Option<&Val> {
match c {
Composite::Named(fields) => fields.get(i).map(|(_, v)| v),
Composite::Unnamed(fields) => fields.get(i),
}
}

/// Any unsigned integer, transparently unwrapping single-field newtypes.
pub(crate) fn uint(v: &Val) -> Option<u128> {
match &v.value {
ValueDef::Primitive(Primitive::U128(n)) => Some(*n),
ValueDef::Composite(c) => nth(c, 0).and_then(uint),
_ => None,
}
}

/// One byte, read strictly. Unlike [`uint`] this does not reach into composites: a
/// `Vec<AccountId32>` must not pass as a byte string by yielding each account's first byte.
pub(crate) fn byte(v: &Val) -> Option<u8> {
match &v.value {
ValueDef::Primitive(Primitive::U128(n)) => u8::try_from(*n).ok(),
_ => None,
}
}

pub(crate) fn boolean(v: &Val) -> Option<bool> {
match &v.value {
ValueDef::Primitive(Primitive::Bool(b)) => Some(*b),
_ => None,
}
}

/// `Some(Some(inner))` / `Some(None)`, or `None` if the value is not an `Option`.
pub(crate) fn option(v: &Val) -> Option<Option<&Val>> {
let (name, inner) = variant(v)?;
match name {
"None" => Some(None),
"Some" => Some(Some(nth(inner, 0)?)),
_ => None,
}
}

/// A uniform error for a field the live runtime does not expose as expected.
pub(crate) fn missing(what: &str) -> QuantusError {
QuantusError::Generic(format!(
"decode: {what} is missing or has an unexpected shape; this runtime's pallet layout is \
not recognized"
))
}

/// Read a named field as a `u32`.
pub(crate) fn u32_field(c: &Composite<u32>, name: &str) -> Result<u32, QuantusError> {
let raw = field(c, name).and_then(uint).ok_or_else(|| missing(name))?;
u32::try_from(raw).map_err(|_| missing(name))
}
14 changes: 4 additions & 10 deletions src/cli/exercise/scenarios/governance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,14 @@ async fn referendum_flow(ctx: &mut ExerciseCtx) -> Result<String> {
.await?;
}

use quantus_subxt::api::runtime_types::pallet_referenda::types::ReferendumInfo;
let info_addr = quantus_subxt::api::storage().tech_referenda().referendum_info_for(index);
use crate::cli::tech_referenda::{fetch_referendum, ReferendumSnapshot};
let latest = ctx.client.get_latest_block().await?;
let info = ctx
.client
.client()
.storage()
.at(latest)
.fetch(&info_addr)
let info = fetch_referendum(&ctx.client, index, latest)
.await?
.ok_or_else(|| QuantusError::Generic(format!("referendum #{index} not found")))?;

match info {
ReferendumInfo::Ongoing(status) => {
ReferendumSnapshot::Ongoing(status) => {
if status.tally.ayes < 3 {
return Err(QuantusError::Generic(format!(
"expected 3 aye votes on referendum #{index}, tally shows {}",
Expand All @@ -112,7 +106,7 @@ async fn referendum_flow(ctx: &mut ExerciseCtx) -> Result<String> {
"referendum #{index} submitted, deposit placed, 3 aye votes tallied (ongoing)"
))
},
ReferendumInfo::Approved(..) => Ok(format!(
ReferendumSnapshot::Approved(..) => Ok(format!(
"referendum #{index} submitted, voted, and already approved (fast-governance node)"
)),
other => Err(QuantusError::Generic(format!(
Expand Down
9 changes: 5 additions & 4 deletions src/cli/exercise/scenarios/preimage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ async fn note_and_verify(ctx: &mut ExerciseCtx) -> Result<String> {
)
.await?;

let status_addr = quantus_subxt::api::storage().preimage().request_status_for(expected_hash);
let latest = ctx.client.get_latest_block().await?;
let status = ctx.client.client().storage().at(latest).fetch(&status_addr).await?;
let status =
crate::cli::preimage::fetch_request_status(&ctx.client, expected_hash, latest).await?;
match status {
Some(_) => Ok(format!("preimage {expected_hash:?} noted and visible in RequestStatusFor")),
None => Err(QuantusError::Generic(format!(
Expand All @@ -54,9 +54,10 @@ async fn note_and_verify(ctx: &mut ExerciseCtx) -> Result<String> {
}

async fn preimage_status_exists(ctx: &ExerciseCtx, hash: sp_core::H256) -> Result<bool> {
let status_addr = quantus_subxt::api::storage().preimage().request_status_for(hash);
let latest = ctx.client.get_latest_block().await?;
Ok(ctx.client.client().storage().at(latest).fetch(&status_addr).await?.is_some())
Ok(crate::cli::preimage::fetch_request_status(&ctx.client, hash, latest)
.await?
.is_some())
}

/// Note a preimage, then clear it with `unnote_preimage` and verify the
Expand Down
10 changes: 4 additions & 6 deletions src/cli/exercise/scenarios/upgrade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,11 @@ async fn cast_collective_ayes(ctx: &ExerciseCtx, index: u32) -> Result<()> {
}

async fn referendum_is_approved(ctx: &ExerciseCtx, index: u32) -> Result<bool> {
use quantus_subxt::api::runtime_types::pallet_referenda::types::ReferendumInfo;
let info_addr = quantus_subxt::api::storage().tech_referenda().referendum_info_for(index);
use crate::cli::tech_referenda::{fetch_referendum, ReferendumSnapshot};
let latest = ctx.client.get_latest_block().await?;
Ok(matches!(
ctx.client.client().storage().at(latest).fetch(&info_addr).await?,
Some(ReferendumInfo::Approved(..))
fetch_referendum(&ctx.client, index, latest).await?,
Some(ReferendumSnapshot::Approved(..))
))
}

Expand All @@ -178,9 +177,8 @@ async fn refund_deposits(ctx: &mut ExerciseCtx, index: u32) -> &'static str {
}

async fn referendum_state(ctx: &ExerciseCtx, index: u32) -> Result<String> {
let info_addr = quantus_subxt::api::storage().tech_referenda().referendum_info_for(index);
let latest = ctx.client.get_latest_block().await?;
let info = ctx.client.client().storage().at(latest).fetch(&info_addr).await?;
let info = crate::cli::tech_referenda::fetch_referendum(&ctx.client, index, latest).await?;
Ok(format!("{info:?}"))
}

Expand Down
1 change: 1 addition & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod batch;
pub mod block;
pub mod cold_signing;
pub mod common;
pub mod dynamic_decode;
pub mod events;
pub mod exercise;
pub mod generic_call;
Expand Down
Loading
Loading