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, NnsNeuronVote},
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
265 .recent_ballots
266 .iter()
267 .find(|ballot| ballot.vote_text != NnsNeuronVote::from_code(ballot.vote))
268 {
269 return Err(NnsNeuronError::InvalidResponse {
270 reason: format!(
271 "neuron {} ballot vote classification {} does not match raw code {}",
272 row.neuron_id, ballot.vote_text, ballot.vote
273 ),
274 });
275 }
276 }
277 if rows
278 .windows(2)
279 .any(|pair| pair[0].neuron_id >= pair[1].neuron_id)
280 {
281 return Err(NnsNeuronError::InvalidResponse {
282 reason: "neuron ids are not strictly ascending and unique".to_string(),
283 });
284 }
285 Ok(())
286}
287
288pub(super) fn validate_neuron_page(
289 page: &NnsNeuronPage,
290 exclusive_start_neuron_id: Option<u64>,
291 page_size: u32,
292) -> Result<(), NnsNeuronError> {
293 if page.neurons.len() > page_size as usize {
294 return Err(NnsNeuronError::InvalidResponse {
295 reason: format!(
296 "source returned {} rows for page size {page_size}",
297 page.neurons.len()
298 ),
299 });
300 }
301 validate_neuron_rows(&page.neurons)?;
302 if let (Some(start), Some(first)) = (
303 exclusive_start_neuron_id,
304 page.neurons.first().map(|row| row.neuron_id),
305 ) && first <= start
306 {
307 return Err(NnsNeuronError::InvalidResponse {
308 reason: format!("first neuron id {first} is not greater than cursor {start}"),
309 });
310 }
311 let expected_next = (page.neurons.len() == page_size as usize)
312 .then(|| page.neurons.last().map(|row| row.neuron_id))
313 .flatten();
314 if page.next_start_neuron_id != expected_next {
315 return Err(NnsNeuronError::InvalidResponse {
316 reason: format!(
317 "next cursor {:?} does not match expected {:?}",
318 page.next_start_neuron_id, expected_next
319 ),
320 });
321 }
322 Ok(())
323}
324
325fn report_context(
326 request: &NnsGovernanceRequest,
327 source: NnsGovernanceSourceProvenance,
328) -> NnsGovernanceReportContext {
329 NnsGovernanceReportContext {
330 schema_version: 1,
331 network: request.network.clone(),
332 governance_canister_id: MAINNET_GOVERNANCE_CANISTER_ID.to_string(),
333 fetched_at: request.fetched_at.clone(),
334 source,
335 }
336}
337
338#[cfg(any(
339 feature = "nns-host",
340 all(feature = "canister", target_arch = "wasm32")
341))]
342pub(in crate::nns::neuron::report) fn governance_result<Response>(
343 result: impl GovernanceResult<Response>,
344) -> Result<Response, NnsNeuronError> {
345 result
346 .into_result()
347 .map_err(|error| NnsNeuronError::GovernanceResponse {
348 error_type: error.error_type,
349 message: error.error_message,
350 })
351}
352
353#[cfg(any(
354 feature = "nns-host",
355 all(feature = "canister", target_arch = "wasm32"),
356 test
357))]
358pub(in crate::nns::neuron::report) fn map_neuron_info_error(
359 error: NnsNeuronError,
360 neuron_id: u64,
361) -> NnsNeuronError {
362 match error {
363 NnsNeuronError::GovernanceResponse { error_type, .. }
364 if error_type == GOVERNANCE_ERROR_TYPE_NOT_FOUND =>
365 {
366 NnsNeuronError::NeuronNotFound { neuron_id }
367 }
368 error => error,
369 }
370}
371
372#[cfg(any(
373 feature = "nns-host",
374 all(feature = "canister", target_arch = "wasm32")
375))]
376pub(in crate::nns::neuron::report) fn neuron_row_from_wire(
377 wire: NeuronInfoWire,
378) -> Result<NnsNeuronRow, NnsNeuronError> {
379 let neuron_id = wire.id.ok_or(NnsNeuronError::MissingNeuronId)?.id;
380 Ok(NnsNeuronRow {
381 neuron_id,
382 state: wire.state,
383 state_text: NnsNeuronState::from_code(wire.state),
384 visibility: wire.visibility,
385 visibility_text: NnsNeuronVisibility::from_code(wire.visibility),
386 neuron_type: wire.neuron_type,
387 neuron_type_text: NnsNeuronType::from_code(wire.neuron_type),
388 stake_e8s: wire.stake_e8s,
389 staked_maturity_e8s_equivalent: wire.staked_maturity_e8s_equivalent,
390 dissolve_delay_seconds: wire.dissolve_delay_seconds,
391 age_seconds: wire.age_seconds,
392 created_timestamp_seconds: wire.created_timestamp_seconds,
393 retrieved_at_timestamp_seconds: wire.retrieved_at_timestamp_seconds,
394 voting_power: wire.voting_power,
395 deciding_voting_power: wire.deciding_voting_power,
396 potential_voting_power: wire.potential_voting_power,
397 voting_power_refreshed_timestamp_seconds: wire.voting_power_refreshed_timestamp_seconds,
398 joined_community_fund_timestamp_seconds: wire.joined_community_fund_timestamp_seconds,
399 eight_year_gang_bonus_base_e8s: wire.eight_year_gang_bonus_base_e8s,
400 known_neuron_data: wire.known_neuron_data.map(|known| NnsKnownNeuronData {
401 name: known.name,
402 description: known.description,
403 links: known.links.unwrap_or_default(),
404 }),
405 recent_ballots: wire
406 .recent_ballots
407 .into_iter()
408 .map(|ballot| NnsNeuronBallotRow {
409 proposal_id: ballot.proposal_id.map(|proposal| proposal.id),
410 vote: ballot.vote,
411 vote_text: NnsNeuronVote::from_code(ballot.vote),
412 })
413 .collect(),
414 })
415}
416
417#[cfg(test)]
418mod tests {
419 use super::{NnsNeuronError, map_neuron_info_error};
420
421 #[test]
422 fn neuron_info_maps_only_the_native_not_found_error() {
423 let not_found = map_neuron_info_error(
424 NnsNeuronError::GovernanceResponse {
425 error_type: 4,
426 message: "wording is not part of the contract".to_string(),
427 },
428 42,
429 );
430 assert!(matches!(
431 not_found,
432 NnsNeuronError::NeuronNotFound { neuron_id: 42 }
433 ));
434
435 let unrelated = map_neuron_info_error(
436 NnsNeuronError::GovernanceResponse {
437 error_type: 12,
438 message: "not found text must not override the code".to_string(),
439 },
440 42,
441 );
442 assert!(matches!(
443 unrelated,
444 NnsNeuronError::GovernanceResponse { error_type: 12, .. }
445 ));
446 }
447}