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