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