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