Skip to main content

ic_query/nns/neuron/report/source/
mod.rs

1//! Module: nns::neuron::report::source
2//!
3//! Responsibility: build NNS neuron reports from a portable async source.
4//! Does not own: CLI parsing, cache IO, transport internals, or text rendering.
5//! Boundary: native, canister, and custom sources converge before report assembly.
6
7#[cfg(any(
8    feature = "nns-host",
9    all(feature = "canister", target_arch = "wasm32")
10))]
11use crate::nns::governance::{GovernanceError, NnsGovernanceNeuronId};
12#[cfg(all(feature = "canister", target_arch = "wasm32"))]
13mod canister;
14#[cfg(feature = "nns-host")]
15mod host;
16
17use super::{
18    NNS_NEURON_INFO_REPORT_SCHEMA_VERSION, NNS_NEURON_LIST_REPORT_SCHEMA_VERSION,
19    NNS_NEURON_MAX_PAGE_SIZE, NnsNeuronError,
20    model::{
21        NnsNeuronInfoReport, NnsNeuronInfoRequest, NnsNeuronListReport, NnsNeuronListRequest,
22        NnsNeuronRow,
23    },
24};
25#[cfg(any(
26    feature = "nns-host",
27    all(feature = "canister", target_arch = "wasm32")
28))]
29use super::{
30    model::{NnsKnownNeuronData, NnsNeuronBallotRow},
31    wire::{
32        GetNeuronIndexRequestWire, GetNeuronIndexResultWire, GetNeuronInfoResultWire,
33        NeuronInfoWire,
34    },
35};
36#[cfg(feature = "nns-host")]
37use crate::nns::LiveNnsSource;
38use crate::nns::governance::{
39    NnsGovernanceReportContext, NnsGovernanceRequest, NnsGovernanceSourceData,
40    validate_governance_request, validate_source_provenance,
41};
42
43#[cfg(any(
44    feature = "nns-host",
45    all(feature = "canister", target_arch = "wasm32"),
46    test
47))]
48const GOVERNANCE_ERROR_TYPE_NOT_FOUND: i32 = 4;
49
50///
51/// NnsNeuronPage
52///
53/// One canonically ascending page from the public Governance neuron index.
54///
55
56#[derive(Clone, Debug, Eq, PartialEq)]
57pub struct NnsNeuronPage {
58    /// Public neuron rows returned by Governance.
59    pub neurons: Vec<NnsNeuronRow>,
60    /// Exclusive lower bound for a possible next page.
61    pub next_start_neuron_id: Option<u64>,
62}
63
64/// Build one bounded neuron-index page through the native replica adapter.
65#[cfg(feature = "nns-host")]
66pub async fn build_nns_neuron_list_report(
67    request: &NnsNeuronListRequest,
68) -> Result<NnsNeuronListReport, super::NnsNeuronHostError> {
69    Ok(build_nns_neuron_list_report_with_source(request, &LiveNnsSource::default()).await?)
70}
71
72/// Build one exact neuron detail through the native replica adapter.
73#[cfg(feature = "nns-host")]
74pub async fn build_nns_neuron_info_report(
75    request: &NnsNeuronInfoRequest,
76) -> Result<NnsNeuronInfoReport, super::NnsNeuronHostError> {
77    Ok(build_nns_neuron_info_report_with_source(request, &LiveNnsSource::default()).await?)
78}
79
80/// Build one bounded NNS neuron-index page from a caller-owned async source.
81pub async fn build_nns_neuron_list_report_with_source(
82    request: &NnsNeuronListRequest,
83    source: &dyn NnsNeuronSource,
84) -> Result<NnsNeuronListReport, NnsNeuronError> {
85    validate_governance_request(&request.governance)?;
86    validate_page_size(request.limit)?;
87    let data = source
88        .fetch_neuron_page(
89            &request.governance,
90            request.exclusive_start_neuron_id,
91            request.limit,
92        )
93        .await?;
94    validate_source_provenance(&request.governance.source, &data.provenance)?;
95    validate_neuron_page(
96        &data.value,
97        request.exclusive_start_neuron_id,
98        request.limit,
99    )?;
100    Ok(list_report_from_rows(
101        request,
102        NnsNeuronReportProvenance::live(NnsGovernanceReportContext::from_source(
103            &request.governance,
104            data.provenance,
105        )),
106        data.value.neurons,
107        data.value.next_start_neuron_id,
108        None,
109    ))
110}
111
112/// Build one exact NNS neuron detail from a caller-owned async source.
113pub async fn build_nns_neuron_info_report_with_source(
114    request: &NnsNeuronInfoRequest,
115    source: &dyn NnsNeuronSource,
116) -> Result<NnsNeuronInfoReport, NnsNeuronError> {
117    validate_governance_request(&request.governance)?;
118    let data = source
119        .fetch_neuron(&request.governance, request.neuron_id)
120        .await?;
121    validate_source_provenance(&request.governance.source, &data.provenance)?;
122    if data.value.neuron_id != request.neuron_id {
123        return Err(NnsNeuronError::InvalidResponse {
124            reason: format!(
125                "source returned neuron {}, expected {}",
126                data.value.neuron_id, request.neuron_id
127            ),
128        });
129    }
130    validate_neuron_rows(std::slice::from_ref(&data.value))?;
131    Ok(info_report_from_row(
132        NnsNeuronReportProvenance::live(NnsGovernanceReportContext::from_source(
133            &request.governance,
134            data.provenance,
135        )),
136        data.value,
137    ))
138}
139
140///
141/// NnsNeuronSourceFuture
142///
143/// Boxed caller-runtime future returned by a neuron source.
144///
145
146pub type NnsNeuronSourceFuture<'a, T> =
147    crate::report::SourceFuture<'a, Result<NnsGovernanceSourceData<T>, NnsNeuronError>>;
148
149///
150/// NnsNeuronSource
151///
152/// Portable async capability for bounded neuron list and exact detail calls.
153///
154
155pub trait NnsNeuronSource: Send + Sync {
156    /// Fetch at most one bounded page from the public neuron index.
157    fn fetch_neuron_page<'a>(
158        &'a self,
159        request: &'a NnsGovernanceRequest,
160        exclusive_start_neuron_id: Option<u64>,
161        page_size: u32,
162    ) -> NnsNeuronSourceFuture<'a, NnsNeuronPage>;
163
164    /// Fetch one exact public limited neuron view.
165    fn fetch_neuron<'a>(
166        &'a self,
167        request: &'a NnsGovernanceRequest,
168        neuron_id: u64,
169    ) -> NnsNeuronSourceFuture<'a, NnsNeuronRow>;
170}
171
172///
173/// NnsNeuronReportProvenance
174///
175/// Shared live or cached provenance used while assembling neuron reports.
176///
177
178#[derive(Clone)]
179pub(super) struct NnsNeuronReportProvenance {
180    pub(super) context: NnsGovernanceReportContext,
181    pub(super) cache_path: Option<String>,
182}
183
184impl NnsNeuronReportProvenance {
185    const fn live(context: NnsGovernanceReportContext) -> Self {
186        Self {
187            context,
188            cache_path: None,
189        }
190    }
191}
192
193pub(super) fn list_report_from_rows(
194    request: &NnsNeuronListRequest,
195    provenance: NnsNeuronReportProvenance,
196    neurons: Vec<NnsNeuronRow>,
197    next_start_neuron_id: Option<u64>,
198    total_neuron_count: Option<usize>,
199) -> NnsNeuronListReport {
200    NnsNeuronListReport {
201        context: NnsGovernanceReportContext {
202            schema_version: NNS_NEURON_LIST_REPORT_SCHEMA_VERSION,
203            ..provenance.context
204        },
205        provenance: provenance
206            .cache_path
207            .map_or(crate::report::ReportProvenance::Live, |cache_path| {
208                crate::report::ReportProvenance::Cache { cache_path }
209            }),
210        requested_limit: request.limit,
211        exclusive_start_neuron_id: request.exclusive_start_neuron_id,
212        next_start_neuron_id,
213        total_neuron_count,
214        point_in_time_guaranteed: false,
215
216        neurons,
217    }
218}
219
220pub(super) fn info_report_from_row(
221    provenance: NnsNeuronReportProvenance,
222    neuron: NnsNeuronRow,
223) -> NnsNeuronInfoReport {
224    NnsNeuronInfoReport {
225        context: NnsGovernanceReportContext {
226            schema_version: NNS_NEURON_INFO_REPORT_SCHEMA_VERSION,
227            ..provenance.context
228        },
229        provenance: provenance
230            .cache_path
231            .map_or(crate::report::ReportProvenance::Live, |cache_path| {
232                crate::report::ReportProvenance::Cache { cache_path }
233            }),
234        neuron,
235    }
236}
237
238pub(super) fn validate_page_size(page_size: u32) -> Result<(), NnsNeuronError> {
239    if (1..=NNS_NEURON_MAX_PAGE_SIZE).contains(&page_size) {
240        Ok(())
241    } else {
242        Err(NnsNeuronError::InvalidPageSize {
243            page_size,
244            max_page_size: NNS_NEURON_MAX_PAGE_SIZE,
245        })
246    }
247}
248
249pub(super) fn validate_neuron_rows(rows: &[NnsNeuronRow]) -> Result<(), NnsNeuronError> {
250    if rows
251        .windows(2)
252        .any(|pair| pair[0].neuron_id >= pair[1].neuron_id)
253    {
254        return Err(NnsNeuronError::InvalidResponse {
255            reason: "neuron ids are not strictly ascending and unique".to_string(),
256        });
257    }
258    Ok(())
259}
260
261pub(super) fn validate_neuron_page(
262    page: &NnsNeuronPage,
263    exclusive_start_neuron_id: Option<u64>,
264    page_size: u32,
265) -> Result<(), NnsNeuronError> {
266    if page.neurons.len() > page_size as usize {
267        return Err(NnsNeuronError::InvalidResponse {
268            reason: format!(
269                "source returned {} rows for page size {page_size}",
270                page.neurons.len()
271            ),
272        });
273    }
274    validate_neuron_rows(&page.neurons)?;
275    if let (Some(start), Some(first)) = (
276        exclusive_start_neuron_id,
277        page.neurons.first().map(|row| row.neuron_id),
278    ) && first <= start
279    {
280        return Err(NnsNeuronError::InvalidResponse {
281            reason: format!("first neuron id {first} is not greater than cursor {start}"),
282        });
283    }
284    let expected_next = (page.neurons.len() == page_size as usize)
285        .then(|| page.neurons.last().map(|row| row.neuron_id))
286        .flatten();
287    if page.next_start_neuron_id != expected_next {
288        return Err(NnsNeuronError::InvalidResponse {
289            reason: format!(
290                "next cursor {:?} does not match expected {:?}",
291                page.next_start_neuron_id, expected_next
292            ),
293        });
294    }
295    Ok(())
296}
297
298#[cfg(any(
299    feature = "nns-host",
300    all(feature = "canister", target_arch = "wasm32")
301))]
302pub(in crate::nns::neuron::report) fn governance_result<Response>(
303    result: Result<Response, GovernanceError>,
304) -> Result<Response, NnsNeuronError> {
305    result.map_err(|error| NnsNeuronError::GovernanceResponse {
306        error_type: error.error_type,
307        message: error.error_message,
308    })
309}
310
311#[cfg(any(
312    feature = "nns-host",
313    all(feature = "canister", target_arch = "wasm32"),
314    test
315))]
316pub(in crate::nns::neuron::report) fn map_neuron_info_error(
317    error: NnsNeuronError,
318    neuron_id: u64,
319) -> NnsNeuronError {
320    match error {
321        NnsNeuronError::GovernanceResponse { error_type, .. }
322            if error_type == GOVERNANCE_ERROR_TYPE_NOT_FOUND =>
323        {
324            NnsNeuronError::NeuronNotFound { neuron_id }
325        }
326        error => error,
327    }
328}
329
330#[cfg(any(
331    feature = "nns-host",
332    all(feature = "canister", target_arch = "wasm32")
333))]
334pub(in crate::nns::neuron::report) fn neuron_row_from_wire(
335    wire: NeuronInfoWire,
336) -> Result<NnsNeuronRow, NnsNeuronError> {
337    let neuron_id = wire.id.ok_or(NnsNeuronError::MissingNeuronId)?.id;
338    Ok(NnsNeuronRow {
339        neuron_id,
340        state: wire.state,
341        visibility: wire.visibility,
342
343        neuron_type: wire.neuron_type,
344        stake_e8s: wire.stake_e8s,
345        staked_maturity_e8s_equivalent: wire.staked_maturity_e8s_equivalent,
346        dissolve_delay_seconds: wire.dissolve_delay_seconds,
347        age_seconds: wire.age_seconds,
348        created_timestamp_seconds: wire.created_timestamp_seconds,
349        retrieved_at_timestamp_seconds: wire.retrieved_at_timestamp_seconds,
350        voting_power: wire.voting_power,
351        deciding_voting_power: wire.deciding_voting_power,
352        potential_voting_power: wire.potential_voting_power,
353        voting_power_refreshed_timestamp_seconds: wire.voting_power_refreshed_timestamp_seconds,
354        joined_community_fund_timestamp_seconds: wire.joined_community_fund_timestamp_seconds,
355        eight_year_gang_bonus_base_e8s: wire.eight_year_gang_bonus_base_e8s,
356        known_neuron_data: wire.known_neuron_data.map(|known| NnsKnownNeuronData {
357            name: known.name,
358            description: known.description,
359            links: known.links.unwrap_or_default(),
360        }),
361        recent_ballots: wire
362            .recent_ballots
363            .into_iter()
364            .map(|ballot| NnsNeuronBallotRow {
365                proposal_id: ballot.proposal_id.map(|proposal| proposal.id),
366                vote: ballot.vote,
367            })
368            .collect(),
369    })
370}
371
372#[cfg(any(
373    feature = "nns-host",
374    all(feature = "canister", target_arch = "wasm32")
375))]
376fn neuron_page_request(
377    exclusive_start_neuron_id: Option<u64>,
378    page_size: u32,
379) -> GetNeuronIndexRequestWire {
380    GetNeuronIndexRequestWire {
381        exclusive_start_neuron_id: exclusive_start_neuron_id.map(|id| NnsGovernanceNeuronId { id }),
382        page_size: Some(page_size),
383    }
384}
385
386#[cfg(any(
387    feature = "nns-host",
388    all(feature = "canister", target_arch = "wasm32")
389))]
390fn neuron_page_from_wire(
391    result: GetNeuronIndexResultWire,
392    page_size: u32,
393) -> Result<NnsNeuronPage, NnsNeuronError> {
394    let neurons = governance_result(result)?
395        .neurons
396        .into_iter()
397        .map(neuron_row_from_wire)
398        .collect::<Result<Vec<_>, _>>()?;
399    let next_start_neuron_id = (neurons.len() == page_size as usize)
400        .then(|| neurons.last().map(|row| row.neuron_id))
401        .flatten();
402    Ok(NnsNeuronPage {
403        neurons,
404        next_start_neuron_id,
405    })
406}
407
408#[cfg(any(
409    feature = "nns-host",
410    all(feature = "canister", target_arch = "wasm32")
411))]
412fn neuron_row_from_response(
413    result: GetNeuronInfoResultWire,
414    neuron_id: u64,
415) -> Result<NnsNeuronRow, NnsNeuronError> {
416    let wire =
417        governance_result(result).map_err(|error| map_neuron_info_error(error, neuron_id))?;
418    neuron_row_from_wire(*wire)
419}
420
421#[cfg(test)]
422mod tests;