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 model::{
16 NnsNeuronInfoReport, NnsNeuronInfoRequest, NnsNeuronListReport, NnsNeuronListRequest,
17 NnsNeuronRow,
18 },
19};
20#[cfg(any(
21 feature = "nns-host",
22 all(feature = "canister", target_arch = "wasm32")
23))]
24use super::{
25 model::{NnsKnownNeuronData, NnsNeuronBallotRow},
26 wire::{
27 GetNeuronIndexRequestWire, GetNeuronIndexResultWire, GovernanceResult, NeuronIdWire,
28 NeuronInfoWire,
29 },
30};
31#[cfg(feature = "nns-host")]
32use crate::nns::LiveNnsSource;
33use crate::nns::governance::{
34 NnsGovernanceReportContext, NnsGovernanceRequest, NnsGovernanceSourceData,
35 validate_governance_request, validate_source_provenance,
36};
37
38#[cfg(any(
39 feature = "nns-host",
40 all(feature = "canister", target_arch = "wasm32"),
41 test
42))]
43const GOVERNANCE_ERROR_TYPE_NOT_FOUND: i32 = 4;
44
45#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct NnsNeuronPage {
53 pub neurons: Vec<NnsNeuronRow>,
55 pub next_start_neuron_id: Option<u64>,
57}
58
59#[cfg(feature = "nns-host")]
61pub async fn build_nns_neuron_list_report(
62 request: &NnsNeuronListRequest,
63) -> Result<NnsNeuronListReport, super::NnsNeuronHostError> {
64 Ok(build_nns_neuron_list_report_with_source(request, &LiveNnsSource::default()).await?)
65}
66
67#[cfg(feature = "nns-host")]
69pub async fn build_nns_neuron_info_report(
70 request: &NnsNeuronInfoRequest,
71) -> Result<NnsNeuronInfoReport, super::NnsNeuronHostError> {
72 Ok(build_nns_neuron_info_report_with_source(request, &LiveNnsSource::default()).await?)
73}
74
75pub async fn build_nns_neuron_list_report_with_source(
77 request: &NnsNeuronListRequest,
78 source: &dyn NnsNeuronSource,
79) -> Result<NnsNeuronListReport, NnsNeuronError> {
80 validate_governance_request(&request.governance)?;
81 validate_page_size(request.limit)?;
82 let data = source
83 .fetch_neuron_page(
84 &request.governance,
85 request.exclusive_start_neuron_id,
86 request.limit,
87 )
88 .await?;
89 validate_source_provenance(&request.governance.source, &data.provenance)?;
90 validate_neuron_page(
91 &data.value,
92 request.exclusive_start_neuron_id,
93 request.limit,
94 )?;
95 Ok(list_report_from_rows(
96 request,
97 NnsNeuronReportProvenance::live(NnsGovernanceReportContext::from_source(
98 &request.governance,
99 data.provenance,
100 )),
101 data.value.neurons,
102 data.value.next_start_neuron_id,
103 None,
104 ))
105}
106
107pub async fn build_nns_neuron_info_report_with_source(
109 request: &NnsNeuronInfoRequest,
110 source: &dyn NnsNeuronSource,
111) -> Result<NnsNeuronInfoReport, NnsNeuronError> {
112 validate_governance_request(&request.governance)?;
113 let data = source
114 .fetch_neuron(&request.governance, request.neuron_id)
115 .await?;
116 validate_source_provenance(&request.governance.source, &data.provenance)?;
117 if data.value.neuron_id != request.neuron_id {
118 return Err(NnsNeuronError::InvalidResponse {
119 reason: format!(
120 "source returned neuron {}, expected {}",
121 data.value.neuron_id, request.neuron_id
122 ),
123 });
124 }
125 validate_neuron_rows(std::slice::from_ref(&data.value))?;
126 Ok(info_report_from_row(
127 request,
128 NnsNeuronReportProvenance::live(NnsGovernanceReportContext::from_source(
129 &request.governance,
130 data.provenance,
131 )),
132 data.value,
133 ))
134}
135
136pub type NnsNeuronSourceFuture<'a, T> =
143 crate::report::SourceFuture<'a, Result<NnsGovernanceSourceData<T>, NnsNeuronError>>;
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 if rows
244 .windows(2)
245 .any(|pair| pair[0].neuron_id >= pair[1].neuron_id)
246 {
247 return Err(NnsNeuronError::InvalidResponse {
248 reason: "neuron ids are not strictly ascending and unique".to_string(),
249 });
250 }
251 Ok(())
252}
253
254pub(super) fn validate_neuron_page(
255 page: &NnsNeuronPage,
256 exclusive_start_neuron_id: Option<u64>,
257 page_size: u32,
258) -> Result<(), NnsNeuronError> {
259 if page.neurons.len() > page_size as usize {
260 return Err(NnsNeuronError::InvalidResponse {
261 reason: format!(
262 "source returned {} rows for page size {page_size}",
263 page.neurons.len()
264 ),
265 });
266 }
267 validate_neuron_rows(&page.neurons)?;
268 if let (Some(start), Some(first)) = (
269 exclusive_start_neuron_id,
270 page.neurons.first().map(|row| row.neuron_id),
271 ) && first <= start
272 {
273 return Err(NnsNeuronError::InvalidResponse {
274 reason: format!("first neuron id {first} is not greater than cursor {start}"),
275 });
276 }
277 let expected_next = (page.neurons.len() == page_size as usize)
278 .then(|| page.neurons.last().map(|row| row.neuron_id))
279 .flatten();
280 if page.next_start_neuron_id != expected_next {
281 return Err(NnsNeuronError::InvalidResponse {
282 reason: format!(
283 "next cursor {:?} does not match expected {:?}",
284 page.next_start_neuron_id, expected_next
285 ),
286 });
287 }
288 Ok(())
289}
290
291#[cfg(any(
292 feature = "nns-host",
293 all(feature = "canister", target_arch = "wasm32")
294))]
295pub(in crate::nns::neuron::report) fn governance_result<Response>(
296 result: impl GovernanceResult<Response>,
297) -> Result<Response, NnsNeuronError> {
298 result
299 .into_result()
300 .map_err(|error| NnsNeuronError::GovernanceResponse {
301 error_type: error.error_type,
302 message: error.error_message,
303 })
304}
305
306#[cfg(any(
307 feature = "nns-host",
308 all(feature = "canister", target_arch = "wasm32"),
309 test
310))]
311pub(in crate::nns::neuron::report) fn map_neuron_info_error(
312 error: NnsNeuronError,
313 neuron_id: u64,
314) -> NnsNeuronError {
315 match error {
316 NnsNeuronError::GovernanceResponse { error_type, .. }
317 if error_type == GOVERNANCE_ERROR_TYPE_NOT_FOUND =>
318 {
319 NnsNeuronError::NeuronNotFound { neuron_id }
320 }
321 error => error,
322 }
323}
324
325#[cfg(any(
326 feature = "nns-host",
327 all(feature = "canister", target_arch = "wasm32")
328))]
329pub(in crate::nns::neuron::report) fn neuron_row_from_wire(
330 wire: NeuronInfoWire,
331) -> Result<NnsNeuronRow, NnsNeuronError> {
332 let neuron_id = wire.id.ok_or(NnsNeuronError::MissingNeuronId)?.id;
333 Ok(NnsNeuronRow {
334 neuron_id,
335 state: wire.state,
336 visibility: wire.visibility,
337
338 neuron_type: wire.neuron_type,
339 stake_e8s: wire.stake_e8s,
340 staked_maturity_e8s_equivalent: wire.staked_maturity_e8s_equivalent,
341 dissolve_delay_seconds: wire.dissolve_delay_seconds,
342 age_seconds: wire.age_seconds,
343 created_timestamp_seconds: wire.created_timestamp_seconds,
344 retrieved_at_timestamp_seconds: wire.retrieved_at_timestamp_seconds,
345 voting_power: wire.voting_power,
346 deciding_voting_power: wire.deciding_voting_power,
347 potential_voting_power: wire.potential_voting_power,
348 voting_power_refreshed_timestamp_seconds: wire.voting_power_refreshed_timestamp_seconds,
349 joined_community_fund_timestamp_seconds: wire.joined_community_fund_timestamp_seconds,
350 eight_year_gang_bonus_base_e8s: wire.eight_year_gang_bonus_base_e8s,
351 known_neuron_data: wire.known_neuron_data.map(|known| NnsKnownNeuronData {
352 name: known.name,
353 description: known.description,
354 links: known.links.unwrap_or_default(),
355 }),
356 recent_ballots: wire
357 .recent_ballots
358 .into_iter()
359 .map(|ballot| NnsNeuronBallotRow {
360 proposal_id: ballot.proposal_id.map(|proposal| proposal.id),
361 vote: ballot.vote,
362 })
363 .collect(),
364 })
365}
366
367#[cfg(any(
368 feature = "nns-host",
369 all(feature = "canister", target_arch = "wasm32")
370))]
371fn neuron_page_request(
372 exclusive_start_neuron_id: Option<u64>,
373 page_size: u32,
374) -> GetNeuronIndexRequestWire {
375 GetNeuronIndexRequestWire {
376 exclusive_start_neuron_id: exclusive_start_neuron_id.map(|id| NeuronIdWire { id }),
377 page_size: Some(page_size),
378 }
379}
380
381#[cfg(any(
382 feature = "nns-host",
383 all(feature = "canister", target_arch = "wasm32")
384))]
385fn neuron_page_from_wire(
386 result: GetNeuronIndexResultWire,
387 page_size: u32,
388) -> Result<NnsNeuronPage, NnsNeuronError> {
389 let neurons = governance_result(result)?
390 .neurons
391 .into_iter()
392 .map(neuron_row_from_wire)
393 .collect::<Result<Vec<_>, _>>()?;
394 let next_start_neuron_id = (neurons.len() == page_size as usize)
395 .then(|| neurons.last().map(|row| row.neuron_id))
396 .flatten();
397 Ok(NnsNeuronPage {
398 neurons,
399 next_start_neuron_id,
400 })
401}
402
403#[cfg(test)]
404mod tests {
405 use super::{NnsNeuronError, map_neuron_info_error};
406
407 #[test]
408 fn neuron_info_maps_only_the_native_not_found_error() {
409 let not_found = map_neuron_info_error(
410 NnsNeuronError::GovernanceResponse {
411 error_type: 4,
412 message: "wording is not part of the contract".to_string(),
413 },
414 42,
415 );
416 assert!(matches!(
417 not_found,
418 NnsNeuronError::NeuronNotFound { neuron_id: 42 }
419 ));
420
421 let unrelated = map_neuron_info_error(
422 NnsNeuronError::GovernanceResponse {
423 error_type: 12,
424 message: "not found text must not override the code".to_string(),
425 },
426 42,
427 );
428 assert!(matches!(
429 unrelated,
430 NnsNeuronError::GovernanceResponse { error_type: 12, .. }
431 ));
432 }
433}