Skip to main content

bittensor_core/
client.rs

1//! Native chain access for the Rust SDK.
2//!
3//! The codec/runtime modules deliberately remain transport agnostic. `Client`
4//! is the small blocking JSON-RPC layer that turns their metadata-driven SCALE
5//! primitives into pinned reads, signed submissions, receipts, and block
6//! streams. It uses the crate's existing rustls-backed `reqwest` dependency, so
7//! the native SDK adds no second async runtime and works in ordinary Rust tests,
8//! command-line programs, and foreign-language bindings alike.
9
10#![allow(clippy::arithmetic_side_effects, clippy::indexing_slicing)]
11
12use std::collections::{BTreeMap, HashMap};
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::sync::{Arc, RwLock};
15use std::thread;
16use std::time::{Duration, Instant};
17
18use codec::{Decode, Encode, MaxEncodedLen};
19use reqwest::blocking::Client as HttpClient;
20use serde_json::{json, Value as JsonValue};
21
22use crate::codec::extrinsic::{era_birth, TxParams};
23use crate::codec::value::Value;
24use crate::error::CoreError;
25use crate::keys::{is_hashed_crypto, is_pq_crypto, is_rotating_crypto, Keypair};
26use crate::mlkem;
27use crate::runtime::type_string::TypeSpec;
28use crate::runtime::{Runtime, RuntimeApiMethodInfo, StorageInfo};
29
30const DEFAULT_RPC_TIMEOUT: Duration = Duration::from_secs(30);
31const DEFAULT_RECEIPT_TIMEOUT: Duration = Duration::from_secs(120);
32const DEFAULT_ERA_PERIOD: u64 = 64;
33// Runtime CheckMortality limits encrypted carriers to eight blocks.
34const SHIELD_ERA_PERIOD: u64 = 8;
35const STORAGE_PAGE_SIZE: u64 = 1_000;
36const RAO_PER_TAO: u128 = 1_000_000_000;
37
38/// A decoded block header.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct BlockHeader {
41    pub hash: String,
42    pub parent_hash: String,
43    pub number: u64,
44}
45
46/// One subnet's small, commonly used read model.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct SubnetInfo {
49    pub netuid: u16,
50    pub tempo: u16,
51    pub burn_rao: u128,
52    pub neuron_count: u16,
53}
54
55/// Swap simulation returned by the runtime API.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct SwapQuote {
58    pub tao_amount: u128,
59    pub alpha_amount: u128,
60    pub tao_fee: u128,
61    pub alpha_fee: u128,
62    pub tao_slippage: u128,
63    pub alpha_slippage: u128,
64}
65
66/// A normalized dispatch failure. `semantic_code` is stable across wording
67/// changes and is suitable for branching in callers and e2e assertions.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct DispatchError {
70    pub pallet: Option<String>,
71    pub name: String,
72    pub docs: Vec<String>,
73    pub semantic_code: String,
74}
75
76impl std::fmt::Display for DispatchError {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        match &self.pallet {
79            Some(pallet) => write!(f, "{pallet}.{}", self.name),
80            None => write!(f, "{}", self.name),
81        }
82    }
83}
84
85/// Inclusion/finalization result for a submitted extrinsic.
86#[derive(Debug, Clone)]
87pub struct TxOutcome {
88    pub success: bool,
89    pub extrinsic_hash: String,
90    pub block_hash: Option<String>,
91    pub block_number: Option<u64>,
92    pub extrinsic_index: Option<u32>,
93    pub fee_rao: Option<u128>,
94    pub events: Vec<Value>,
95    pub error: Option<DispatchError>,
96    pub message: String,
97    pub data: BTreeMap<String, Value>,
98}
99
100impl TxOutcome {
101    fn pool_rejection(hash: String, message: String) -> Self {
102        Self {
103            success: false,
104            extrinsic_hash: hash,
105            block_hash: None,
106            block_number: None,
107            extrinsic_index: None,
108            fee_rao: None,
109            events: Vec::new(),
110            error: None,
111            message,
112            data: BTreeMap::new(),
113        }
114    }
115}
116
117// Reorg-safe receipt tracking states. An inclusion is only final when the exact
118// inclusion block is still canonical after finality reaches its height.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120enum InclusionFinalization {
121    Finalized,
122    Reorged,
123}
124
125fn classify_inclusion_finalization(
126    inclusion_hash: &str,
127    canonical_hash: Option<&str>,
128    included_at: u64,
129    finalized_at: u64,
130) -> Option<InclusionFinalization> {
131    match canonical_hash {
132        Some(canonical) if canonical.eq_ignore_ascii_case(inclusion_hash) => {
133            (finalized_at >= included_at).then_some(InclusionFinalization::Finalized)
134        }
135        _ => Some(InclusionFinalization::Reorged),
136    }
137}
138
139/// The native Bittensor chain client.
140pub struct Client {
141    endpoint: String,
142    http: HttpClient,
143    next_id: AtomicU64,
144    runtime: RwLock<Arc<Runtime>>,
145    genesis_hash: [u8; 32],
146    ss58_format: u16,
147}
148
149/// One immutable authority/nonce selection for an ordinary signature or both
150/// signatures of a Shield submission. Authority is never reloaded between the
151/// encrypted inner proof and its carrier proof.
152struct SigningContext {
153    runtime: Arc<Runtime>,
154    params: TxParams,
155    registered: Option<RegisteredSigningAuthority>,
156}
157
158struct RegisteredSigningAuthority {
159    key: Keypair,
160    generation: u64,
161}
162
163impl Client {
164    /// Connect to a Substrate JSON-RPC endpoint. Websocket URLs are accepted for
165    /// compatibility and are mapped to HTTP on the same host/port.
166    pub fn connect(endpoint: impl Into<String>) -> Result<Self, CoreError> {
167        let endpoint = http_endpoint(&endpoint.into());
168        let http = HttpClient::builder()
169            .timeout(DEFAULT_RPC_TIMEOUT)
170            .build()
171            .map_err(|error| CoreError::Rpc(format!("cannot build HTTP client: {error}")))?;
172        let bootstrap = RpcBootstrap {
173            endpoint: endpoint.clone(),
174            http: http.clone(),
175            next_id: AtomicU64::new(1),
176        };
177        let ss58_format = bootstrap.ss58_format()?;
178        let version = bootstrap.runtime_version()?;
179        let metadata = bootstrap.metadata()?;
180        let genesis_hash = parse_h256(&bootstrap.block_hash(Some(0))?)?;
181        let runtime = Runtime::parse(
182            &metadata,
183            version.spec_version,
184            version.transaction_version,
185            ss58_format,
186        )?;
187        Ok(Self {
188            endpoint,
189            http,
190            next_id: AtomicU64::new(100),
191            runtime: RwLock::new(Arc::new(runtime)),
192            genesis_hash,
193            ss58_format,
194        })
195    }
196
197    pub fn endpoint(&self) -> &str {
198        &self.endpoint
199    }
200
201    /// Stable names of the typed read helpers supplied by the native SDK.
202    /// Generic [`Client::query`], [`Client::query_map`], and
203    /// [`Client::runtime_call`] remain available for the full live metadata
204    /// surface.
205    pub fn read_catalog(&self) -> &'static [&'static str] {
206        &[
207            "balance",
208            "existential_deposit",
209            "subnets",
210            "metagraph",
211            "neurons",
212            "subnet_hyperparameters",
213            "stake",
214            "quote_stake",
215            "block_number",
216            "block_time",
217            "lease",
218            "leases",
219            "mev_shield_next_key",
220            "proxies",
221            "multisig",
222            "identity",
223            "subnet_identity",
224            "children",
225            "root_claim_type",
226            "auto_stake",
227        ]
228    }
229
230    pub fn ss58_format(&self) -> u16 {
231        self.ss58_format
232    }
233
234    pub fn genesis_hash(&self) -> [u8; 32] {
235        self.genesis_hash
236    }
237
238    pub fn runtime(&self) -> Result<Arc<Runtime>, CoreError> {
239        self.runtime
240            .read()
241            .map_err(|_| CoreError::Rpc("runtime metadata lock is poisoned".into()))
242            .map(|runtime| Arc::clone(&runtime))
243    }
244
245    /// Refresh metadata after a runtime upgrade. Returns true when the cached
246    /// runtime changed.
247    pub fn refresh_runtime(&self) -> Result<bool, CoreError> {
248        let version = self.runtime_version()?;
249        let current = self.runtime()?;
250        if current.spec_version == version.spec_version
251            && current.transaction_version == version.transaction_version
252        {
253            return Ok(false);
254        }
255        let metadata = self.metadata()?;
256        let next = Runtime::parse(
257            &metadata,
258            version.spec_version,
259            version.transaction_version,
260            self.ss58_format,
261        )?;
262        let mut guard = self
263            .runtime
264            .write()
265            .map_err(|_| CoreError::Rpc("runtime metadata lock is poisoned".into()))?;
266        *guard = Arc::new(next);
267        Ok(true)
268    }
269
270    /// Escape hatch for node RPCs not yet wrapped by the SDK.
271    pub fn rpc_value(&self, method: &str, params: JsonValue) -> Result<JsonValue, CoreError> {
272        rpc_request(
273            &self.http,
274            &self.endpoint,
275            self.next_id.fetch_add(1, Ordering::Relaxed),
276            method,
277            params,
278        )
279    }
280
281    fn runtime_version(&self) -> Result<RuntimeVersion, CoreError> {
282        parse_runtime_version(self.rpc_value("state_getRuntimeVersion", json!([]))?)
283    }
284
285    fn metadata(&self) -> Result<Vec<u8>, CoreError> {
286        fetch_metadata(|method, params| self.rpc_value(method, params))
287    }
288
289    pub fn block_hash(&self, block: Option<u64>) -> Result<String, CoreError> {
290        let params = block.map_or_else(|| json!([]), |number| json!([number]));
291        let value = self.rpc_value("chain_getBlockHash", params)?;
292        Ok(json_string(&value, "chain_getBlockHash result")?.to_string())
293    }
294
295    fn canonical_block_hash(&self, block: u64) -> Result<Option<String>, CoreError> {
296        let value = self.rpc_value("chain_getBlockHash", json!([block]))?;
297        match value {
298            JsonValue::Null => Ok(None),
299            JsonValue::String(hash) => Ok(Some(hash)),
300            other => Err(CoreError::Rpc(format!(
301                "chain_getBlockHash returned {other}, expected a hash or null"
302            ))),
303        }
304    }
305
306    pub fn finalized_head(&self) -> Result<String, CoreError> {
307        let value = self.rpc_value("chain_getFinalizedHead", json!([]))?;
308        Ok(json_string(&value, "chain_getFinalizedHead result")?.to_string())
309    }
310
311    pub fn header(&self, block_hash: Option<&str>) -> Result<BlockHeader, CoreError> {
312        let params = block_hash.map_or_else(|| json!([]), |hash| json!([hash]));
313        let value = self.rpc_value("chain_getHeader", params)?;
314        let (number, parent_hash) = parse_header_fields(&value)?;
315        let hash = match block_hash {
316            Some(hash) => hash.to_string(),
317            None => self.block_hash(Some(number))?,
318        };
319        Ok(BlockHeader {
320            hash,
321            parent_hash,
322            number,
323        })
324    }
325
326    /// Best-head poll for [`BlockStream`]: fetch the canonical hash (a second
327    /// RPC call) only when the head has advanced past `last`.
328    fn best_header_past(&self, last: Option<u64>) -> Result<Option<BlockHeader>, CoreError> {
329        let value = self.rpc_value("chain_getHeader", json!([]))?;
330        let (number, parent_hash) = parse_header_fields(&value)?;
331        if last.is_some_and(|last| number <= last) {
332            return Ok(None);
333        }
334        let hash = self.block_hash(Some(number))?;
335        Ok(Some(BlockHeader {
336            hash,
337            parent_hash,
338            number,
339        }))
340    }
341
342    pub fn block_number(&self) -> Result<u64, CoreError> {
343        self.header(None).map(|header| header.number)
344    }
345
346    pub fn block_time(&self) -> Result<Duration, CoreError> {
347        let millis = as_u128(&self.constant("Aura", "SlotDuration")?)
348            .ok_or_else(|| CoreError::Codec("Aura.SlotDuration is not an integer".into()))?;
349        let millis = u64::try_from(millis)
350            .map_err(|_| CoreError::Codec("Aura.SlotDuration does not fit u64".into()))?;
351        Ok(Duration::from_millis(millis.max(1)))
352    }
353
354    /// Polling block stream. HTTP and websocket endpoints therefore expose the
355    /// same API; callers do not need an async runtime merely to follow heads.
356    ///
357    /// The poll interval adapts to the chain's slot duration (a quarter of a
358    /// slot, clamped to 50ms..3s): an idle mainnet follower costs the node
359    /// about one cheap RPC call every 3 seconds, while fast-blocks local
360    /// chains are still polled every ~62ms.
361    pub fn blocks(&self, finalized: bool) -> BlockStream<'_> {
362        let poll_interval = (self.block_time().unwrap_or_else(|_| Duration::from_secs(1)) / 4)
363            .clamp(Duration::from_millis(50), Duration::from_secs(3));
364        BlockStream {
365            client: self,
366            finalized,
367            last_number: None,
368            last_finalized_hash: None,
369            poll_interval,
370        }
371    }
372
373    pub fn at(&self, block: Option<u64>) -> Result<Snapshot<'_>, CoreError> {
374        let hash = self.block_hash(block)?;
375        let number = self.header(Some(&hash))?.number;
376        Ok(Snapshot {
377            client: self,
378            block_hash: hash,
379            block_number: number,
380        })
381    }
382
383    pub fn compose_call(
384        &self,
385        pallet: &str,
386        function: &str,
387        params: &Value,
388    ) -> Result<Vec<u8>, CoreError> {
389        self.runtime()?.compose_call(pallet, function, params)
390    }
391
392    pub fn decode_call(&self, data: &[u8]) -> Result<Value, CoreError> {
393        self.runtime()?.decode_spec(&TypeSpec::Call, data, true)
394    }
395
396    pub fn decode_scale(&self, type_name: &str, data: &[u8]) -> Result<Value, CoreError> {
397        let runtime = self.runtime()?;
398        if type_name == "Call" {
399            return runtime.decode_spec(&TypeSpec::Call, data, true);
400        }
401        let type_id = runtime
402            .type_id_of(type_name)
403            .ok_or_else(|| CoreError::NotInRuntime(format!("type {type_name}")))?;
404        runtime.decode_spec(&TypeSpec::Id(type_id), data, true)
405    }
406
407    pub fn constant(&self, pallet: &str, name: &str) -> Result<Value, CoreError> {
408        let runtime = self.runtime()?;
409        let info = runtime
410            .constant(pallet, name)
411            .cloned()
412            .ok_or_else(|| CoreError::NotInRuntime(format!("constant {pallet}.{name}")))?;
413        runtime.decode_spec(&TypeSpec::Id(info.ty), &info.value, true)
414    }
415
416    pub fn query(
417        &self,
418        pallet: &str,
419        storage: &str,
420        params: &[Value],
421        block_hash: Option<&str>,
422    ) -> Result<Value, CoreError> {
423        self.query_with_runtime(
424            self.runtime()?.as_ref(),
425            pallet,
426            storage,
427            params,
428            block_hash,
429        )
430    }
431
432    fn query_with_runtime(
433        &self,
434        runtime: &Runtime,
435        pallet: &str,
436        storage: &str,
437        params: &[Value],
438        block_hash: Option<&str>,
439    ) -> Result<Value, CoreError> {
440        let info = storage_info(runtime, pallet, storage)?;
441        let key = runtime.storage_key(&info, params)?;
442        let raw = self.storage_raw(&key, block_hash)?;
443        decode_storage(runtime, &info, raw.as_deref())
444    }
445
446    pub fn query_batch(
447        &self,
448        pallet: &str,
449        storage: &str,
450        param_sets: &[Vec<Value>],
451        block_hash: Option<&str>,
452    ) -> Result<Vec<Value>, CoreError> {
453        if param_sets.is_empty() {
454            return Ok(Vec::new());
455        }
456        let runtime = self.runtime()?;
457        let info = storage_info(&runtime, pallet, storage)?;
458        let mut keys = Vec::with_capacity(param_sets.len());
459        for params in param_sets {
460            keys.push(runtime.storage_key(&info, params)?);
461        }
462        let values = self.storage_batch_raw(&keys, block_hash)?;
463        values
464            .iter()
465            .map(|raw| decode_storage(&runtime, &info, raw.as_deref()))
466            .collect()
467    }
468
469    pub fn query_map(
470        &self,
471        pallet: &str,
472        storage: &str,
473        fixed_params: &[Value],
474        block_hash: Option<&str>,
475    ) -> Result<Vec<(Value, Value)>, CoreError> {
476        let runtime = self.runtime()?;
477        let info = storage_info(&runtime, pallet, storage)?;
478        if fixed_params.len() > info.key_types.len() {
479            return Err(CoreError::Codec(format!(
480                "{pallet}.{storage} has {} key parts, got {} fixed params",
481                info.key_types.len(),
482                fixed_params.len()
483            )));
484        }
485        let prefix = runtime.storage_key(&info, fixed_params)?;
486        let keys = self.storage_keys_paged(&prefix, block_hash)?;
487        let raws = self.storage_batch_raw(&keys, block_hash)?;
488        let mut rows = Vec::with_capacity(keys.len());
489        for (key, raw) in keys.iter().zip(raws.iter()) {
490            let decoded_key = runtime.decode_storage_key_params(&info, key, fixed_params.len())?;
491            let key_value = match decoded_key.as_slice() {
492                [] => Value::Tuple(Vec::new()),
493                [one] => one.clone(),
494                many => Value::Tuple(many.to_vec()),
495            };
496            rows.push((key_value, decode_storage(&runtime, &info, raw.as_deref())?));
497        }
498        Ok(rows)
499    }
500
501    pub fn runtime_call(
502        &self,
503        api: &str,
504        method: &str,
505        params: &[Value],
506        block_hash: Option<&str>,
507    ) -> Result<Value, CoreError> {
508        let runtime = self.runtime()?;
509        let method_info = runtime_api_method(&runtime, api, method)?.clone();
510        if method_info.inputs.len() != params.len() {
511            return Err(CoreError::Codec(format!(
512                "{api}.{method} expects {} params, got {}",
513                method_info.inputs.len(),
514                params.len()
515            )));
516        }
517        let mut encoded = Vec::new();
518        for (input, value) in method_info.inputs.iter().zip(params.iter()) {
519            runtime.encode_id(input.ty, value, &mut encoded)?;
520        }
521        let mut rpc_params = vec![
522            JsonValue::String(format!("{api}_{method}")),
523            JsonValue::String(hex_prefixed(&encoded)),
524        ];
525        if let Some(hash) = block_hash {
526            rpc_params.push(JsonValue::String(hash.to_string()));
527        }
528        let result = self.rpc_value("state_call", JsonValue::Array(rpc_params))?;
529        let bytes = decode_hex(json_string(&result, "state_call result")?)?;
530        runtime.decode_spec(&TypeSpec::Id(method_info.output), &bytes, true)
531    }
532
533    pub fn account_next_index(&self, address: &str) -> Result<u64, CoreError> {
534        let value = self.rpc_value("system_accountNextIndex", json!([address]))?;
535        json_u64(&value)
536    }
537
538    /// Sign without submitting. Returns `(encoded extrinsic, 0x hash)`.
539    /// Registered accounts require the current confirmed nonce and no pending
540    /// transaction. ML-DSA authority is selected from finalized state.
541    pub fn sign_extrinsic(
542        &self,
543        call_data: &[u8],
544        signer: &Keypair,
545        nonce: u64,
546        period: Option<u64>,
547    ) -> Result<(Vec<u8>, String), CoreError> {
548        let context = self.prepare_signing(signer, Some(nonce), period, false)?;
549        self.sign_prepared(call_data, signer, &context, 0)
550    }
551
552    fn prepare_signing(
553        &self,
554        signer: &Keypair,
555        requested_nonce: Option<u64>,
556        period: Option<u64>,
557        shielded: bool,
558    ) -> Result<SigningContext, CoreError> {
559        let runtime = self.runtime()?;
560        let mut registered = None;
561        let mut authority_block = None;
562        let stateless = crate::keys::is_fixed_pq_crypto(signer.crypto_type());
563        let nonce = if stateless {
564            if !runtime.supports_mldsa_profile(signer.mldsa_profile()?) {
565                return Err(CoreError::Policy(
566                    "runtime does not support registration-free post-quantum accounts".into(),
567                ));
568            }
569            let state_hash = self.finalized_head()?;
570            if self.query_with_runtime(
571                &runtime,
572                "AdminUtils",
573                "HashedAccountsEnabled",
574                &[],
575                Some(&state_hash),
576            )? != Value::Bool(true)
577            {
578                return Err(CoreError::Policy(
579                    "post-quantum authorization is not active".into(),
580                ));
581            }
582            authority_block = Some(state_hash);
583            match requested_nonce {
584                Some(nonce) => nonce,
585                None => self.account_next_index(&signer.ss58_address())?,
586            }
587        } else if is_rotating_crypto(signer.crypto_type()) {
588            let mldsa = is_pq_crypto(signer.crypto_type());
589            if mldsa && !runtime.supports_mldsa_profile(signer.mldsa_descriptor()?[1]) {
590                return Err(CoreError::Policy(
591                    "runtime does not support post-quantum accounts".into(),
592                ));
593            }
594            let state_hash = if shielded || mldsa {
595                self.finalized_head()?
596            } else {
597                self.block_hash(None)?
598            };
599            let query = |pallet, storage, params: &[Value]| {
600                self.query_with_runtime(&runtime, pallet, storage, params, Some(&state_hash))
601            };
602            if query("AdminUtils", "HashedAccountsEnabled", &[])? != Value::Bool(true) {
603                return Err(CoreError::Policy(
604                    "hashed account authorization is not active".into(),
605                ));
606            }
607            let address = [Value::str(signer.ss58_address())];
608            let record = query(
609                "HashedAccounts",
610                if mldsa { "MlDsaAccounts" } else { "Accounts" },
611                &address,
612            )?;
613            if mldsa {
614                let descriptor = field(&record, "descriptor")
615                    .ok_or_else(|| CoreError::Codec("post-quantum descriptor missing".into()))?;
616                let expected = signer.mldsa_descriptor()?;
617                if field(descriptor, "version").and_then(as_u128) != Some(u128::from(expected[0]))
618                    || field(descriptor, "algorithm").and_then(as_u128)
619                        != Some(u128::from(expected[1]))
620                    || field(descriptor, "initial_commitment")
621                        .and_then(value_bytes)
622                        .as_deref()
623                        != Some(&expected[2..])
624                {
625                    return Err(CoreError::Policy(
626                        "post-quantum descriptor does not match this wallet".into(),
627                    ));
628                }
629            }
630            let generation = field(&record, "generation")
631                .and_then(as_u128)
632                .and_then(|value| u64::try_from(value).ok())
633                .ok_or_else(|| {
634                    CoreError::Policy("hashed account must be registered before use".into())
635                })?;
636            let active = signer.at_generation(generation)?;
637            let commitment = field(&record, "commitment")
638                .and_then(value_bytes)
639                .ok_or_else(|| CoreError::Codec("hashed account commitment missing".into()))?;
640            if commitment != active.authorization_current_commitment()? {
641                return Err(CoreError::Policy(
642                    "hashed account commitment does not match this mnemonic".into(),
643                ));
644            }
645            // Nonce and authority must come from the same state. A pool nonce
646            // cannot be combined with an older finalized signing generation.
647            let account = query("System", "Account", &address)?;
648            let confirmed_nonce = field(&account, "nonce")
649                .and_then(as_u128)
650                .and_then(|value| u64::try_from(value).ok())
651                .ok_or_else(|| CoreError::Codec("System.Account.nonce is missing".into()))?;
652            self.ensure_registered_nonce(signer, confirmed_nonce)?;
653            if requested_nonce.is_some_and(|nonce| nonce != confirmed_nonce) {
654                return Err(CoreError::Policy(
655                    "hashed accounts must use the current on-chain nonce; refresh and sign again"
656                        .into(),
657                ));
658            }
659            registered = Some(RegisteredSigningAuthority {
660                key: active,
661                generation,
662            });
663            authority_block = Some(state_hash);
664            confirmed_nonce
665        } else {
666            match requested_nonce {
667                Some(nonce) => nonce,
668                None => self.account_next_index(&signer.ss58_address())?,
669            }
670        };
671        let params = match authority_block {
672            Some(hash) => {
673                let current = self.header(Some(&hash))?.number;
674                self.transaction_params_at(nonce, period, current)?
675            }
676            None => self.transaction_params(nonce, period)?,
677        };
678        Ok(SigningContext {
679            runtime,
680            params,
681            registered,
682        })
683    }
684
685    fn ensure_registered_nonce(
686        &self,
687        signer: &Keypair,
688        confirmed_nonce: u64,
689    ) -> Result<(), CoreError> {
690        if self.account_next_index(&signer.ss58_address())? != confirmed_nonce {
691            return Err(CoreError::Policy(
692                "hashed account has a pending transaction; wait for finalization before signing again"
693                    .into(),
694            ));
695        }
696        Ok(())
697    }
698
699    /// Only the encrypted inner transaction may use offset one: its carrier
700    /// consumes the selected generation and nonce before the inner executes.
701    fn sign_prepared(
702        &self,
703        call_data: &[u8],
704        signer: &Keypair,
705        context: &SigningContext,
706        generation_offset: u64,
707    ) -> Result<(Vec<u8>, String), CoreError> {
708        if generation_offset > 1 {
709            return Err(CoreError::Policy("invalid Shield generation offset".into()));
710        }
711        let params = TxParams {
712            era: context.params.era.clone(),
713            nonce: context
714                .params
715                .nonce
716                .checked_add(generation_offset)
717                .ok_or_else(|| CoreError::Policy("account nonce exhausted".into()))?,
718            tip: context.params.tip,
719            tip_asset_id: context.params.tip_asset_id,
720            genesis_hash: context.params.genesis_hash,
721            era_block_hash: context.params.era_block_hash,
722            metadata_hash: context.params.metadata_hash,
723        };
724        if crate::keys::is_fixed_pq_crypto(signer.crypto_type())
725            && params.nonce >= u64::from(u32::MAX)
726        {
727            return Err(CoreError::Policy("account nonce exhausted".into()));
728        }
729        let runtime = &context.runtime;
730        let signature = if let Some(authority) = &context.registered {
731            // Also check immediately before each proof. A concurrent pending or
732            // newly finalized transaction invalidates the whole prepared pair;
733            // never refresh only the carrier to a newer generation.
734            self.ensure_registered_nonce(signer, context.params.nonce)?;
735            let generation = authority
736                .generation
737                .checked_add(generation_offset)
738                .ok_or_else(|| CoreError::Policy("hashed signing generation exhausted".into()))?;
739            let selected = authority.key.at_generation(generation)?;
740            let (extra, additional) = runtime.signature_payload_parts(&params)?;
741            let mut payload = call_data.to_vec();
742            payload.extend_from_slice(&extra);
743            payload.extend_from_slice(&additional);
744            selected.sign_authorization(&payload)?
745        } else if crate::keys::is_fixed_pq_crypto(signer.crypto_type()) {
746            // All account/chain/call/nonce fields are authenticated in the full
747            // transcript, including Shield's inner nonce offset. The fixed key
748            // has no on-chain generation to reload between the two signatures.
749            let (extra, additional) = runtime.signature_payload_parts(&params)?;
750            let mut payload = call_data.to_vec();
751            payload.extend_from_slice(&extra);
752            payload.extend_from_slice(&additional);
753            signer.sign_authorization(&payload)?
754        } else {
755            let payload = runtime.signature_payload(call_data, &params)?;
756            signer.sign(&payload)?
757        };
758        let (extrinsic, hash) = runtime.encode_signed_extrinsic(
759            call_data,
760            signer.public_key_bytes(),
761            &signature,
762            signer.crypto_type(),
763            &params,
764        )?;
765        Ok((extrinsic, hex_prefixed(&hash)))
766    }
767
768    fn transaction_params(&self, nonce: u64, period: Option<u64>) -> Result<TxParams, CoreError> {
769        let current = self.block_number()?;
770        self.transaction_params_at(nonce, period, current)
771    }
772
773    fn transaction_params_at(
774        &self,
775        nonce: u64,
776        period: Option<u64>,
777        current: u64,
778    ) -> Result<TxParams, CoreError> {
779        let (era, era_block_hash) = match period {
780            Some(period) if period > 0 => {
781                let birth = era_birth(period, current);
782                let hash = parse_h256(&self.block_hash(Some(birth))?)?;
783                (
784                    Value::record(vec![
785                        ("period".into(), Value::Uint(u128::from(period))),
786                        ("current".into(), Value::Uint(u128::from(current))),
787                    ]),
788                    hash,
789                )
790            }
791            _ => (Value::str("00"), self.genesis_hash),
792        };
793        Ok(TxParams {
794            era,
795            nonce,
796            tip: 0,
797            tip_asset_id: None,
798            genesis_hash: self.genesis_hash,
799            era_block_hash,
800            metadata_hash: None,
801        })
802    }
803
804    /// Quotes only decode the envelope: never disclose an active signing key
805    /// or produce a reusable authorization for an operation the user may cancel.
806    fn payment_info(&self, call_data: &[u8], signer: &Keypair) -> Result<JsonValue, CoreError> {
807        let runtime = self.runtime()?;
808        let nonce = self.account_next_index(&signer.ss58_address())?;
809        let params = self.transaction_params(nonce, Some(DEFAULT_ERA_PERIOD))?;
810        let proof_len = if is_hashed_crypto(signer.crypto_type()) {
811            subtensor_hashed::Proof::max_encoded_len()
812        } else {
813            64
814        };
815        let proof = if crate::keys::is_falcon_crypto(signer.crypto_type()) {
816            let profile = crate::keys::pq_profile(signer.crypto_type())
817                .ok_or_else(|| CoreError::Codec("invalid Falcon crypto type".into()))?;
818            subtensor_hashed::falcon::SignerAddress {
819                account: signer.public_key_bytes(),
820                version: subtensor_hashed::falcon::VERSION,
821                algorithm: profile,
822                generation: 0,
823                public_key: vec![
824                    0;
825                    subtensor_hashed::falcon::public_key_len(profile).ok_or_else(
826                        || CoreError::Codec("invalid Falcon profile".into())
827                    )?
828                ],
829                next_commitment: [1; 64],
830                signature: vec![
831                    0;
832                    subtensor_hashed::falcon::signature_len(profile).ok_or_else(
833                        || CoreError::Codec("invalid Falcon profile".into())
834                    )?
835                ],
836            }
837            .encode()
838        } else if is_pq_crypto(signer.crypto_type()) {
839            subtensor_hashed::mldsa::SignerAddress {
840                account: signer.public_key_bytes(),
841                version: subtensor_hashed::mldsa::VERSION,
842                algorithm: if signer.crypto_type() == crate::keys::CRYPTO_MLDSA65 {
843                    subtensor_hashed::mldsa::ALGORITHM
844                } else {
845                    subtensor_hashed::mldsa::FIXED_ALGORITHM
846                },
847                generation: 0,
848                public_key: [0; subtensor_hashed::mldsa::PUBLIC_KEY_LEN],
849                next_commitment: [1; 64],
850                signature: [0; subtensor_hashed::mldsa::SIGNATURE_LEN],
851            }
852            .encode()
853        } else {
854            vec![0; proof_len]
855        };
856        let (extrinsic, _) = runtime.encode_signed_extrinsic(
857            call_data,
858            signer.public_key_bytes(),
859            &proof,
860            signer.crypto_type(),
861            &params,
862        )?;
863        self.rpc_value("payment_queryInfo", json!([hex_prefixed(&extrinsic)]))
864    }
865
866    pub fn estimate_fee(&self, call_data: &[u8], signer: &Keypair) -> Result<u128, CoreError> {
867        let value = self.payment_info(call_data, signer)?;
868        let object = value
869            .as_object()
870            .ok_or_else(|| CoreError::Rpc("payment_queryInfo returned a non-object".into()))?;
871        let fee = object
872            .get("partialFee")
873            .or_else(|| object.get("partial_fee"))
874            .ok_or_else(|| CoreError::Rpc("payment_queryInfo omitted partialFee".into()))?;
875        json_u128(fee)
876    }
877
878    pub fn estimate_weight(&self, call_data: &[u8], signer: &Keypair) -> Result<Value, CoreError> {
879        let value = self.payment_info(call_data, signer)?;
880        let weight = value
881            .as_object()
882            .and_then(|object| object.get("weight"))
883            .ok_or_else(|| CoreError::Rpc("payment_queryInfo omitted weight".into()))?;
884        json_to_value(weight)
885    }
886
887    pub fn submit(
888        &self,
889        call_data: &[u8],
890        signer: &Keypair,
891        nonce: Option<u64>,
892        period: Option<u64>,
893        wait_for_finalization: bool,
894    ) -> Result<TxOutcome, CoreError> {
895        let context = self.prepare_signing(signer, nonce, period, false)?;
896        let (extrinsic, hash) = self.sign_prepared(call_data, signer, &context, 0)?;
897        self.submit_encoded(&extrinsic, hash, wait_for_finalization)
898    }
899
900    pub fn submit_encoded(
901        &self,
902        extrinsic: &[u8],
903        expected_hash: String,
904        wait_for_finalization: bool,
905    ) -> Result<TxOutcome, CoreError> {
906        let start_block = self.block_number()?;
907        let xt_hex = hex_prefixed(extrinsic);
908        let submitted = match self.rpc_value("author_submitExtrinsic", json!([xt_hex])) {
909            Ok(value) => value,
910            Err(error) => {
911                return Ok(TxOutcome::pool_rejection(expected_hash, error.to_string()));
912            }
913        };
914        let submitted_hash = submitted
915            .as_str()
916            .map_or(expected_hash, ToString::to_string);
917        self.wait_for_inclusion(
918            extrinsic,
919            submitted_hash,
920            start_block,
921            wait_for_finalization,
922            DEFAULT_RECEIPT_TIMEOUT,
923        )
924    }
925
926    /// Full MEV-shield pipeline: sign the inner call at nonce+1, seal it with
927    /// the current ML-KEM-768 key, then submit the carrier at nonce. The result
928    /// describes the decrypted call and sums both fees; `data` retains the
929    /// carrier hash and each reported fee separately.
930    pub fn submit_shielded(
931        &self,
932        call_data: &[u8],
933        signer: &Keypair,
934        wait_for_finalization: bool,
935    ) -> Result<TxOutcome, CoreError> {
936        let next_key = self.query("MevShield", "NextKey", &[], None)?;
937        let public_key = value_bytes(&next_key)
938            .ok_or_else(|| CoreError::Rpc("MevShield.NextKey is unavailable".into()))?;
939        let context = self.prepare_signing(signer, None, Some(SHIELD_ERA_PERIOD), true)?;
940        let (inner, inner_hash) = self.sign_prepared(call_data, signer, &context, 1)?;
941        let ciphertext = mlkem::seal(&public_key, &inner, true)?;
942        let outer = context.runtime.compose_call(
943            "MevShield",
944            "submit_encrypted",
945            &Value::record(vec![("ciphertext".into(), Value::Bytes(ciphertext))]),
946        )?;
947        let (extrinsic, hash) = self.sign_prepared(&outer, signer, &context, 0)?;
948        let carrier = self.submit_encoded(&extrinsic, hash, wait_for_finalization)?;
949        self.resolve_shielded_inner(
950            &inner,
951            inner_hash,
952            carrier,
953            wait_for_finalization,
954            DEFAULT_RECEIPT_TIMEOUT,
955        )
956    }
957
958    fn resolve_shielded_inner(
959        &self,
960        inner: &[u8],
961        inner_hash: String,
962        carrier: TxOutcome,
963        wait_for_finalization: bool,
964        timeout: Duration,
965    ) -> Result<TxOutcome, CoreError> {
966        if !carrier.success {
967            return Ok(carrier);
968        }
969        let start_block = carrier
970            .block_number
971            .ok_or_else(|| CoreError::Rpc("successful Shield carrier omitted its block".into()))?;
972        // The carrier only accepts ciphertext. Report the requested call's
973        // dispatch result, including nested failures and missing inner inclusion.
974        let mut result = self.wait_for_inclusion(
975            inner,
976            inner_hash.clone(),
977            start_block,
978            wait_for_finalization,
979            timeout,
980        )?;
981        result.data.insert("shielded".into(), Value::Bool(true));
982        result
983            .data
984            .insert("inner_extrinsic_hash".into(), Value::str(inner_hash));
985        result.data.insert(
986            "carrier_extrinsic_hash".into(),
987            Value::str(carrier.extrinsic_hash),
988        );
989        if let Some(fee) = result.fee_rao {
990            result.data.insert("inner_fee_rao".into(), Value::Uint(fee));
991        }
992        if let Some(fee) = carrier.fee_rao {
993            result
994                .data
995                .insert("carrier_fee_rao".into(), Value::Uint(fee));
996            result.fee_rao = Some(
997                fee.checked_add(result.fee_rao.unwrap_or_default())
998                    .ok_or_else(|| CoreError::Codec("Shield total fee exceeds u128".into()))?,
999            );
1000        }
1001        Ok(result)
1002    }
1003
1004    pub fn balance_rao(&self, address: &str) -> Result<u128, CoreError> {
1005        let account = self.query("System", "Account", &[Value::str(address)], None)?;
1006        field(&account, "data")
1007            .and_then(|data| field(data, "free"))
1008            .and_then(as_u128)
1009            .ok_or_else(|| CoreError::Codec("System.Account.data.free is missing".into()))
1010    }
1011
1012    pub fn existential_deposit_rao(&self) -> Result<u128, CoreError> {
1013        as_u128(&self.constant("Balances", "ExistentialDeposit")?)
1014            .ok_or_else(|| CoreError::Codec("ExistentialDeposit is not an integer".into()))
1015    }
1016
1017    pub fn subnets(&self, block_hash: Option<&str>) -> Result<Vec<SubnetInfo>, CoreError> {
1018        let added = self.query_map("SubtensorModule", "NetworksAdded", &[], block_hash)?;
1019        let tempos = value_map_u16(self.query_map("SubtensorModule", "Tempo", &[], block_hash)?);
1020        let burns = value_map_u128(self.query_map("SubtensorModule", "Burn", &[], block_hash)?);
1021        let counts =
1022            value_map_u16(self.query_map("SubtensorModule", "SubnetworkN", &[], block_hash)?);
1023        let mut out = Vec::new();
1024        for (key, value) in added {
1025            if !matches!(value, Value::Bool(true)) {
1026                continue;
1027            }
1028            let Some(netuid) = as_u128(&key).and_then(|n| u16::try_from(n).ok()) else {
1029                continue;
1030            };
1031            out.push(SubnetInfo {
1032                netuid,
1033                tempo: tempos.get(&netuid).copied().unwrap_or(0),
1034                burn_rao: burns.get(&netuid).copied().unwrap_or(0),
1035                neuron_count: counts.get(&netuid).copied().unwrap_or(0),
1036            });
1037        }
1038        out.sort_by_key(|subnet| subnet.netuid);
1039        Ok(out)
1040    }
1041
1042    pub fn metagraph(&self, netuid: u16, block_hash: Option<&str>) -> Result<Value, CoreError> {
1043        self.runtime_call(
1044            "SubnetInfoRuntimeApi",
1045            "get_metagraph",
1046            &[Value::Uint(u128::from(netuid))],
1047            block_hash,
1048        )
1049    }
1050
1051    /// Typed fast-path for the runtime's lite-neuron collection.
1052    pub fn neurons(&self, netuid: u16, block_hash: Option<&str>) -> Result<Vec<Value>, CoreError> {
1053        match self.runtime_call(
1054            "NeuronInfoRuntimeApi",
1055            "get_neurons_lite",
1056            &[Value::Uint(u128::from(netuid))],
1057            block_hash,
1058        )? {
1059            Value::List(neurons) => Ok(neurons),
1060            other => Err(CoreError::Codec(format!(
1061                "NeuronInfoRuntimeApi.get_neurons_lite returned {other:?}, expected a list"
1062            ))),
1063        }
1064    }
1065
1066    pub fn subnet_hyperparameters(
1067        &self,
1068        netuid: u16,
1069        block_hash: Option<&str>,
1070    ) -> Result<Value, CoreError> {
1071        let entries = self.runtime_call(
1072            "SubnetInfoRuntimeApi",
1073            "get_subnet_hyperparams_v3",
1074            &[Value::Uint(u128::from(netuid))],
1075            block_hash,
1076        )?;
1077        flatten_subnet_hyperparams_v3(entries)
1078    }
1079
1080    pub fn stake_rao(
1081        &self,
1082        coldkey: &str,
1083        hotkey: &str,
1084        netuid: u16,
1085        block_hash: Option<&str>,
1086    ) -> Result<u128, CoreError> {
1087        let info = self.runtime_call(
1088            "StakeInfoRuntimeApi",
1089            "get_stake_info_for_hotkey_coldkey_netuid",
1090            &[
1091                Value::str(hotkey),
1092                Value::str(coldkey),
1093                Value::Uint(u128::from(netuid)),
1094            ],
1095            block_hash,
1096        )?;
1097        if matches!(info, Value::Null) {
1098            return Ok(0);
1099        }
1100        field(&info, "stake")
1101            .and_then(as_u128)
1102            .ok_or_else(|| CoreError::Codec("stake runtime API omitted stake".into()))
1103    }
1104
1105    pub fn quote_stake(
1106        &self,
1107        netuid: u16,
1108        amount_rao: u128,
1109        block_hash: Option<&str>,
1110    ) -> Result<SwapQuote, CoreError> {
1111        let quote = self.runtime_call(
1112            "SwapRuntimeApi",
1113            "sim_swap_tao_for_alpha",
1114            &[Value::Uint(u128::from(netuid)), Value::Uint(amount_rao)],
1115            block_hash,
1116        )?;
1117        Ok(SwapQuote {
1118            tao_amount: required_u128(&quote, "tao_amount")?,
1119            alpha_amount: required_u128(&quote, "alpha_amount")?,
1120            tao_fee: required_u128(&quote, "tao_fee")?,
1121            alpha_fee: required_u128(&quote, "alpha_fee")?,
1122            tao_slippage: required_u128(&quote, "tao_slippage")?,
1123            alpha_slippage: required_u128(&quote, "alpha_slippage")?,
1124        })
1125    }
1126
1127    pub fn tao(value: u128) -> Result<u128, CoreError> {
1128        value
1129            .checked_mul(RAO_PER_TAO)
1130            .ok_or_else(|| CoreError::Codec("TAO amount overflows u128".into()))
1131    }
1132
1133    fn storage_raw(
1134        &self,
1135        key: &[u8],
1136        block_hash: Option<&str>,
1137    ) -> Result<Option<String>, CoreError> {
1138        let mut params = vec![JsonValue::String(hex_prefixed(key))];
1139        if let Some(hash) = block_hash {
1140            params.push(JsonValue::String(hash.to_string()));
1141        }
1142        let value = self.rpc_value("state_getStorage", JsonValue::Array(params))?;
1143        match value {
1144            JsonValue::Null => Ok(None),
1145            JsonValue::String(raw) => Ok(Some(raw)),
1146            _ => Err(CoreError::Rpc(
1147                "state_getStorage returned neither hex nor null".into(),
1148            )),
1149        }
1150    }
1151
1152    fn storage_keys_paged(
1153        &self,
1154        prefix: &[u8],
1155        block_hash: Option<&str>,
1156    ) -> Result<Vec<Vec<u8>>, CoreError> {
1157        let prefix_hex = hex_prefixed(prefix);
1158        let mut all = Vec::new();
1159        let mut start: Option<String> = None;
1160        loop {
1161            let mut params = vec![
1162                JsonValue::String(prefix_hex.clone()),
1163                JsonValue::from(STORAGE_PAGE_SIZE),
1164                start
1165                    .as_ref()
1166                    .map_or(JsonValue::Null, |key| JsonValue::String(key.clone())),
1167            ];
1168            if let Some(hash) = block_hash {
1169                params.push(JsonValue::String(hash.to_string()));
1170            }
1171            let value = self.rpc_value("state_getKeysPaged", JsonValue::Array(params))?;
1172            let page = value
1173                .as_array()
1174                .ok_or_else(|| CoreError::Rpc("state_getKeysPaged returned a non-array".into()))?;
1175            if page.is_empty() {
1176                break;
1177            }
1178            let mut last = None;
1179            for item in page {
1180                let key = json_string(item, "storage key")?.to_string();
1181                all.push(decode_hex(&key)?);
1182                last = Some(key);
1183            }
1184            if page.len() < usize::try_from(STORAGE_PAGE_SIZE).unwrap_or(usize::MAX) {
1185                break;
1186            }
1187            if last == start {
1188                return Err(CoreError::Rpc(
1189                    "state_getKeysPaged repeated its cursor".into(),
1190                ));
1191            }
1192            start = last;
1193        }
1194        Ok(all)
1195    }
1196
1197    fn storage_batch_raw(
1198        &self,
1199        keys: &[Vec<u8>],
1200        block_hash: Option<&str>,
1201    ) -> Result<Vec<Option<String>>, CoreError> {
1202        if keys.is_empty() {
1203            return Ok(Vec::new());
1204        }
1205        let key_hex: Vec<String> = keys.iter().map(|key| hex_prefixed(key)).collect();
1206        let mut params = vec![json!(key_hex)];
1207        if let Some(hash) = block_hash {
1208            params.push(JsonValue::String(hash.to_string()));
1209        }
1210        let value = self.rpc_value("state_queryStorageAt", JsonValue::Array(params))?;
1211        let sets = value
1212            .as_array()
1213            .ok_or_else(|| CoreError::Rpc("state_queryStorageAt returned a non-array".into()))?;
1214        let changes = sets
1215            .first()
1216            .and_then(JsonValue::as_object)
1217            .and_then(|set| set.get("changes"))
1218            .and_then(JsonValue::as_array)
1219            .ok_or_else(|| CoreError::Rpc("state_queryStorageAt omitted changes".into()))?;
1220        let mut by_key: HashMap<String, Option<String>> = HashMap::new();
1221        for change in changes {
1222            let pair = change
1223                .as_array()
1224                .ok_or_else(|| CoreError::Rpc("storage change is not a pair".into()))?;
1225            let key = pair
1226                .first()
1227                .and_then(JsonValue::as_str)
1228                .ok_or_else(|| CoreError::Rpc("storage change has no key".into()))?;
1229            let raw = pair
1230                .get(1)
1231                .and_then(JsonValue::as_str)
1232                .map(ToString::to_string);
1233            by_key.insert(key.to_ascii_lowercase(), raw);
1234        }
1235        Ok(keys
1236            .iter()
1237            .map(|key| {
1238                by_key
1239                    .remove(&hex_prefixed(key).to_ascii_lowercase())
1240                    .flatten()
1241            })
1242            .collect())
1243    }
1244
1245    fn wait_for_inclusion(
1246        &self,
1247        extrinsic: &[u8],
1248        hash: String,
1249        start_block: u64,
1250        wait_for_finalization: bool,
1251        timeout: Duration,
1252    ) -> Result<TxOutcome, CoreError> {
1253        let deadline = Instant::now() + timeout;
1254        let xt_hex = hex_prefixed(extrinsic).to_ascii_lowercase();
1255        let mut next_block = start_block;
1256        let poll = self
1257            .block_time()
1258            .unwrap_or_else(|_| Duration::from_millis(250))
1259            .min(Duration::from_secs(1));
1260        'track_inclusion: while Instant::now() < deadline {
1261            let head = self.block_number()?;
1262            while next_block <= head {
1263                let included_at = next_block;
1264                let block_hash = self.block_hash(Some(included_at))?;
1265                if let Some(index) = self.find_extrinsic(&block_hash, &xt_hex)? {
1266                    if wait_for_finalization {
1267                        match self.wait_until_finalized(&block_hash, included_at, deadline, poll)? {
1268                            InclusionFinalization::Finalized => {}
1269                            InclusionFinalization::Reorged => {
1270                                // The old inclusion block is no longer canonical.
1271                                // Rescan from its height so a re-inclusion at the
1272                                // same or a later height can still produce a receipt.
1273                                next_block = included_at;
1274                                continue 'track_inclusion;
1275                            }
1276                        }
1277                    }
1278                    return self.decode_outcome(hash, block_hash, included_at, index);
1279                }
1280                next_block = next_block.saturating_add(1);
1281            }
1282            thread::sleep(poll);
1283        }
1284        Ok(TxOutcome::pool_rejection(
1285            hash,
1286            format!("extrinsic was not included within {}s", timeout.as_secs()),
1287        ))
1288    }
1289
1290    fn find_extrinsic(&self, block_hash: &str, wanted: &str) -> Result<Option<u32>, CoreError> {
1291        let block = self.rpc_value("chain_getBlock", json!([block_hash]))?;
1292        let extrinsics = block
1293            .as_object()
1294            .and_then(|object| object.get("block"))
1295            .and_then(JsonValue::as_object)
1296            .and_then(|inner| inner.get("extrinsics"))
1297            .and_then(JsonValue::as_array)
1298            .ok_or_else(|| CoreError::Rpc("chain_getBlock omitted extrinsics".into()))?;
1299        for (index, extrinsic) in extrinsics.iter().enumerate() {
1300            if extrinsic
1301                .as_str()
1302                .is_some_and(|raw| raw.eq_ignore_ascii_case(wanted))
1303            {
1304                return u32::try_from(index)
1305                    .map(Some)
1306                    .map_err(|_| CoreError::Rpc("extrinsic index exceeds u32".into()));
1307            }
1308        }
1309        Ok(None)
1310    }
1311
1312    fn wait_until_finalized(
1313        &self,
1314        inclusion_hash: &str,
1315        included_at: u64,
1316        deadline: Instant,
1317        poll: Duration,
1318    ) -> Result<InclusionFinalization, CoreError> {
1319        while Instant::now() < deadline {
1320            // Fetch finality first, then the canonical hash at the inclusion
1321            // height. Once finality has reached that height, the best chain must
1322            // contain the same finalized ancestor at that height.
1323            let finalized = self.finalized_head()?;
1324            let finalized_at = self.header(Some(&finalized))?.number;
1325            let canonical_hash = self.canonical_block_hash(included_at)?;
1326            if let Some(status) = classify_inclusion_finalization(
1327                inclusion_hash,
1328                canonical_hash.as_deref(),
1329                included_at,
1330                finalized_at,
1331            ) {
1332                return Ok(status);
1333            }
1334            thread::sleep(poll);
1335        }
1336        Err(CoreError::Rpc(format!(
1337            "block {included_at} ({inclusion_hash}) was included but not finalized before the receipt timeout"
1338        )))
1339    }
1340
1341    fn decode_outcome(
1342        &self,
1343        extrinsic_hash: String,
1344        block_hash: String,
1345        block_number: u64,
1346        extrinsic_index: u32,
1347    ) -> Result<TxOutcome, CoreError> {
1348        let records = self.query("System", "Events", &[], Some(&block_hash))?;
1349        let mut events = Vec::new();
1350        if let Value::List(items) = records {
1351            for item in items {
1352                if field(&item, "extrinsic_idx")
1353                    .and_then(as_u128)
1354                    .is_some_and(|index| index == u128::from(extrinsic_index))
1355                {
1356                    events.push(item);
1357                }
1358            }
1359        }
1360        let runtime = self.runtime()?;
1361        let mut success = events
1362            .iter()
1363            .any(|event| event_is(event, "System", "ExtrinsicSuccess"));
1364        let mut error = events
1365            .iter()
1366            .find(|event| event_is(event, "System", "ExtrinsicFailed"))
1367            .and_then(event_attributes)
1368            .and_then(|attributes| field(attributes, "dispatch_error").or(Some(attributes)))
1369            .map(|value| dispatch_error(&runtime, value))
1370            .transpose()?;
1371
1372        if error.is_none() {
1373            for (module, event_name) in [
1374                ("Proxy", "ProxyExecuted"),
1375                ("Sudo", "Sudid"),
1376                ("Multisig", "MultisigExecuted"),
1377            ] {
1378                let Some(attributes) = events
1379                    .iter()
1380                    .find(|event| event_is(event, module, event_name))
1381                    .and_then(event_attributes)
1382                else {
1383                    continue;
1384                };
1385                let result = field(attributes, "result").unwrap_or(attributes);
1386                if let Some(inner) = variant(result, "Err") {
1387                    error = Some(dispatch_error(&runtime, inner)?);
1388                    success = false;
1389                    break;
1390                }
1391            }
1392        }
1393
1394        let fee_rao = events
1395            .iter()
1396            .find(|event| event_is(event, "TransactionPayment", "TransactionFeePaid"))
1397            .and_then(event_attributes)
1398            .and_then(|attributes| field(attributes, "actual_fee"))
1399            .and_then(as_u128);
1400        let message = match &error {
1401            Some(error) => error.to_string(),
1402            None if success => "extrinsic succeeded".into(),
1403            None => "extrinsic did not emit System.ExtrinsicSuccess".into(),
1404        };
1405        Ok(TxOutcome {
1406            success,
1407            extrinsic_hash,
1408            block_hash: Some(block_hash),
1409            block_number: Some(block_number),
1410            extrinsic_index: Some(extrinsic_index),
1411            fee_rao,
1412            events,
1413            error,
1414            message,
1415            data: BTreeMap::new(),
1416        })
1417    }
1418}
1419
1420/// A read-only view pinned to one block hash.
1421pub struct Snapshot<'a> {
1422    client: &'a Client,
1423    pub block_hash: String,
1424    pub block_number: u64,
1425}
1426
1427impl Snapshot<'_> {
1428    pub fn query(&self, pallet: &str, storage: &str, params: &[Value]) -> Result<Value, CoreError> {
1429        self.client
1430            .query(pallet, storage, params, Some(&self.block_hash))
1431    }
1432
1433    pub fn query_map(
1434        &self,
1435        pallet: &str,
1436        storage: &str,
1437        fixed_params: &[Value],
1438    ) -> Result<Vec<(Value, Value)>, CoreError> {
1439        self.client
1440            .query_map(pallet, storage, fixed_params, Some(&self.block_hash))
1441    }
1442
1443    pub fn runtime_call(
1444        &self,
1445        api: &str,
1446        method: &str,
1447        params: &[Value],
1448    ) -> Result<Value, CoreError> {
1449        self.client
1450            .runtime_call(api, method, params, Some(&self.block_hash))
1451    }
1452
1453    pub fn balance_rao(&self, address: &str) -> Result<u128, CoreError> {
1454        let account = self.query("System", "Account", &[Value::str(address)])?;
1455        field(&account, "data")
1456            .and_then(|data| field(data, "free"))
1457            .and_then(as_u128)
1458            .ok_or_else(|| CoreError::Codec("System.Account.data.free is missing".into()))
1459    }
1460}
1461
1462/// Iterator returned by [`Client::blocks`].
1463pub struct BlockStream<'a> {
1464    client: &'a Client,
1465    finalized: bool,
1466    last_number: Option<u64>,
1467    last_finalized_hash: Option<String>,
1468    poll_interval: Duration,
1469}
1470
1471impl BlockStream<'_> {
1472    /// One poll cycle. Idle polls cost a single RPC call: the best-head path
1473    /// skips the hash lookup until the number advances, and the finalized path
1474    /// skips the header fetch while `chain_getFinalizedHead` is unchanged.
1475    fn poll(&mut self) -> Result<Option<BlockHeader>, CoreError> {
1476        let header = if self.finalized {
1477            let hash = self.client.finalized_head()?;
1478            if self.last_finalized_hash.as_deref() == Some(hash.as_str()) {
1479                return Ok(None);
1480            }
1481            self.last_finalized_hash = Some(hash.clone());
1482            let header = self.client.header(Some(&hash))?;
1483            (self.last_number.is_none_or(|last| header.number > last)).then_some(header)
1484        } else {
1485            self.client.best_header_past(self.last_number)?
1486        };
1487        if let Some(header) = &header {
1488            self.last_number = Some(header.number);
1489        }
1490        Ok(header)
1491    }
1492}
1493
1494impl Iterator for BlockStream<'_> {
1495    type Item = Result<BlockHeader, CoreError>;
1496
1497    fn next(&mut self) -> Option<Self::Item> {
1498        loop {
1499            match self.poll() {
1500                Ok(Some(header)) => return Some(Ok(header)),
1501                Ok(None) => thread::sleep(self.poll_interval),
1502                Err(error) => return Some(Err(error)),
1503            }
1504        }
1505    }
1506}
1507
1508struct RpcBootstrap {
1509    endpoint: String,
1510    http: HttpClient,
1511    next_id: AtomicU64,
1512}
1513
1514impl RpcBootstrap {
1515    fn rpc(&self, method: &str, params: JsonValue) -> Result<JsonValue, CoreError> {
1516        rpc_request(
1517            &self.http,
1518            &self.endpoint,
1519            self.next_id.fetch_add(1, Ordering::Relaxed),
1520            method,
1521            params,
1522        )
1523    }
1524
1525    fn runtime_version(&self) -> Result<RuntimeVersion, CoreError> {
1526        parse_runtime_version(self.rpc("state_getRuntimeVersion", json!([]))?)
1527    }
1528
1529    fn metadata(&self) -> Result<Vec<u8>, CoreError> {
1530        fetch_metadata(|method, params| self.rpc(method, params))
1531    }
1532
1533    fn block_hash(&self, block: Option<u64>) -> Result<String, CoreError> {
1534        let params = block.map_or_else(|| json!([]), |number| json!([number]));
1535        let value = self.rpc("chain_getBlockHash", params)?;
1536        Ok(json_string(&value, "chain_getBlockHash result")?.to_string())
1537    }
1538
1539    fn ss58_format(&self) -> Result<u16, CoreError> {
1540        let properties = self.rpc("system_properties", json!([]))?;
1541        let value = properties
1542            .as_object()
1543            .and_then(|object| object.get("ss58Format"))
1544            .map(json_u64)
1545            .transpose()?
1546            .unwrap_or(42);
1547        u16::try_from(value)
1548            .map_err(|_| CoreError::Rpc(format!("ss58Format {value} does not fit u16")))
1549    }
1550}
1551
1552#[derive(Debug, Clone, Copy)]
1553struct RuntimeVersion {
1554    spec_version: u32,
1555    transaction_version: u32,
1556}
1557
1558fn parse_runtime_version(value: JsonValue) -> Result<RuntimeVersion, CoreError> {
1559    let object = value
1560        .as_object()
1561        .ok_or_else(|| CoreError::Rpc("state_getRuntimeVersion returned a non-object".into()))?;
1562    let spec = object
1563        .get("specVersion")
1564        .ok_or_else(|| CoreError::Rpc("runtime version has no specVersion".into()))?;
1565    let transaction = object
1566        .get("transactionVersion")
1567        .ok_or_else(|| CoreError::Rpc("runtime version has no transactionVersion".into()))?;
1568    Ok(RuntimeVersion {
1569        spec_version: u32::try_from(json_u64(spec)?)
1570            .map_err(|_| CoreError::Rpc("specVersion does not fit u32".into()))?,
1571        transaction_version: u32::try_from(json_u64(transaction)?)
1572            .map_err(|_| CoreError::Rpc("transactionVersion does not fit u32".into()))?,
1573    })
1574}
1575
1576/// Fetch metadata V15 when the runtime exposes the metadata runtime API.
1577///
1578/// `state_getMetadata` returns V14 on current Substrate nodes, which omits the
1579/// runtime-API descriptions required by `Client::runtime_call`. Older nodes may
1580/// not expose `Metadata_metadata_at_version`, so retain V14 as a compatibility
1581/// fallback for storage reads and call composition.
1582fn fetch_metadata<F>(mut rpc: F) -> Result<Vec<u8>, CoreError>
1583where
1584    F: FnMut(&str, JsonValue) -> Result<JsonValue, CoreError>,
1585{
1586    let requested_version = hex_prefixed(&15u32.to_le_bytes());
1587    match rpc(
1588        "state_call",
1589        json!(["Metadata_metadata_at_version", requested_version]),
1590    ) {
1591        Ok(value) => {
1592            let encoded = decode_hex(json_string(&value, "Metadata_metadata_at_version result")?)?;
1593            let mut input = encoded.as_slice();
1594            let metadata = Option::<Vec<u8>>::decode(&mut input).map_err(|error| {
1595                CoreError::Codec(format!(
1596                    "cannot decode Metadata_metadata_at_version result: {error}"
1597                ))
1598            })?;
1599            if !input.is_empty() {
1600                return Err(CoreError::Codec(format!(
1601                    "{} trailing bytes in Metadata_metadata_at_version result",
1602                    input.len()
1603                )));
1604            }
1605            if let Some(metadata) = metadata {
1606                return Ok(metadata);
1607            }
1608        }
1609        Err(CoreError::Rpc(_)) => {}
1610        Err(error) => return Err(error),
1611    }
1612
1613    let value = rpc("state_getMetadata", json!([]))?;
1614    decode_hex(json_string(&value, "state_getMetadata result")?)
1615}
1616
1617fn rpc_request(
1618    client: &HttpClient,
1619    endpoint: &str,
1620    id: u64,
1621    method: &str,
1622    params: JsonValue,
1623) -> Result<JsonValue, CoreError> {
1624    let response = client
1625        .post(endpoint)
1626        .json(&json!({
1627            "jsonrpc": "2.0",
1628            "id": id,
1629            "method": method,
1630            "params": params,
1631        }))
1632        .send()
1633        .map_err(|error| CoreError::Rpc(format!("{method} request failed: {error}")))?
1634        .error_for_status()
1635        .map_err(|error| CoreError::Rpc(format!("{method} HTTP error: {error}")))?;
1636    let body: JsonValue = response
1637        .json()
1638        .map_err(|error| CoreError::Rpc(format!("{method} returned invalid JSON: {error}")))?;
1639    if let Some(error) = body.as_object().and_then(|object| object.get("error")) {
1640        return Err(CoreError::Rpc(format!("{method}: {error}")));
1641    }
1642    body.as_object()
1643        .and_then(|object| object.get("result"))
1644        .cloned()
1645        .ok_or_else(|| CoreError::Rpc(format!("{method} response omitted result")))
1646}
1647
1648fn http_endpoint(endpoint: &str) -> String {
1649    if let Some(rest) = endpoint.strip_prefix("ws://") {
1650        return format!("http://{rest}");
1651    }
1652    if let Some(rest) = endpoint.strip_prefix("wss://") {
1653        return format!("https://{rest}");
1654    }
1655    endpoint.to_string()
1656}
1657
1658fn parse_header_fields(value: &JsonValue) -> Result<(u64, String), CoreError> {
1659    let object = value
1660        .as_object()
1661        .ok_or_else(|| CoreError::Rpc("chain_getHeader returned a non-object".into()))?;
1662    let number = json_u64(
1663        object
1664            .get("number")
1665            .ok_or_else(|| CoreError::Rpc("header has no number".into()))?,
1666    )?;
1667    let parent_hash = object
1668        .get("parentHash")
1669        .and_then(JsonValue::as_str)
1670        .unwrap_or_default()
1671        .to_string();
1672    Ok((number, parent_hash))
1673}
1674
1675fn storage_info(runtime: &Runtime, pallet: &str, storage: &str) -> Result<StorageInfo, CoreError> {
1676    runtime
1677        .storage_entry(pallet, storage)
1678        .cloned()
1679        .ok_or_else(|| CoreError::NotInRuntime(format!("storage {pallet}.{storage}")))
1680}
1681
1682fn runtime_api_method<'a>(
1683    runtime: &'a Runtime,
1684    api: &str,
1685    method: &str,
1686) -> Result<&'a RuntimeApiMethodInfo, CoreError> {
1687    runtime
1688        .apis
1689        .iter()
1690        .find(|candidate| candidate.name == api)
1691        .and_then(|candidate| {
1692            candidate
1693                .methods
1694                .iter()
1695                .find(|candidate| candidate.name == method)
1696        })
1697        .ok_or_else(|| CoreError::NotInRuntime(format!("runtime API {api}.{method}")))
1698}
1699
1700fn decode_storage(
1701    runtime: &Runtime,
1702    info: &StorageInfo,
1703    raw: Option<&str>,
1704) -> Result<Value, CoreError> {
1705    let bytes = match raw {
1706        Some(raw) => decode_hex(raw)?,
1707        None if info.modifier == "Optional" => return Ok(Value::Null),
1708        None => info.default_bytes.clone(),
1709    };
1710    runtime.decode_spec(&TypeSpec::Id(info.value_type), &bytes, true)
1711}
1712
1713fn dispatch_error(runtime: &Runtime, value: &Value) -> Result<DispatchError, CoreError> {
1714    if let Some(module) = variant(value, "Module") {
1715        let index = field(module, "index")
1716            .and_then(as_u128)
1717            .or_else(|| tuple_item(module, 0).and_then(as_u128))
1718            .and_then(|value| u8::try_from(value).ok())
1719            .ok_or_else(|| CoreError::Codec("module error omitted pallet index".into()))?;
1720        let error_index = field(module, "error")
1721            .or_else(|| tuple_item(module, 1))
1722            .and_then(first_byte)
1723            .ok_or_else(|| CoreError::Codec("module error omitted error bytes".into()))?;
1724        let pallet = runtime.pallet_at(index).map(|pallet| pallet.name.clone());
1725        let (name, docs) = runtime
1726            .module_error(index, error_index)
1727            .unwrap_or_else(|_| (format!("Error{error_index}"), Vec::new()));
1728        return Ok(DispatchError {
1729            pallet,
1730            semantic_code: semantic_error_code(&name),
1731            name,
1732            docs,
1733        });
1734    }
1735    let name = variant_name(value).unwrap_or_else(|| "DispatchError".into());
1736    Ok(DispatchError {
1737        pallet: None,
1738        semantic_code: semantic_error_code(&name),
1739        name,
1740        docs: Vec::new(),
1741    })
1742}
1743
1744fn semantic_error_code(name: &str) -> String {
1745    let canonical = match name {
1746        "SubNetworkDoesNotExist"
1747        | "SubnetDoesNotExist"
1748        | "SubnetNotExists"
1749        | "SubNetDoesNotExist" => "subnet_not_exists",
1750        "NotProxy" => "not_proxy",
1751        "NoPermission" | "NotAllowed" | "Unproxyable" => "not_allowed",
1752        "HotKeyNotRegisteredInNetwork" | "HotkeyNotRegisteredInSubNet" | "NotRegistered" => {
1753            "not_registered"
1754        }
1755        "TxRateLimitExceeded" | "SettingWeightsTooFast" => "rate_limited",
1756        _ => return snake_case(name),
1757    };
1758    canonical.into()
1759}
1760
1761fn snake_case(name: &str) -> String {
1762    let mut out = String::new();
1763    for (index, ch) in name.chars().enumerate() {
1764        if ch.is_uppercase() {
1765            if index > 0 {
1766                out.push('_');
1767            }
1768            for lower in ch.to_lowercase() {
1769                out.push(lower);
1770            }
1771        } else {
1772            out.push(ch);
1773        }
1774    }
1775    out
1776}
1777
1778fn event_is(record: &Value, module: &str, name: &str) -> bool {
1779    field(record, "module_id")
1780        .or_else(|| field(record, "event").and_then(|event| field(event, "module_id")))
1781        .and_then(as_str)
1782        == Some(module)
1783        && field(record, "event_id")
1784            .or_else(|| field(record, "event").and_then(|event| field(event, "event_id")))
1785            .and_then(as_str)
1786            == Some(name)
1787}
1788
1789fn event_attributes(record: &Value) -> Option<&Value> {
1790    field(record, "attributes")
1791        .or_else(|| field(record, "event").and_then(|event| field(event, "attributes")))
1792}
1793
1794/// Flatten `get_subnet_hyperparams_v3`'s `[{name, value}, ...]` list into a
1795/// `{name: raw}` map, matching the Python `subnet_hyperparameters` read.
1796fn flatten_subnet_hyperparams_v3(raw: Value) -> Result<Value, CoreError> {
1797    if matches!(raw, Value::Null) {
1798        return Ok(Value::Null);
1799    }
1800    let Value::List(entries) = raw else {
1801        return Err(CoreError::Codec(
1802            "SubnetInfoRuntimeApi.get_subnet_hyperparams_v3 returned a non-list".into(),
1803        ));
1804    };
1805    let mut out = Vec::with_capacity(entries.len());
1806    for entry in entries {
1807        let name = field(&entry, "name")
1808            .and_then(as_str)
1809            .ok_or_else(|| {
1810                CoreError::Codec(
1811                    "SubnetInfoRuntimeApi.get_subnet_hyperparams_v3 entry omitted name".into(),
1812                )
1813            })?
1814            .to_owned();
1815        let tagged = field(&entry, "value").ok_or_else(|| {
1816            CoreError::Codec(
1817                "SubnetInfoRuntimeApi.get_subnet_hyperparams_v3 entry omitted value".into(),
1818            )
1819        })?;
1820        out.push((Value::Str(name), flatten_hyperparam_value(tagged.clone())));
1821    }
1822    Ok(Value::Dict(out))
1823}
1824
1825/// Peel one v3 `{TypeTag: payload}` value down to the raw payload.
1826///
1827/// `U64F64` keeps the raw `bits` (what `sudo set` writes for
1828/// `burn_increase_mult`). `I32F32` is only used for
1829/// `alpha_sigmoid_steepness`, whose setter takes the integer part
1830/// (`bits / 2^32`), not the raw bits.
1831fn flatten_hyperparam_value(tagged: Value) -> Value {
1832    const I32F32_ONE: i128 = 1 << 32;
1833    let Value::Dict(entries) = &tagged else {
1834        return tagged;
1835    };
1836    let [(tag, payload)] = entries.as_slice() else {
1837        return tagged;
1838    };
1839    let Some(tag) = as_str(tag) else {
1840        return tagged;
1841    };
1842    if let Value::Dict(inner) = payload {
1843        if let [(Value::Str(key), bits)] = inner.as_slice() {
1844            if key == "bits" {
1845                if tag == "I32F32" {
1846                    let bits = match bits {
1847                        Value::Int(v) => *v,
1848                        Value::Uint(v) => match i128::try_from(*v) {
1849                            Ok(v) => v,
1850                            Err(_) => return tagged,
1851                        },
1852                        _ => return tagged,
1853                    };
1854                    return Value::Int(bits.div_euclid(I32F32_ONE));
1855                }
1856                return bits.clone();
1857            }
1858        }
1859    }
1860    payload.clone()
1861}
1862
1863pub fn field<'a>(value: &'a Value, name: &str) -> Option<&'a Value> {
1864    let Value::Dict(entries) = value else {
1865        return None;
1866    };
1867    entries.iter().find_map(|(key, value)| match key {
1868        Value::Str(key) if key == name => Some(value),
1869        _ => None,
1870    })
1871}
1872
1873pub fn variant<'a>(value: &'a Value, name: &str) -> Option<&'a Value> {
1874    field(value, name)
1875}
1876
1877pub fn variant_name(value: &Value) -> Option<String> {
1878    match value {
1879        Value::Str(name) => Some(name.clone()),
1880        Value::Dict(entries) if entries.len() == 1 => entries.first().and_then(|(key, _)| {
1881            if let Value::Str(name) = key {
1882                Some(name.clone())
1883            } else {
1884                None
1885            }
1886        }),
1887        _ => None,
1888    }
1889}
1890
1891pub fn as_str(value: &Value) -> Option<&str> {
1892    match value {
1893        Value::Str(value) => Some(value),
1894        _ => None,
1895    }
1896}
1897
1898pub fn as_u128(value: &Value) -> Option<u128> {
1899    match value {
1900        Value::Int(value) => u128::try_from(*value).ok(),
1901        Value::Uint(value) => Some(*value),
1902        _ => None,
1903    }
1904}
1905
1906pub fn as_bool(value: &Value) -> Option<bool> {
1907    match value {
1908        Value::Bool(value) => Some(*value),
1909        _ => None,
1910    }
1911}
1912
1913pub fn value_bytes(value: &Value) -> Option<Vec<u8>> {
1914    match value {
1915        Value::Bytes(bytes) => Some(bytes.clone()),
1916        Value::Str(value) => {
1917            if value.starts_with("0x") {
1918                decode_hex(value).ok()
1919            } else {
1920                Some(value.as_bytes().to_vec())
1921            }
1922        }
1923        Value::List(items) | Value::Tuple(items) => {
1924            let direct = items
1925                .iter()
1926                .map(as_u128)
1927                .map(|value| value.and_then(|value| u8::try_from(value).ok()))
1928                .collect::<Option<Vec<_>>>();
1929            direct.or_else(|| match items.as_slice() {
1930                [inner] => value_bytes(inner),
1931                _ => None,
1932            })
1933        }
1934        Value::Dict(entries) => match entries.as_slice() {
1935            [(_, inner)] => value_bytes(inner),
1936            _ => None,
1937        },
1938        _ => None,
1939    }
1940}
1941
1942fn tuple_item(value: &Value, index: usize) -> Option<&Value> {
1943    match value {
1944        Value::Tuple(items) | Value::List(items) => items.get(index),
1945        _ => None,
1946    }
1947}
1948
1949fn first_byte(value: &Value) -> Option<u8> {
1950    value_bytes(value).and_then(|bytes| bytes.first().copied())
1951}
1952
1953fn required_u128(value: &Value, name: &str) -> Result<u128, CoreError> {
1954    field(value, name)
1955        .and_then(as_u128)
1956        .ok_or_else(|| CoreError::Codec(format!("runtime result omitted integer field {name}")))
1957}
1958
1959fn value_map_u16(rows: Vec<(Value, Value)>) -> BTreeMap<u16, u16> {
1960    rows.into_iter()
1961        .filter_map(|(key, value)| {
1962            let key = as_u128(&key).and_then(|value| u16::try_from(value).ok())?;
1963            let value = as_u128(&value).and_then(|value| u16::try_from(value).ok())?;
1964            Some((key, value))
1965        })
1966        .collect()
1967}
1968
1969fn value_map_u128(rows: Vec<(Value, Value)>) -> BTreeMap<u16, u128> {
1970    rows.into_iter()
1971        .filter_map(|(key, value)| {
1972            let key = as_u128(&key).and_then(|value| u16::try_from(value).ok())?;
1973            Some((key, as_u128(&value)?))
1974        })
1975        .collect()
1976}
1977
1978fn json_string<'a>(value: &'a JsonValue, context: &str) -> Result<&'a str, CoreError> {
1979    match value {
1980        JsonValue::String(text) => Ok(text),
1981        _ => Err(CoreError::Rpc(format!("{context} is not a string"))),
1982    }
1983}
1984
1985fn json_number_u64(value: &JsonValue) -> Option<u64> {
1986    match value {
1987        JsonValue::Number(number) => number.to_string().parse().ok(),
1988        _ => None,
1989    }
1990}
1991
1992fn json_number_u128(value: &JsonValue) -> Option<u128> {
1993    match value {
1994        JsonValue::Number(number) => number.to_string().parse().ok(),
1995        _ => None,
1996    }
1997}
1998
1999fn json_number_i128(value: &JsonValue) -> Option<i128> {
2000    match value {
2001        JsonValue::Number(number) => number.to_string().parse().ok(),
2002        _ => None,
2003    }
2004}
2005
2006fn json_u64(value: &JsonValue) -> Result<u64, CoreError> {
2007    if let Some(value) = json_number_u64(value) {
2008        return Ok(value);
2009    }
2010    let text = value
2011        .as_str()
2012        .ok_or_else(|| CoreError::Rpc(format!("expected integer, got {value}")))?;
2013    if let Some(hex) = text.strip_prefix("0x") {
2014        return u64::from_str_radix(hex, 16)
2015            .map_err(|error| CoreError::Rpc(format!("invalid hex integer {text}: {error}")));
2016    }
2017    text.parse::<u64>()
2018        .map_err(|error| CoreError::Rpc(format!("invalid integer {text}: {error}")))
2019}
2020
2021fn json_u128(value: &JsonValue) -> Result<u128, CoreError> {
2022    if let Some(value) = json_number_u128(value) {
2023        return Ok(value);
2024    }
2025    let text = value
2026        .as_str()
2027        .ok_or_else(|| CoreError::Rpc(format!("expected integer, got {value}")))?;
2028    if let Some(hex) = text.strip_prefix("0x") {
2029        return u128::from_str_radix(hex, 16)
2030            .map_err(|error| CoreError::Rpc(format!("invalid hex integer {text}: {error}")));
2031    }
2032    text.parse::<u128>()
2033        .map_err(|error| CoreError::Rpc(format!("invalid integer {text}: {error}")))
2034}
2035
2036fn json_to_value(value: &JsonValue) -> Result<Value, CoreError> {
2037    Ok(match value {
2038        JsonValue::Null => Value::Null,
2039        JsonValue::Bool(value) => Value::Bool(*value),
2040        JsonValue::Number(value) => {
2041            let number = JsonValue::Number(value.clone());
2042            if let Some(signed) = json_number_i128(&number) {
2043                Value::Int(i128::from(signed))
2044            } else if let Some(unsigned) = json_number_u128(&number) {
2045                Value::Uint(unsigned)
2046            } else {
2047                return Err(CoreError::Codec(format!(
2048                    "non-integral JSON number cannot become SCALE Value: {value}"
2049                )));
2050            }
2051        }
2052        JsonValue::String(value) => Value::str(value),
2053        JsonValue::Array(items) => Value::List(
2054            items
2055                .iter()
2056                .map(json_to_value)
2057                .collect::<Result<Vec<_>, _>>()?,
2058        ),
2059        JsonValue::Object(entries) => Value::record(
2060            entries
2061                .iter()
2062                .map(|(key, value)| Ok((key.clone(), json_to_value(value)?)))
2063                .collect::<Result<Vec<_>, CoreError>>()?,
2064        ),
2065    })
2066}
2067
2068fn parse_h256(value: &str) -> Result<[u8; 32], CoreError> {
2069    let bytes = decode_hex(value)?;
2070    <[u8; 32]>::try_from(bytes.as_slice())
2071        .map_err(|_| CoreError::Rpc(format!("expected H256, got {} bytes", bytes.len())))
2072}
2073
2074fn decode_hex(value: &str) -> Result<Vec<u8>, CoreError> {
2075    hex::decode(value.trim_start_matches("0x"))
2076        .map_err(|error| CoreError::Codec(format!("invalid hex {value}: {error}")))
2077}
2078
2079fn hex_prefixed(bytes: &[u8]) -> String {
2080    format!("0x{}", hex::encode(bytes))
2081}
2082
2083#[cfg(test)]
2084mod reorg_finalization_tests {
2085    use super::{classify_inclusion_finalization, InclusionFinalization};
2086
2087    #[test]
2088    fn canonical_inclusion_remains_pending_below_finalized_height() {
2089        assert_eq!(
2090            classify_inclusion_finalization("0xabc", Some("0xABC"), 10, 9),
2091            None
2092        );
2093    }
2094
2095    #[test]
2096    fn canonical_inclusion_finalizes_at_or_above_its_height() {
2097        assert_eq!(
2098            classify_inclusion_finalization("0xabc", Some("0xABC"), 10, 10),
2099            Some(InclusionFinalization::Finalized)
2100        );
2101        assert_eq!(
2102            classify_inclusion_finalization("0xabc", Some("0xabc"), 10, 12),
2103            Some(InclusionFinalization::Finalized)
2104        );
2105    }
2106
2107    #[test]
2108    fn replaced_or_missing_inclusion_hash_is_a_reorg() {
2109        assert_eq!(
2110            classify_inclusion_finalization("0xabc", Some("0xdef"), 10, 12),
2111            Some(InclusionFinalization::Reorged)
2112        );
2113        assert_eq!(
2114            classify_inclusion_finalization("0xabc", None, 10, 9),
2115            Some(InclusionFinalization::Reorged)
2116        );
2117    }
2118}
2119
2120#[cfg(test)]
2121mod fee_quote_tests {
2122    #![allow(clippy::unwrap_used, clippy::expect_used)]
2123    use super::*;
2124    use std::io::{BufRead, BufReader, Read, Write};
2125    use std::net::TcpListener;
2126
2127    fn runtime() -> Runtime {
2128        crate::codec::extrinsic::hashed_tests::runtime_falcon()
2129    }
2130
2131    #[test]
2132    fn fee_and_weight_rpc_use_sized_dummy_proofs_for_every_wallet_mode() {
2133        for crypto in [
2134            0,
2135            1,
2136            4,
2137            6,
2138            crate::keys::CRYPTO_MLDSA65,
2139            crate::keys::CRYPTO_MLDSA65_FIXED,
2140            crate::keys::CRYPTO_FALCON512,
2141            crate::keys::CRYPTO_FALCON512_HASHED,
2142            crate::keys::CRYPTO_FALCON1024,
2143            crate::keys::CRYPTO_FALCON1024_HASHED,
2144        ] {
2145            let runtime = runtime();
2146            let full = Keypair::from_seed(&[17; 32], crypto).unwrap();
2147            // Public-only keys cannot sign or derive a generation's public key.
2148            let public = Keypair::new(Some(&full.ss58_address()), None, crypto, 42).unwrap();
2149            let call = runtime
2150                .compose_call(
2151                    "System",
2152                    "remark",
2153                    &Value::record(vec![("remark".into(), Value::Bytes(vec![7; 300]))]),
2154                )
2155                .unwrap();
2156            let params = TxParams {
2157                era: Value::record(vec![
2158                    ("period".into(), Value::Uint(64)),
2159                    ("current".into(), Value::Uint(10)),
2160                ]),
2161                nonce: 64,
2162                tip: 0,
2163                tip_asset_id: None,
2164                genesis_hash: [8; 32],
2165                era_block_hash: [8; 32],
2166                metadata_hash: None,
2167            };
2168            let proof = if is_hashed_crypto(crypto) || is_pq_crypto(crypto) {
2169                full.sign_authorization(b"length control").unwrap()
2170            } else {
2171                full.sign(b"length control").unwrap()
2172            };
2173            let (control, _) = runtime
2174                .encode_signed_extrinsic(&call, full.public_key_bytes(), &proof, crypto, &params)
2175                .unwrap();
2176            let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2177            let endpoint = format!("http://{}", listener.local_addr().unwrap());
2178            let server = thread::spawn(move || {
2179                let mut quotes = Vec::new();
2180                while quotes.len() < 2 {
2181                    let (mut stream, _) = listener.accept().unwrap();
2182                    stream
2183                        .set_read_timeout(Some(Duration::from_secs(5)))
2184                        .unwrap();
2185                    let mut reader = BufReader::new(&mut stream);
2186                    let mut length = 0;
2187                    let mut headers = String::new();
2188                    loop {
2189                        let mut line = String::new();
2190                        assert_ne!(reader.read_line(&mut line).unwrap(), 0);
2191                        headers.push_str(&line);
2192                        if line == "\r\n" {
2193                            break;
2194                        }
2195                        if let Some(value) = line.to_lowercase().strip_prefix("content-length:") {
2196                            length = value.trim().parse::<usize>().unwrap();
2197                        }
2198                    }
2199                    match headers.lines().next().unwrap() {
2200                        "HEAD / HTTP/1.1" => {
2201                            // Readiness probes are not JSON-RPC requests or fee quotes.
2202                            assert_eq!(length, 0, "HEAD probe must not carry a body");
2203                            write!(
2204                                stream,
2205                                "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
2206                            )
2207                            .unwrap();
2208                            continue;
2209                        }
2210                        "POST / HTTP/1.1" => {}
2211                        other => panic!("unexpected mock HTTP request: {other}"),
2212                    }
2213                    assert!(length > 0, "mock request has no body length: {headers:?}");
2214                    let mut body = vec![0; length];
2215                    reader.read_exact(&mut body).unwrap();
2216                    let request: JsonValue = serde_json::from_slice(&body).unwrap();
2217                    let result = match request["method"].as_str().unwrap() {
2218                        "system_accountNextIndex" => json!(64),
2219                        "chain_getHeader" => {
2220                            json!({"number":"0xa", "parentHash":hex_prefixed(&[8;32])})
2221                        }
2222                        "chain_getBlockHash" => json!(hex_prefixed(&[8; 32])),
2223                        "payment_queryInfo" => {
2224                            quotes
2225                                .push(decode_hex(request["params"][0].as_str().unwrap()).unwrap());
2226                            json!({"partialFee":"1234", "weight":{"ref_time":456, "proof_size":78}})
2227                        }
2228                        other => {
2229                            panic!("quote must not read authorization state or submit: {other}")
2230                        }
2231                    };
2232                    let response =
2233                        json!({"jsonrpc":"2.0", "id":request["id"], "result":result}).to_string();
2234                    write!(stream, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", response.len(), response).unwrap();
2235                }
2236                quotes
2237            });
2238            let client = Client {
2239                endpoint,
2240                http: HttpClient::builder()
2241                    .no_proxy()
2242                    .timeout(Duration::from_secs(5))
2243                    .build()
2244                    .unwrap(),
2245                next_id: AtomicU64::new(1),
2246                runtime: RwLock::new(Arc::new(runtime)),
2247                genesis_hash: [8; 32],
2248                ss58_format: 42,
2249            };
2250            let probe = || {
2251                let response = client.http.head(&client.endpoint).send().unwrap();
2252                assert_eq!(response.status().as_u16(), 200);
2253                assert!(response.bytes().unwrap().is_empty());
2254            };
2255            // Reproduce a bodyless probe before each real quote. Both quotes must
2256            // still reach the server and satisfy the payload assertions below.
2257            probe();
2258            assert_eq!(client.estimate_fee(&call, &public).unwrap(), 1234);
2259            probe();
2260            assert_eq!(
2261                client.estimate_weight(&call, &public).unwrap(),
2262                json_to_value(&json!({"ref_time":456,"proof_size":78})).unwrap()
2263            );
2264            let quotes = server.join().unwrap();
2265            assert_eq!(quotes[0], quotes[1]);
2266            assert_eq!(quotes[0].len(), control.len());
2267            let mut body = quotes[0].as_slice();
2268            let _ = codec::Compact::<u32>::decode(&mut body).unwrap();
2269            let mut control_body = control.as_slice();
2270            let _ = codec::Compact::<u32>::decode(&mut control_body).unwrap();
2271            if crate::keys::is_falcon_crypto(crypto) {
2272                assert_eq!(&body[..2], &[0x84, 2]);
2273                let mut raw = &body[2..];
2274                let profile = full.mldsa_profile().unwrap();
2275                assert_eq!(
2276                    codec::Compact::<u32>::decode(&mut raw).unwrap().0 as usize,
2277                    subtensor_hashed::falcon::proof_len(profile).unwrap()
2278                );
2279                let address = subtensor_hashed::falcon::SignerAddress::decode(&mut raw).unwrap();
2280                assert_eq!(address.account, full.public_key_bytes());
2281                assert!(address.public_key.iter().all(|byte| *byte == 0));
2282                assert!(address.signature.iter().all(|byte| *byte == 0));
2283                assert_eq!(&raw[..65], &[vec![0], vec![0x4d; 64]].concat());
2284                let active = full.pq_public_key().unwrap();
2285                assert!(!quotes[0].windows(active.len()).any(|w| w == active));
2286            } else if is_pq_crypto(crypto) {
2287                assert_eq!(&body[..2], &[0x84, 2]);
2288                let mut raw = &body[2..];
2289                assert_eq!(codec::Compact::<u32>::decode(&mut raw).unwrap().0, 5367);
2290                let address = subtensor_hashed::mldsa::SignerAddress::decode(&mut raw).unwrap();
2291                assert_eq!(address.account, full.public_key_bytes());
2292                assert_eq!(address.public_key, [0; 1952]);
2293                assert_eq!(address.signature, [0; 3309]);
2294                assert_eq!(&raw[..65], &[vec![0], vec![0x4d; 64]].concat());
2295                let active = full.mldsa_public_key().unwrap();
2296                assert!(!quotes[0].windows(active.len()).any(|w| w == active));
2297            } else if is_hashed_crypto(crypto) {
2298                assert_eq!(&body[..2], &[0x84, 2]);
2299                let mut raw = &body[2..];
2300                assert_eq!(codec::Compact::<u32>::decode(&mut raw).unwrap().0, 106);
2301                let address = subtensor_hashed::SignerAddress::decode(&mut raw).unwrap();
2302                assert_eq!(address.account, full.public_key_bytes());
2303                assert_eq!(address.public_key, [0; 32]);
2304                assert_eq!(address.next_commitment, [0; 32]);
2305                assert_eq!(&raw[1..65], &[0; 64]);
2306                let active = full.hashed_public_key().unwrap();
2307                assert!(!quotes[0].windows(active.len()).any(|w| w == active));
2308            } else {
2309                assert_eq!(&body[35..99], &[0; 64]);
2310                assert_eq!(&body[..35], &control_body[..35]);
2311            }
2312            assert!(client
2313                .runtime()
2314                .unwrap()
2315                .decode_extrinsic(&quotes[0], true)
2316                .is_ok());
2317        }
2318    }
2319}
2320
2321#[cfg(test)]
2322mod registered_signing_tests {
2323    #![allow(clippy::unwrap_used, clippy::expect_used)]
2324
2325    use super::*;
2326    use chacha20poly1305::{aead::Aead, KeyInit, XChaCha20Poly1305, XNonce};
2327    use ml_kem::{kem::Decapsulate, EncodedSizeUser, KemCore, MlKem768};
2328    use scale_info::{Field, Path, PortableType, Type, TypeDef, TypeDefArray, TypeDefComposite};
2329    use std::io::{BufRead, BufReader, Read, Write};
2330    use std::net::TcpListener;
2331    use std::sync::atomic::AtomicBool;
2332    use std::sync::Mutex;
2333
2334    const GENERATION: u64 = 12;
2335    const NONCE: u64 = 63;
2336    const PINNED_HASH: [u8; 32] = [8; 32];
2337    const ADVANCED_HASH: [u8; 32] = [9; 32];
2338
2339    fn add_type(runtime: &mut Runtime, type_def: TypeDef<scale_info::form::PortableForm>) -> u32 {
2340        let id = runtime.types.types.len() as u32;
2341        runtime.types.types.push(PortableType {
2342            id,
2343            ty: Type {
2344                path: Path::default(),
2345                type_params: vec![],
2346                type_def,
2347                docs: vec![],
2348            },
2349        });
2350        id
2351    }
2352
2353    fn record_type(runtime: &mut Runtime, fields: &[(&str, u32)]) -> u32 {
2354        add_type(
2355            runtime,
2356            TypeDef::Composite(TypeDefComposite {
2357                fields: fields
2358                    .iter()
2359                    .map(|(name, ty)| Field {
2360                        name: Some((*name).into()),
2361                        ty: (*ty).into(),
2362                        type_name: None,
2363                        docs: vec![],
2364                    })
2365                    .collect(),
2366            }),
2367        )
2368    }
2369
2370    /// Add the account records to the legacy codec fixture. Calls, extensions,
2371    /// signing envelopes, and System.Account keys retain the real metadata.
2372    fn runtime(crypto: u8) -> Runtime {
2373        let mut runtime = crate::codec::extrinsic::hashed_tests::runtime_falcon();
2374        let u8_ty = add_type(
2375            &mut runtime,
2376            TypeDef::Primitive(scale_info::TypeDefPrimitive::U8),
2377        );
2378        let u64_ty = add_type(
2379            &mut runtime,
2380            TypeDef::Primitive(scale_info::TypeDefPrimitive::U64),
2381        );
2382        let bool_ty = add_type(
2383            &mut runtime,
2384            TypeDef::Primitive(scale_info::TypeDefPrimitive::Bool),
2385        );
2386        let commitment_len = if is_pq_crypto(crypto) { 64 } else { 32 };
2387        let commitment_ty = add_type(
2388            &mut runtime,
2389            TypeDef::Array(TypeDefArray {
2390                len: commitment_len,
2391                type_param: u8_ty.into(),
2392            }),
2393        );
2394        let descriptor_ty = record_type(
2395            &mut runtime,
2396            &[
2397                ("version", u8_ty),
2398                ("algorithm", u8_ty),
2399                ("initial_commitment", commitment_ty),
2400            ],
2401        );
2402        let authority_ty = record_type(
2403            &mut runtime,
2404            &[
2405                ("descriptor", descriptor_ty),
2406                ("generation", u64_ty),
2407                ("commitment", commitment_ty),
2408            ],
2409        );
2410        let account_ty = record_type(&mut runtime, &[("nonce", u64_ty)]);
2411        let system = runtime
2412            .pallets
2413            .iter_mut()
2414            .find(|p| p.name == "System")
2415            .unwrap();
2416        let account = system
2417            .storage
2418            .iter_mut()
2419            .find(|s| s.name == "Account")
2420            .unwrap();
2421        account.value_type = account_ty;
2422        let mut authority = account.clone();
2423        authority.prefix = "HashedAccounts".into();
2424        authority.name = if is_pq_crypto(crypto) {
2425            "MlDsaAccounts"
2426        } else {
2427            "Accounts"
2428        }
2429        .into();
2430        authority.value_type = authority_ty;
2431        runtime
2432            .pallets
2433            .iter_mut()
2434            .find(|p| p.name == "HashedAccounts")
2435            .unwrap()
2436            .storage = vec![authority];
2437        runtime
2438            .pallets
2439            .iter_mut()
2440            .find(|p| p.name == "AdminUtils")
2441            .unwrap()
2442            .storage
2443            .push(StorageInfo {
2444                name: "HashedAccountsEnabled".into(),
2445                prefix: "AdminUtils".into(),
2446                modifier: "Default".into(),
2447                hashers: vec![],
2448                key_types: vec![],
2449                value_type: bool_ty,
2450                default_bytes: vec![0],
2451            });
2452        runtime
2453    }
2454
2455    #[derive(Default)]
2456    struct NodeState {
2457        nonce_override: Option<u64>,
2458        advanced: bool,
2459        pending: bool,
2460        advance_after_inner: bool,
2461        consume_nonce_on_advance: bool,
2462        nonce_reads: usize,
2463        finalized_reads: usize,
2464        authority_reads: Vec<String>,
2465        account_reads: Vec<String>,
2466        submitted: Vec<Vec<u8>>,
2467        included: Vec<Vec<u8>>,
2468        events: Vec<u8>,
2469        block_reads: usize,
2470    }
2471
2472    struct MockNode {
2473        client: Client,
2474        state: Arc<Mutex<NodeState>>,
2475        stop: Arc<AtomicBool>,
2476        server: Option<thread::JoinHandle<()>>,
2477        decrypt_key: <MlKem768 as KemCore>::DecapsulationKey,
2478    }
2479
2480    impl Drop for MockNode {
2481        fn drop(&mut self) {
2482            self.stop.store(true, Ordering::Relaxed);
2483            if let Some(server) = self.server.take() {
2484                server.join().unwrap();
2485            }
2486        }
2487    }
2488
2489    impl MockNode {
2490        fn new(signer: &Keypair) -> Self {
2491            let crypto = signer.crypto_type();
2492            let runtime = runtime(crypto);
2493            let address = [Value::str(signer.ss58_address())];
2494            let storage_key = |pallet, storage, params: &[Value]| {
2495                hex_prefixed(
2496                    &runtime
2497                        .storage_key(&storage_info(&runtime, pallet, storage).unwrap(), params)
2498                        .unwrap(),
2499                )
2500            };
2501            let enabled_key = storage_key("AdminUtils", "HashedAccountsEnabled", &[]);
2502            let authority_key = storage_key(
2503                "HashedAccounts",
2504                if is_pq_crypto(crypto) {
2505                    "MlDsaAccounts"
2506                } else {
2507                    "Accounts"
2508                },
2509                &address,
2510            );
2511            let account_key = storage_key("System", "Account", &address);
2512            let next_key = storage_key("MevShield", "NextKey", &[]);
2513            let events_key = storage_key("System", "Events", &[]);
2514            let descriptor = if is_pq_crypto(crypto) {
2515                signer.mldsa_descriptor().unwrap()
2516            } else {
2517                signer.hashed_descriptor().unwrap()
2518            };
2519            let authority = |generation: u64| {
2520                let mut encoded = descriptor.clone();
2521                encoded.extend(generation.encode());
2522                encoded.extend(
2523                    signer
2524                        .at_generation(generation)
2525                        .unwrap()
2526                        .authorization_current_commitment()
2527                        .unwrap(),
2528                );
2529                encoded
2530            };
2531            let (old_authority, new_authority) = if crate::keys::is_fixed_pq_crypto(crypto) {
2532                // A fixed signer must never read this deliberately absent record.
2533                (Vec::new(), Vec::new())
2534            } else {
2535                (authority(GENERATION), authority(GENERATION + 1))
2536            };
2537            let (decrypt_key, encrypt_key) = MlKem768::generate(&mut rand_core::OsRng);
2538            let key_info = storage_info(&runtime, "MevShield", "NextKey").unwrap();
2539            let mut encoded_key = Vec::new();
2540            runtime
2541                .encode_id(
2542                    key_info.value_type,
2543                    &Value::Bytes(encrypt_key.as_bytes().to_vec()),
2544                    &mut encoded_key,
2545                )
2546                .unwrap();
2547            let state = Arc::new(Mutex::new(NodeState::default()));
2548            let server_state = Arc::clone(&state);
2549            let stop = Arc::new(AtomicBool::new(false));
2550            let server_stop = Arc::clone(&stop);
2551            let listener = TcpListener::bind("127.0.0.1:0").unwrap();
2552            listener.set_nonblocking(true).unwrap();
2553            let endpoint = format!("http://{}", listener.local_addr().unwrap());
2554            let server = thread::spawn(move || {
2555                while !server_stop.load(Ordering::Relaxed) {
2556                    let (mut stream, _) = match listener.accept() {
2557                        Ok(value) => value,
2558                        Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
2559                            thread::sleep(Duration::from_millis(1));
2560                            continue;
2561                        }
2562                        Err(error) => panic!("mock accept failed: {error}"),
2563                    };
2564                    // Accepted sockets may inherit the listener's nonblocking mode
2565                    // (notably on macOS); request reads below use blocking timeouts.
2566                    stream.set_nonblocking(false).unwrap();
2567                    stream
2568                        .set_read_timeout(Some(Duration::from_secs(5)))
2569                        .unwrap();
2570                    let mut reader = BufReader::new(&mut stream);
2571                    let mut length = 0;
2572                    loop {
2573                        let mut line = String::new();
2574                        assert_ne!(reader.read_line(&mut line).unwrap(), 0);
2575                        if line == "\r\n" {
2576                            break;
2577                        }
2578                        if let Some(value) = line.to_lowercase().strip_prefix("content-length:") {
2579                            length = value.trim().parse::<usize>().unwrap();
2580                        }
2581                    }
2582                    let mut body = vec![0; length];
2583                    reader.read_exact(&mut body).unwrap();
2584                    let request: JsonValue = serde_json::from_slice(&body).unwrap();
2585                    let mut state = server_state.lock().unwrap();
2586                    let hash = if state.advanced {
2587                        ADVANCED_HASH
2588                    } else {
2589                        PINNED_HASH
2590                    };
2591                    let result = match request["method"].as_str().unwrap() {
2592                        "chain_getFinalizedHead" => {
2593                            state.finalized_reads += 1;
2594                            json!(hex_prefixed(&hash))
2595                        }
2596                        "chain_getBlockHash" => json!(hex_prefixed(&hash)),
2597                        "chain_getHeader" => {
2598                            json!({"number":"0xa", "parentHash":hex_prefixed(&PINNED_HASH)})
2599                        }
2600                        "chain_getBlock" => {
2601                            state.block_reads += 1;
2602                            json!({"block":{"extrinsics":state.included.iter().map(|xt| hex_prefixed(xt)).collect::<Vec<_>>()}})
2603                        }
2604                        "system_accountNextIndex" => {
2605                            state.nonce_reads += 1;
2606                            // Preparation and the inner proof already checked
2607                            // the nonce. Advance finality just before the
2608                            // carrier's check, after encryption has completed.
2609                            if state.advance_after_inner && state.nonce_reads == 3 {
2610                                state.advanced = true;
2611                            }
2612                            let nonce = NONCE
2613                                + u64::from(
2614                                    state.pending
2615                                        || (state.advanced && state.consume_nonce_on_advance),
2616                                );
2617                            json!(state.nonce_override.unwrap_or(nonce))
2618                        }
2619                        "state_getStorage" => {
2620                            let key = request["params"][0].as_str().unwrap();
2621                            let block = request["params"].get(1).and_then(JsonValue::as_str);
2622                            if key == next_key {
2623                                json!(hex_prefixed(&encoded_key))
2624                            } else if key == events_key {
2625                                json!(hex_prefixed(&state.events))
2626                            } else {
2627                                let block = block.expect("authority reads must specify a block");
2628                                let new = block == hex_prefixed(&ADVANCED_HASH);
2629                                assert!(new || block == hex_prefixed(&PINNED_HASH));
2630                                if key == enabled_key {
2631                                    json!("0x01")
2632                                } else if key == authority_key {
2633                                    state.authority_reads.push(block.into());
2634                                    json!(hex_prefixed(if new && state.consume_nonce_on_advance {
2635                                        &new_authority
2636                                    } else {
2637                                        &old_authority
2638                                    }))
2639                                } else if key == account_key {
2640                                    state.account_reads.push(block.into());
2641                                    json!(hex_prefixed(
2642                                        &(NONCE + u64::from(new && state.consume_nonce_on_advance))
2643                                            .encode()
2644                                    ))
2645                                } else {
2646                                    panic!("unexpected storage key {key}")
2647                                }
2648                            }
2649                        }
2650                        "author_submitExtrinsic" => {
2651                            state
2652                                .submitted
2653                                .push(decode_hex(request["params"][0].as_str().unwrap()).unwrap());
2654                            JsonValue::Null
2655                        }
2656                        method => panic!("unexpected RPC {method}"),
2657                    };
2658                    let response = if request["method"] == "author_submitExtrinsic" {
2659                        // Capture signing output without simulating block execution.
2660                        json!({"jsonrpc":"2.0", "id":request["id"], "error":{"code":1010,"message":"captured by signing test"}})
2661                    } else { json!({"jsonrpc":"2.0", "id":request["id"], "result":result}) }.to_string();
2662                    write!(stream, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", response.len(), response).unwrap();
2663                }
2664            });
2665            Self {
2666                client: Client {
2667                    endpoint,
2668                    http: HttpClient::builder()
2669                        .no_proxy()
2670                        .timeout(Duration::from_secs(5))
2671                        .build()
2672                        .unwrap(),
2673                    next_id: AtomicU64::new(1),
2674                    runtime: RwLock::new(Arc::new(runtime)),
2675                    genesis_hash: PINNED_HASH,
2676                    ss58_format: 42,
2677                },
2678                state,
2679                stop,
2680                server: Some(server),
2681                decrypt_key,
2682            }
2683        }
2684
2685        fn call(&self) -> Vec<u8> {
2686            self.client
2687                .compose_call(
2688                    "System",
2689                    "remark",
2690                    &Value::record(vec![("remark".into(), Value::Bytes(vec![42]))]),
2691                )
2692                .unwrap()
2693        }
2694
2695        fn decrypt(&self, ciphertext: &[u8]) -> Vec<u8> {
2696            let blob = &ciphertext[16..];
2697            let kem_len = usize::from(u16::from_le_bytes([blob[0], blob[1]]));
2698            let kem = ml_kem::Ciphertext::<MlKem768>::try_from(&blob[2..2 + kem_len]).unwrap();
2699            let shared = self.decrypt_key.decapsulate(&kem).unwrap();
2700            let nonce = &blob[2 + kem_len..2 + kem_len + 24];
2701            XChaCha20Poly1305::new_from_slice(shared.as_ref())
2702                .unwrap()
2703                .decrypt(XNonce::from_slice(nonce), &blob[2 + kem_len + 24..])
2704                .unwrap()
2705        }
2706    }
2707
2708    fn assert_proof(
2709        node: &MockNode,
2710        signer: &Keypair,
2711        extrinsic: &[u8],
2712        call: &[u8],
2713        generation: u64,
2714        nonce: u64,
2715    ) {
2716        use sp_core::Pair;
2717        let runtime = node.client.runtime().unwrap();
2718        let decoded = runtime.decode_extrinsic(extrinsic, true).unwrap();
2719        assert_eq!(
2720            field(&decoded, "nonce").and_then(as_u128),
2721            Some(u128::from(nonce))
2722        );
2723        let era = match field(&decoded, "era").unwrap() {
2724            Value::List(parts) | Value::Tuple(parts) => Value::record(vec![
2725                ("period".into(), parts[0].clone()),
2726                ("phase".into(), parts[1].clone()),
2727            ]),
2728            value => value.clone(),
2729        };
2730        let params = TxParams {
2731            era,
2732            nonce,
2733            tip: 0,
2734            tip_asset_id: None,
2735            genesis_hash: PINNED_HASH,
2736            era_block_hash: PINNED_HASH,
2737            metadata_hash: None,
2738        };
2739        let (extra, additional) = runtime.signature_payload_parts(&params).unwrap();
2740        let mut payload = call.to_vec();
2741        payload.extend(extra);
2742        payload.extend(additional);
2743        let mut bytes = extrinsic;
2744        let _ = codec::Compact::<u32>::decode(&mut bytes).unwrap();
2745        assert_eq!(u8::decode(&mut bytes).unwrap(), 0x84);
2746        assert_eq!(u8::decode(&mut bytes).unwrap(), 2);
2747        let address = Vec::<u8>::decode(&mut bytes).unwrap();
2748        if crate::keys::is_falcon_crypto(signer.crypto_type()) {
2749            let proof = subtensor_hashed::falcon::SignerAddress::from_bytes(&address).unwrap();
2750            assert_eq!(proof.generation, generation);
2751            assert_eq!(proof.account, signer.public_key_bytes());
2752            assert!(proof.verify(&payload));
2753        } else if is_pq_crypto(signer.crypto_type()) {
2754            let proof =
2755                subtensor_hashed::mldsa::SignerAddress::decode(&mut address.as_slice()).unwrap();
2756            assert_eq!(proof.generation, generation);
2757            assert_eq!(proof.account, signer.public_key_bytes());
2758            assert!(proof.verify(&payload));
2759        } else {
2760            let proof = subtensor_hashed::SignerAddress::decode(&mut address.as_slice()).unwrap();
2761            assert_eq!(proof.generation, generation);
2762            assert_eq!(proof.account, signer.public_key_bytes());
2763            let scheme = u8::decode(&mut bytes).unwrap();
2764            let signature = <[u8; 64]>::decode(&mut bytes).unwrap();
2765            let payload = proof.signing_hash(&payload);
2766            let valid = if scheme == 1 {
2767                sp_core::sr25519::Pair::verify(
2768                    &sp_core::sr25519::Signature::from_raw(signature),
2769                    payload,
2770                    &sp_core::sr25519::Public::from_raw(proof.public_key),
2771                )
2772            } else {
2773                assert_eq!(scheme, 0);
2774                sp_core::ed25519::Pair::verify(
2775                    &sp_core::ed25519::Signature::from_raw(signature),
2776                    payload,
2777                    &sp_core::ed25519::Public::from_raw(proof.public_key),
2778                )
2779            };
2780            assert!(valid);
2781        }
2782    }
2783
2784    fn wallet_modes() -> [u8; 5] {
2785        [
2786            crate::keys::CRYPTO_HASHED,
2787            crate::keys::CRYPTO_HASHED_ED25519,
2788            crate::keys::CRYPTO_MLDSA65,
2789            crate::keys::CRYPTO_FALCON512_HASHED,
2790            crate::keys::CRYPTO_FALCON1024_HASHED,
2791        ]
2792    }
2793
2794    fn shield_events(node: &MockNode, inner_success: bool) -> Vec<u8> {
2795        let dispatch_info = Value::record(vec![
2796            (
2797                "weight".into(),
2798                Value::record(vec![
2799                    ("ref_time".into(), Value::Uint(1)),
2800                    ("proof_size".into(), Value::Uint(0)),
2801                ]),
2802            ),
2803            ("class".into(), Value::str("Normal")),
2804            ("pays_fee".into(), Value::str("Yes")),
2805        ]);
2806        let event = |index, pallet: &str, name: &str, attributes| {
2807            Value::record(vec![
2808                (
2809                    "phase".into(),
2810                    Value::record(vec![("ApplyExtrinsic".into(), Value::Uint(index))]),
2811                ),
2812                (
2813                    "event".into(),
2814                    Value::record(vec![(
2815                        pallet.into(),
2816                        Value::record(vec![(name.into(), attributes)]),
2817                    )]),
2818                ),
2819                ("topics".into(), Value::List(vec![])),
2820            ])
2821        };
2822        let mut records = vec![event(
2823            0,
2824            "System",
2825            "ExtrinsicSuccess",
2826            Value::record(vec![("dispatch_info".into(), dispatch_info.clone())]),
2827        )];
2828        records.push(event(
2829            1,
2830            "System",
2831            if inner_success {
2832                "ExtrinsicSuccess"
2833            } else {
2834                "ExtrinsicFailed"
2835            },
2836            Value::record(if inner_success {
2837                vec![("dispatch_info".into(), dispatch_info)]
2838            } else {
2839                vec![
2840                    ("dispatch_error".into(), Value::str("BadOrigin")),
2841                    ("dispatch_info".into(), dispatch_info),
2842                ]
2843            }),
2844        ));
2845        records.push(event(
2846            1,
2847            "TransactionPayment",
2848            "TransactionFeePaid",
2849            Value::record(vec![
2850                ("who".into(), Value::Bytes(vec![42; 32])),
2851                ("actual_fee".into(), Value::Uint(3)),
2852                ("tip".into(), Value::Uint(0)),
2853            ]),
2854        ));
2855        let runtime = node.client.runtime().unwrap();
2856        let mut encoded = Vec::new();
2857        runtime
2858            .encode_id(
2859                storage_info(&runtime, "System", "Events")
2860                    .unwrap()
2861                    .value_type,
2862                &Value::List(records),
2863                &mut encoded,
2864            )
2865            .unwrap();
2866        encoded
2867    }
2868
2869    fn successful_carrier() -> TxOutcome {
2870        let mut carrier = TxOutcome::pool_rejection("carrier".into(), "success".into());
2871        carrier.success = true;
2872        carrier.block_number = Some(10);
2873        carrier.fee_rao = Some(7);
2874        carrier
2875    }
2876
2877    #[test]
2878    fn shield_reports_inner_dispatch_outcome_and_both_fees() {
2879        for success in [false, true] {
2880            let signer = Keypair::from_seed(&[34; 32], crate::keys::CRYPTO_MLDSA65_FIXED).unwrap();
2881            let node = MockNode::new(&signer);
2882            let inner = vec![42];
2883            let events = shield_events(&node, success);
2884            {
2885                let mut state = node.state.lock().unwrap();
2886                state.included = vec![vec![41], inner.clone()];
2887                state.events = events;
2888            }
2889            let result = node
2890                .client
2891                .resolve_shielded_inner(
2892                    &inner,
2893                    "inner".into(),
2894                    successful_carrier(),
2895                    true,
2896                    Duration::from_secs(5),
2897                )
2898                .unwrap();
2899            assert_eq!(result.success, success);
2900            assert_eq!(
2901                result.error.as_ref().map(|error| error.name.as_str()),
2902                if success { None } else { Some("BadOrigin") }
2903            );
2904            assert_eq!(result.extrinsic_hash, "inner");
2905            assert_eq!(result.extrinsic_index, Some(1));
2906            assert_eq!(result.fee_rao, Some(10));
2907            assert_eq!(result.data["carrier_extrinsic_hash"], Value::str("carrier"));
2908            assert_eq!(result.data["carrier_fee_rao"], Value::Uint(7));
2909            assert_eq!(result.data["inner_fee_rao"], Value::Uint(3));
2910        }
2911    }
2912
2913    #[test]
2914    fn shield_carrier_success_does_not_hide_missing_inner_inclusion() {
2915        let signer = Keypair::from_seed(&[35; 32], crate::keys::CRYPTO_MLDSA65_FIXED).unwrap();
2916        let node = MockNode::new(&signer);
2917        node.state.lock().unwrap().included = vec![vec![41]];
2918        let result = node
2919            .client
2920            .resolve_shielded_inner(
2921                &[42],
2922                "inner".into(),
2923                successful_carrier(),
2924                false,
2925                Duration::from_millis(100),
2926            )
2927            .unwrap();
2928        assert!(!result.success);
2929        assert!(result.message.contains("not included"));
2930        assert_eq!(result.extrinsic_hash, "inner");
2931        assert_eq!(result.fee_rao, Some(7));
2932        assert_eq!(result.data["carrier_extrinsic_hash"], Value::str("carrier"));
2933        assert!(node.state.lock().unwrap().block_reads > 0);
2934    }
2935
2936    #[test]
2937    fn fixed_ms_signs_queued_nonces_without_authority_records() {
2938        for crypto in [
2939            crate::keys::CRYPTO_MLDSA65_FIXED,
2940            crate::keys::CRYPTO_FALCON512,
2941            crate::keys::CRYPTO_FALCON1024,
2942        ] {
2943            let signer = Keypair::from_seed(&[31; 32], crypto).unwrap();
2944            let node = MockNode::new(&signer);
2945            let call = node.call();
2946            node.state.lock().unwrap().pending = true;
2947            for nonce in [NONCE, NONCE + 1, NONCE + 2] {
2948                let (encoded, _) = node
2949                    .client
2950                    .sign_extrinsic(&call, &signer, nonce, None)
2951                    .unwrap();
2952                assert_proof(&node, &signer, &encoded, &call, 0, nonce);
2953            }
2954            let state = node.state.lock().unwrap();
2955            assert!(state.authority_reads.is_empty());
2956            assert!(state.account_reads.is_empty());
2957        }
2958    }
2959
2960    #[test]
2961    fn fixed_ms_shield_signs_both_nonces_with_the_same_key() {
2962        for crypto in [
2963            crate::keys::CRYPTO_MLDSA65_FIXED,
2964            crate::keys::CRYPTO_FALCON512,
2965            crate::keys::CRYPTO_FALCON1024,
2966        ] {
2967            let signer = Keypair::from_seed(&[32; 32], crypto).unwrap();
2968            let node = MockNode::new(&signer);
2969            let call = node.call();
2970            let outcome = node.client.submit_shielded(&call, &signer, false).unwrap();
2971            assert!(!outcome.success); // The server captures, then rejects the carrier.
2972            let state = node.state.lock().unwrap();
2973            assert!(state.authority_reads.is_empty());
2974            assert!(state.account_reads.is_empty());
2975            assert_eq!(state.finalized_reads, 1);
2976            let outer = &state.submitted[0];
2977            let runtime = node.client.runtime().unwrap();
2978            let decoded = runtime.decode_extrinsic(outer, true).unwrap();
2979            let Value::List(args) = field(field(&decoded, "call").unwrap(), "call_args").unwrap()
2980            else {
2981                panic!("decoded call arguments")
2982            };
2983            let ciphertext = args
2984                .iter()
2985                .find_map(|arg| {
2986                    (field(arg, "name") == Some(&Value::str("ciphertext")))
2987                        .then(|| value_bytes(field(arg, "value").unwrap()).unwrap())
2988                })
2989                .unwrap();
2990            let inner = node.decrypt(&ciphertext);
2991            let outer_call = runtime
2992                .compose_call(
2993                    "MevShield",
2994                    "submit_encrypted",
2995                    &Value::record(vec![("ciphertext".into(), Value::Bytes(ciphertext))]),
2996                )
2997                .unwrap();
2998            assert_proof(&node, &signer, outer, &outer_call, 0, NONCE);
2999            assert_proof(&node, &signer, &inner, &call, 0, NONCE + 1);
3000        }
3001    }
3002
3003    #[test]
3004    fn shield_uses_short_mortal_eras_for_every_wallet_mode() {
3005        for crypto in [0, 1, 4, 6, 7, 8] {
3006            let signer = Keypair::from_seed(&[36; 32], crypto).unwrap();
3007            let fixture = Keypair::from_seed(&[36; 32], crate::keys::CRYPTO_HASHED).unwrap();
3008            let node = MockNode::new(if crypto <= 1 { &fixture } else { &signer });
3009            let call = node.call();
3010            let outcome = node.client.submit_shielded(&call, &signer, false).unwrap();
3011            assert!(!outcome.success); // The server captures, then rejects the carrier.
3012            let outer = node.state.lock().unwrap().submitted[0].clone();
3013            let runtime = node.client.runtime().unwrap();
3014            let decoded = runtime.decode_extrinsic(&outer, true).unwrap();
3015            let Value::List(args) = field(field(&decoded, "call").unwrap(), "call_args").unwrap()
3016            else {
3017                panic!("decoded call arguments")
3018            };
3019            let ciphertext = args
3020                .iter()
3021                .find_map(|arg| {
3022                    (field(arg, "name") == Some(&Value::str("ciphertext")))
3023                        .then(|| value_bytes(field(arg, "value").unwrap()).unwrap())
3024                })
3025                .unwrap();
3026            let inner = node.decrypt(&ciphertext);
3027            for extrinsic in [&outer, &inner] {
3028                let decoded = runtime.decode_extrinsic(extrinsic, true).unwrap();
3029                let era = field(&decoded, "era").unwrap();
3030                assert_eq!(tuple_item(era, 0).and_then(as_u128), Some(8));
3031            }
3032            let (ordinary, _) = node
3033                .client
3034                .sign_extrinsic(&call, &signer, NONCE, Some(DEFAULT_ERA_PERIOD))
3035                .unwrap();
3036            let decoded = runtime.decode_extrinsic(&ordinary, true).unwrap();
3037            assert_eq!(
3038                tuple_item(field(&decoded, "era").unwrap(), 0).and_then(as_u128),
3039                Some(64)
3040            );
3041        }
3042    }
3043
3044    #[test]
3045    fn fixed_ms_shield_rejects_exhausted_inner_nonce_before_submitting() {
3046        for crypto in [
3047            crate::keys::CRYPTO_MLDSA65_FIXED,
3048            crate::keys::CRYPTO_FALCON512,
3049            crate::keys::CRYPTO_FALCON1024,
3050        ] {
3051            let signer = Keypair::from_seed(&[33; 32], crypto).unwrap();
3052            let node = MockNode::new(&signer);
3053            node.state.lock().unwrap().nonce_override = Some(u64::from(u32::MAX) - 1);
3054            let error = node
3055                .client
3056                .submit_shielded(&node.call(), &signer, false)
3057                .unwrap_err();
3058            assert!(error.to_string().contains("nonce exhausted"));
3059            assert!(node.state.lock().unwrap().submitted.is_empty());
3060        }
3061    }
3062
3063    #[test]
3064    fn standard_keys_keep_explicit_nonce_signing_without_registered_authority() {
3065        for crypto in [crate::keys::CRYPTO_ED25519, crate::keys::CRYPTO_SR25519] {
3066            let signer = Keypair::from_seed(&[26; 32], crypto).unwrap();
3067            let fixture = Keypair::from_seed(&[26; 32], crate::keys::CRYPTO_HASHED).unwrap();
3068            let node = MockNode::new(&fixture);
3069            let (encoded, _) = node
3070                .client
3071                .sign_extrinsic(&node.call(), &signer, NONCE + 7, None)
3072                .unwrap();
3073            let decoded = node
3074                .client
3075                .runtime()
3076                .unwrap()
3077                .decode_extrinsic(&encoded, true)
3078                .unwrap();
3079            assert_eq!(
3080                field(&decoded, "nonce").and_then(as_u128),
3081                Some(u128::from(NONCE + 7))
3082            );
3083            let state = node.state.lock().unwrap();
3084            assert_eq!(state.finalized_reads, 0);
3085            assert_eq!(state.nonce_reads, 0);
3086            assert!(state.authority_reads.is_empty());
3087            assert!(state.account_reads.is_empty());
3088        }
3089    }
3090
3091    #[test]
3092    fn shield_pair_pins_authority_and_nonce_when_finality_advances() {
3093        for crypto in wallet_modes() {
3094            let signer = Keypair::from_seed(&[27; 32], crypto).unwrap();
3095            let node = MockNode::new(&signer);
3096            node.state.lock().unwrap().advance_after_inner = true;
3097            let call = node.call();
3098            let outcome = node.client.submit_shielded(&call, &signer, false).unwrap();
3099            assert!(!outcome.success); // Mock node only captures the signed carrier.
3100            let state = node.state.lock().unwrap();
3101            assert!(state.advanced);
3102            assert_eq!(state.finalized_reads, 1);
3103            assert_eq!(state.authority_reads, [hex_prefixed(&PINNED_HASH)]);
3104            assert_eq!(state.account_reads, [hex_prefixed(&PINNED_HASH)]);
3105            assert_eq!(state.submitted.len(), 1);
3106            let outer = &state.submitted[0];
3107            let decoded = node
3108                .client
3109                .runtime()
3110                .unwrap()
3111                .decode_extrinsic(outer, true)
3112                .unwrap();
3113            let outer_call = field(&decoded, "call").unwrap();
3114            let args = field(outer_call, "call_args").unwrap();
3115            let Value::List(args) = args else {
3116                panic!("decoded call arguments")
3117            };
3118            let ciphertext = args
3119                .iter()
3120                .find_map(|arg| {
3121                    (field(arg, "name") == Some(&Value::str("ciphertext")))
3122                        .then(|| value_bytes(field(arg, "value").unwrap()).unwrap())
3123                })
3124                .unwrap();
3125            let inner = node.decrypt(&ciphertext);
3126            let encoded_outer = node
3127                .client
3128                .compose_call(
3129                    "MevShield",
3130                    "submit_encrypted",
3131                    &Value::record(vec![("ciphertext".into(), Value::Bytes(ciphertext))]),
3132                )
3133                .unwrap();
3134            assert_proof(&node, &signer, outer, &encoded_outer, GENERATION, NONCE);
3135            assert_proof(&node, &signer, &inner, &call, GENERATION + 1, NONCE + 1);
3136        }
3137    }
3138
3139    #[test]
3140    fn shield_rejects_nonce_consumed_between_inner_and_carrier() {
3141        for crypto in wallet_modes() {
3142            let signer = Keypair::from_seed(&[28; 32], crypto).unwrap();
3143            let node = MockNode::new(&signer);
3144            {
3145                let mut state = node.state.lock().unwrap();
3146                state.advance_after_inner = true;
3147                state.consume_nonce_on_advance = true;
3148            }
3149            let error = node
3150                .client
3151                .submit_shielded(&node.call(), &signer, false)
3152                .unwrap_err();
3153            assert!(error.to_string().contains("pending transaction"));
3154            let state = node.state.lock().unwrap();
3155            assert!(state.advanced);
3156            assert_eq!(state.authority_reads, [hex_prefixed(&PINNED_HASH)]);
3157            assert!(state.submitted.is_empty());
3158        }
3159    }
3160
3161    #[test]
3162    fn registered_submissions_reject_pending_and_explicit_future_nonces_then_refresh() {
3163        for crypto in wallet_modes() {
3164            let signer = Keypair::from_seed(&[29; 32], crypto).unwrap();
3165            let node = MockNode::new(&signer);
3166            let call = node.call();
3167            node.state.lock().unwrap().pending = true;
3168            let error = node
3169                .client
3170                .submit(&call, &signer, None, Some(DEFAULT_ERA_PERIOD), false)
3171                .unwrap_err();
3172            assert!(error.to_string().contains("pending transaction"));
3173            node.state.lock().unwrap().pending = false;
3174            let error = node
3175                .client
3176                .sign_extrinsic(&call, &signer, NONCE + 1, Some(DEFAULT_ERA_PERIOD))
3177                .unwrap_err();
3178            assert!(error.to_string().contains("current on-chain nonce"));
3179            let (first, _) = node
3180                .client
3181                .sign_extrinsic(&call, &signer, NONCE, Some(DEFAULT_ERA_PERIOD))
3182                .unwrap();
3183            assert_proof(&node, &signer, &first, &call, GENERATION, NONCE);
3184            {
3185                let mut state = node.state.lock().unwrap();
3186                state.advanced = true;
3187                state.consume_nonce_on_advance = true;
3188            }
3189            let (next, _) = node
3190                .client
3191                .sign_extrinsic(&call, &signer, NONCE + 1, None)
3192                .unwrap();
3193            assert_proof(&node, &signer, &next, &call, GENERATION + 1, NONCE + 1);
3194            assert!(node.state.lock().unwrap().submitted.is_empty());
3195        }
3196    }
3197}