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