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