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