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 NnsNeuronReportProvenance::live(NnsGovernanceReportContext::from_source(
133 &request.governance,
134 data.provenance,
135 )),
136 data.value,
137 ))
138}
139
140pub type NnsNeuronSourceFuture<'a, T> =
147 crate::report::SourceFuture<'a, Result<NnsGovernanceSourceData<T>, NnsNeuronError>>;
148
149pub trait NnsNeuronSource: Send + Sync {
156 fn fetch_neuron_page<'a>(
158 &'a self,
159 request: &'a NnsGovernanceRequest,
160 exclusive_start_neuron_id: Option<u64>,
161 page_size: u32,
162 ) -> NnsNeuronSourceFuture<'a, NnsNeuronPage>;
163
164 fn fetch_neuron<'a>(
166 &'a self,
167 request: &'a NnsGovernanceRequest,
168 neuron_id: u64,
169 ) -> NnsNeuronSourceFuture<'a, NnsNeuronRow>;
170}
171
172#[derive(Clone)]
179pub(super) struct NnsNeuronReportProvenance {
180 pub(super) context: NnsGovernanceReportContext,
181 pub(super) cache_path: Option<String>,
182}
183
184impl NnsNeuronReportProvenance {
185 const fn live(context: NnsGovernanceReportContext) -> Self {
186 Self {
187 context,
188 cache_path: None,
189 }
190 }
191}
192
193pub(super) fn list_report_from_rows(
194 request: &NnsNeuronListRequest,
195 provenance: NnsNeuronReportProvenance,
196 neurons: Vec<NnsNeuronRow>,
197 next_start_neuron_id: Option<u64>,
198 total_neuron_count: Option<usize>,
199) -> NnsNeuronListReport {
200 NnsNeuronListReport {
201 context: NnsGovernanceReportContext {
202 schema_version: NNS_NEURON_LIST_REPORT_SCHEMA_VERSION,
203 ..provenance.context
204 },
205 provenance: provenance
206 .cache_path
207 .map_or(crate::report::ReportProvenance::Live, |cache_path| {
208 crate::report::ReportProvenance::Cache { cache_path }
209 }),
210 requested_limit: request.limit,
211 exclusive_start_neuron_id: request.exclusive_start_neuron_id,
212 next_start_neuron_id,
213 total_neuron_count,
214 point_in_time_guaranteed: false,
215 returned_neuron_count: neurons.len(),
216 neurons,
217 }
218}
219
220pub(super) fn info_report_from_row(
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 provenance: provenance
230 .cache_path
231 .map_or(crate::report::ReportProvenance::Live, |cache_path| {
232 crate::report::ReportProvenance::Cache { cache_path }
233 }),
234 neuron,
235 }
236}
237
238pub(super) fn validate_page_size(page_size: u32) -> Result<(), NnsNeuronError> {
239 if (1..=NNS_NEURON_MAX_PAGE_SIZE).contains(&page_size) {
240 Ok(())
241 } else {
242 Err(NnsNeuronError::InvalidPageSize {
243 page_size,
244 max_page_size: NNS_NEURON_MAX_PAGE_SIZE,
245 })
246 }
247}
248
249pub(super) fn validate_neuron_rows(rows: &[NnsNeuronRow]) -> Result<(), NnsNeuronError> {
250 if rows
251 .windows(2)
252 .any(|pair| pair[0].neuron_id >= pair[1].neuron_id)
253 {
254 return Err(NnsNeuronError::InvalidResponse {
255 reason: "neuron ids are not strictly ascending and unique".to_string(),
256 });
257 }
258 Ok(())
259}
260
261pub(super) fn validate_neuron_page(
262 page: &NnsNeuronPage,
263 exclusive_start_neuron_id: Option<u64>,
264 page_size: u32,
265) -> Result<(), NnsNeuronError> {
266 if page.neurons.len() > page_size as usize {
267 return Err(NnsNeuronError::InvalidResponse {
268 reason: format!(
269 "source returned {} rows for page size {page_size}",
270 page.neurons.len()
271 ),
272 });
273 }
274 validate_neuron_rows(&page.neurons)?;
275 if let (Some(start), Some(first)) = (
276 exclusive_start_neuron_id,
277 page.neurons.first().map(|row| row.neuron_id),
278 ) && first <= start
279 {
280 return Err(NnsNeuronError::InvalidResponse {
281 reason: format!("first neuron id {first} is not greater than cursor {start}"),
282 });
283 }
284 let expected_next = (page.neurons.len() == page_size as usize)
285 .then(|| page.neurons.last().map(|row| row.neuron_id))
286 .flatten();
287 if page.next_start_neuron_id != expected_next {
288 return Err(NnsNeuronError::InvalidResponse {
289 reason: format!(
290 "next cursor {:?} does not match expected {:?}",
291 page.next_start_neuron_id, expected_next
292 ),
293 });
294 }
295 Ok(())
296}
297
298#[cfg(any(
299 feature = "nns-host",
300 all(feature = "canister", target_arch = "wasm32")
301))]
302pub(in crate::nns::neuron::report) fn governance_result<Response>(
303 result: Result<Response, GovernanceError>,
304) -> Result<Response, NnsNeuronError> {
305 result.map_err(|error| NnsNeuronError::GovernanceResponse {
306 error_type: error.error_type,
307 message: error.error_message,
308 })
309}
310
311#[cfg(any(
312 feature = "nns-host",
313 all(feature = "canister", target_arch = "wasm32"),
314 test
315))]
316pub(in crate::nns::neuron::report) fn map_neuron_info_error(
317 error: NnsNeuronError,
318 neuron_id: u64,
319) -> NnsNeuronError {
320 match error {
321 NnsNeuronError::GovernanceResponse { error_type, .. }
322 if error_type == GOVERNANCE_ERROR_TYPE_NOT_FOUND =>
323 {
324 NnsNeuronError::NeuronNotFound { neuron_id }
325 }
326 error => error,
327 }
328}
329
330#[cfg(any(
331 feature = "nns-host",
332 all(feature = "canister", target_arch = "wasm32")
333))]
334pub(in crate::nns::neuron::report) fn neuron_row_from_wire(
335 wire: NeuronInfoWire,
336) -> Result<NnsNeuronRow, NnsNeuronError> {
337 let neuron_id = wire.id.ok_or(NnsNeuronError::MissingNeuronId)?.id;
338 Ok(NnsNeuronRow {
339 neuron_id,
340 state: wire.state,
341 visibility: wire.visibility,
342
343 neuron_type: wire.neuron_type,
344 stake_e8s: wire.stake_e8s,
345 staked_maturity_e8s_equivalent: wire.staked_maturity_e8s_equivalent,
346 dissolve_delay_seconds: wire.dissolve_delay_seconds,
347 age_seconds: wire.age_seconds,
348 created_timestamp_seconds: wire.created_timestamp_seconds,
349 retrieved_at_timestamp_seconds: wire.retrieved_at_timestamp_seconds,
350 voting_power: wire.voting_power,
351 deciding_voting_power: wire.deciding_voting_power,
352 potential_voting_power: wire.potential_voting_power,
353 voting_power_refreshed_timestamp_seconds: wire.voting_power_refreshed_timestamp_seconds,
354 joined_community_fund_timestamp_seconds: wire.joined_community_fund_timestamp_seconds,
355 eight_year_gang_bonus_base_e8s: wire.eight_year_gang_bonus_base_e8s,
356 known_neuron_data: wire.known_neuron_data.map(|known| NnsKnownNeuronData {
357 name: known.name,
358 description: known.description,
359 links: known.links.unwrap_or_default(),
360 }),
361 recent_ballots: wire
362 .recent_ballots
363 .into_iter()
364 .map(|ballot| NnsNeuronBallotRow {
365 proposal_id: ballot.proposal_id.map(|proposal| proposal.id),
366 vote: ballot.vote,
367 })
368 .collect(),
369 })
370}
371
372#[cfg(any(
373 feature = "nns-host",
374 all(feature = "canister", target_arch = "wasm32")
375))]
376fn neuron_page_request(
377 exclusive_start_neuron_id: Option<u64>,
378 page_size: u32,
379) -> GetNeuronIndexRequestWire {
380 GetNeuronIndexRequestWire {
381 exclusive_start_neuron_id: exclusive_start_neuron_id.map(|id| NnsGovernanceNeuronId { id }),
382 page_size: Some(page_size),
383 }
384}
385
386#[cfg(any(
387 feature = "nns-host",
388 all(feature = "canister", target_arch = "wasm32")
389))]
390fn neuron_page_from_wire(
391 result: GetNeuronIndexResultWire,
392 page_size: u32,
393) -> Result<NnsNeuronPage, NnsNeuronError> {
394 let neurons = governance_result(result)?
395 .neurons
396 .into_iter()
397 .map(neuron_row_from_wire)
398 .collect::<Result<Vec<_>, _>>()?;
399 let next_start_neuron_id = (neurons.len() == page_size as usize)
400 .then(|| neurons.last().map(|row| row.neuron_id))
401 .flatten();
402 Ok(NnsNeuronPage {
403 neurons,
404 next_start_neuron_id,
405 })
406}
407
408#[cfg(any(
409 feature = "nns-host",
410 all(feature = "canister", target_arch = "wasm32")
411))]
412fn neuron_row_from_response(
413 result: GetNeuronInfoResultWire,
414 neuron_id: u64,
415) -> Result<NnsNeuronRow, NnsNeuronError> {
416 let wire =
417 governance_result(result).map_err(|error| map_neuron_info_error(error, neuron_id))?;
418 neuron_row_from_wire(*wire)
419}
420
421#[cfg(test)]
422mod tests {
423 use super::{NnsNeuronError, map_neuron_info_error};
424
425 #[test]
426 fn neuron_info_maps_only_the_native_not_found_error() {
427 let not_found = map_neuron_info_error(
428 NnsNeuronError::GovernanceResponse {
429 error_type: 4,
430 message: "wording is not part of the contract".to_string(),
431 },
432 42,
433 );
434 assert!(matches!(
435 not_found,
436 NnsNeuronError::NeuronNotFound { neuron_id: 42 }
437 ));
438
439 let unrelated = map_neuron_info_error(
440 NnsNeuronError::GovernanceResponse {
441 error_type: 12,
442 message: "not found text must not override the code".to_string(),
443 },
444 42,
445 );
446 assert!(matches!(
447 unrelated,
448 NnsNeuronError::GovernanceResponse { error_type: 12, .. }
449 ));
450 }
451}