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