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::{GovernanceResult, NeuronInfoWire},
27};
28#[cfg(feature = "nns-host")]
29use crate::nns::LiveNnsSource;
30use crate::nns::{
31 MAINNET_GOVERNANCE_CANISTER_ID,
32 governance::{
33 NnsGovernanceReportContext, NnsGovernanceRequest, NnsGovernanceSourceData,
34 NnsGovernanceSourceProvenance, validate_governance_request, validate_source_provenance,
35 },
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(report_context(&request.governance, data.provenance)),
99 data.value.neurons,
100 data.value.next_start_neuron_id,
101 None,
102 ))
103}
104
105pub async fn build_nns_neuron_info_report_with_source(
107 request: &NnsNeuronInfoRequest,
108 source: &dyn NnsNeuronSource,
109) -> Result<NnsNeuronInfoReport, NnsNeuronError> {
110 validate_governance_request(&request.governance)?;
111 let data = source
112 .fetch_neuron(&request.governance, request.neuron_id)
113 .await?;
114 validate_source_provenance(&request.governance.source, &data.provenance)?;
115 if data.value.neuron_id != request.neuron_id {
116 return Err(NnsNeuronError::InvalidResponse {
117 reason: format!(
118 "source returned neuron {}, expected {}",
119 data.value.neuron_id, request.neuron_id
120 ),
121 });
122 }
123 validate_neuron_rows(std::slice::from_ref(&data.value))?;
124 Ok(info_report_from_row(
125 request,
126 NnsNeuronReportProvenance::live(report_context(&request.governance, data.provenance)),
127 data.value,
128 ))
129}
130
131pub type NnsNeuronSourceFuture<'a, T> =
138 Pin<Box<dyn Future<Output = Result<NnsGovernanceSourceData<T>, NnsNeuronError>> + Send + 'a>>;
139
140pub trait NnsNeuronSource: Send + Sync {
147 fn fetch_neuron_page<'a>(
149 &'a self,
150 request: &'a NnsGovernanceRequest,
151 exclusive_start_neuron_id: Option<u64>,
152 page_size: u32,
153 ) -> NnsNeuronSourceFuture<'a, NnsNeuronPage>;
154
155 fn fetch_neuron<'a>(
157 &'a self,
158 request: &'a NnsGovernanceRequest,
159 neuron_id: u64,
160 ) -> NnsNeuronSourceFuture<'a, NnsNeuronRow>;
161}
162
163#[derive(Clone)]
170pub(super) struct NnsNeuronReportProvenance {
171 pub(super) context: NnsGovernanceReportContext,
172 pub(super) cache_path: Option<String>,
173}
174
175impl NnsNeuronReportProvenance {
176 const fn live(context: NnsGovernanceReportContext) -> Self {
177 Self {
178 context,
179 cache_path: None,
180 }
181 }
182}
183
184pub(super) fn list_report_from_rows(
185 request: &NnsNeuronListRequest,
186 provenance: NnsNeuronReportProvenance,
187 neurons: Vec<NnsNeuronRow>,
188 next_start_neuron_id: Option<u64>,
189 total_neuron_count: Option<usize>,
190) -> NnsNeuronListReport {
191 NnsNeuronListReport {
192 context: NnsGovernanceReportContext {
193 schema_version: NNS_NEURON_LIST_REPORT_SCHEMA_VERSION,
194 ..provenance.context
195 },
196 from_cache: provenance.cache_path.is_some(),
197 cache_path: provenance.cache_path,
198 requested_limit: request.limit,
199 exclusive_start_neuron_id: request.exclusive_start_neuron_id,
200 next_start_neuron_id,
201 total_neuron_count,
202 point_in_time_guaranteed: false,
203 returned_neuron_count: neurons.len(),
204 verbose: request.verbose,
205 neurons,
206 }
207}
208
209pub(super) fn info_report_from_row(
210 request: &NnsNeuronInfoRequest,
211 provenance: NnsNeuronReportProvenance,
212 neuron: NnsNeuronRow,
213) -> NnsNeuronInfoReport {
214 NnsNeuronInfoReport {
215 context: NnsGovernanceReportContext {
216 schema_version: NNS_NEURON_INFO_REPORT_SCHEMA_VERSION,
217 ..provenance.context
218 },
219 from_cache: provenance.cache_path.is_some(),
220 cache_path: provenance.cache_path,
221 verbose: request.verbose,
222 neuron,
223 }
224}
225
226pub(super) fn validate_page_size(page_size: u32) -> Result<(), NnsNeuronError> {
227 if (1..=NNS_NEURON_MAX_PAGE_SIZE).contains(&page_size) {
228 Ok(())
229 } else {
230 Err(NnsNeuronError::InvalidPageSize {
231 page_size,
232 max_page_size: NNS_NEURON_MAX_PAGE_SIZE,
233 })
234 }
235}
236
237pub(super) fn validate_neuron_rows(rows: &[NnsNeuronRow]) -> Result<(), NnsNeuronError> {
238 if rows
239 .windows(2)
240 .any(|pair| pair[0].neuron_id >= pair[1].neuron_id)
241 {
242 return Err(NnsNeuronError::InvalidResponse {
243 reason: "neuron ids are not strictly ascending and unique".to_string(),
244 });
245 }
246 Ok(())
247}
248
249pub(super) fn validate_neuron_page(
250 page: &NnsNeuronPage,
251 exclusive_start_neuron_id: Option<u64>,
252 page_size: u32,
253) -> Result<(), NnsNeuronError> {
254 if page.neurons.len() > page_size as usize {
255 return Err(NnsNeuronError::InvalidResponse {
256 reason: format!(
257 "source returned {} rows for page size {page_size}",
258 page.neurons.len()
259 ),
260 });
261 }
262 validate_neuron_rows(&page.neurons)?;
263 if let (Some(start), Some(first)) = (
264 exclusive_start_neuron_id,
265 page.neurons.first().map(|row| row.neuron_id),
266 ) && first <= start
267 {
268 return Err(NnsNeuronError::InvalidResponse {
269 reason: format!("first neuron id {first} is not greater than cursor {start}"),
270 });
271 }
272 let expected_next = (page.neurons.len() == page_size as usize)
273 .then(|| page.neurons.last().map(|row| row.neuron_id))
274 .flatten();
275 if page.next_start_neuron_id != expected_next {
276 return Err(NnsNeuronError::InvalidResponse {
277 reason: format!(
278 "next cursor {:?} does not match expected {:?}",
279 page.next_start_neuron_id, expected_next
280 ),
281 });
282 }
283 Ok(())
284}
285
286fn report_context(
287 request: &NnsGovernanceRequest,
288 source: NnsGovernanceSourceProvenance,
289) -> NnsGovernanceReportContext {
290 NnsGovernanceReportContext {
291 schema_version: 1,
292 network: request.network.clone(),
293 governance_canister_id: MAINNET_GOVERNANCE_CANISTER_ID.to_string(),
294 fetched_at: request.fetched_at.clone(),
295 source,
296 }
297}
298
299#[cfg(any(
300 feature = "nns-host",
301 all(feature = "canister", target_arch = "wasm32")
302))]
303pub(in crate::nns::neuron::report) fn governance_result<Response>(
304 result: impl GovernanceResult<Response>,
305) -> Result<Response, NnsNeuronError> {
306 result
307 .into_result()
308 .map_err(|error| NnsNeuronError::GovernanceResponse {
309 error_type: error.error_type,
310 message: error.error_message,
311 })
312}
313
314#[cfg(any(
315 feature = "nns-host",
316 all(feature = "canister", target_arch = "wasm32"),
317 test
318))]
319pub(in crate::nns::neuron::report) fn map_neuron_info_error(
320 error: NnsNeuronError,
321 neuron_id: u64,
322) -> NnsNeuronError {
323 match error {
324 NnsNeuronError::GovernanceResponse { error_type, .. }
325 if error_type == GOVERNANCE_ERROR_TYPE_NOT_FOUND =>
326 {
327 NnsNeuronError::NeuronNotFound { neuron_id }
328 }
329 error => error,
330 }
331}
332
333#[cfg(any(
334 feature = "nns-host",
335 all(feature = "canister", target_arch = "wasm32")
336))]
337pub(in crate::nns::neuron::report) fn neuron_row_from_wire(
338 wire: NeuronInfoWire,
339) -> Result<NnsNeuronRow, NnsNeuronError> {
340 let neuron_id = wire.id.ok_or(NnsNeuronError::MissingNeuronId)?.id;
341 Ok(NnsNeuronRow {
342 neuron_id,
343 state: wire.state,
344 visibility: wire.visibility,
345
346 neuron_type: wire.neuron_type,
347 stake_e8s: wire.stake_e8s,
348 staked_maturity_e8s_equivalent: wire.staked_maturity_e8s_equivalent,
349 dissolve_delay_seconds: wire.dissolve_delay_seconds,
350 age_seconds: wire.age_seconds,
351 created_timestamp_seconds: wire.created_timestamp_seconds,
352 retrieved_at_timestamp_seconds: wire.retrieved_at_timestamp_seconds,
353 voting_power: wire.voting_power,
354 deciding_voting_power: wire.deciding_voting_power,
355 potential_voting_power: wire.potential_voting_power,
356 voting_power_refreshed_timestamp_seconds: wire.voting_power_refreshed_timestamp_seconds,
357 joined_community_fund_timestamp_seconds: wire.joined_community_fund_timestamp_seconds,
358 eight_year_gang_bonus_base_e8s: wire.eight_year_gang_bonus_base_e8s,
359 known_neuron_data: wire.known_neuron_data.map(|known| NnsKnownNeuronData {
360 name: known.name,
361 description: known.description,
362 links: known.links.unwrap_or_default(),
363 }),
364 recent_ballots: wire
365 .recent_ballots
366 .into_iter()
367 .map(|ballot| NnsNeuronBallotRow {
368 proposal_id: ballot.proposal_id.map(|proposal| proposal.id),
369 vote: ballot.vote,
370 })
371 .collect(),
372 })
373}
374
375#[cfg(test)]
376mod tests {
377 use super::{NnsNeuronError, map_neuron_info_error};
378
379 #[test]
380 fn neuron_info_maps_only_the_native_not_found_error() {
381 let not_found = map_neuron_info_error(
382 NnsNeuronError::GovernanceResponse {
383 error_type: 4,
384 message: "wording is not part of the contract".to_string(),
385 },
386 42,
387 );
388 assert!(matches!(
389 not_found,
390 NnsNeuronError::NeuronNotFound { neuron_id: 42 }
391 ));
392
393 let unrelated = map_neuron_info_error(
394 NnsNeuronError::GovernanceResponse {
395 error_type: 12,
396 message: "not found text must not override the code".to_string(),
397 },
398 42,
399 );
400 assert!(matches!(
401 unrelated,
402 NnsNeuronError::GovernanceResponse { error_type: 12, .. }
403 ));
404 }
405}