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