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