1#[cfg(any(
8 feature = "nns-host",
9 all(feature = "canister", target_arch = "wasm32")
10))]
11use crate::nns::governance::{GovernanceError, NnsGovernanceNeuronId};
12#[cfg(all(feature = "canister", target_arch = "wasm32"))]
13mod canister;
14#[cfg(feature = "nns-host")]
15mod host;
16
17use crate::report::ReportProvenance;
18
19use super::{
20 NNS_NEURON_INFO_REPORT_SCHEMA_VERSION, NNS_NEURON_LIST_REPORT_SCHEMA_VERSION,
21 NNS_NEURON_MAX_PAGE_SIZE, NnsNeuronError,
22 model::{
23 NnsNeuronInfoReport, NnsNeuronInfoRequest, NnsNeuronListReport, NnsNeuronListRequest,
24 NnsNeuronRow,
25 },
26};
27#[cfg(any(
28 feature = "nns-host",
29 all(feature = "canister", target_arch = "wasm32")
30))]
31use super::{
32 model::{NnsKnownNeuronData, NnsNeuronBallotRow},
33 wire::{
34 GetNeuronIndexRequestWire, GetNeuronIndexResultWire, GetNeuronInfoResultWire,
35 NeuronInfoWire,
36 },
37};
38#[cfg(feature = "nns-host")]
39use crate::nns::LiveNnsSource;
40use crate::nns::governance::{
41 NnsGovernanceReportContext, NnsGovernanceRequest, NnsGovernanceSourceData,
42 validate_governance_request, validate_source_provenance,
43};
44
45#[cfg(any(
46 feature = "nns-host",
47 all(feature = "canister", target_arch = "wasm32"),
48 test
49))]
50const GOVERNANCE_ERROR_TYPE_NOT_FOUND: i32 = 4;
51
52#[derive(Clone, Debug, Eq, PartialEq)]
59pub struct NnsNeuronPage {
60 pub neurons: Vec<NnsNeuronRow>,
62 pub next_start_neuron_id: Option<u64>,
64}
65
66#[cfg(feature = "nns-host")]
68pub async fn build_nns_neuron_list_report(
69 request: &NnsNeuronListRequest,
70) -> Result<NnsNeuronListReport, super::NnsNeuronHostError> {
71 Ok(build_nns_neuron_list_report_with_source(request, &LiveNnsSource::default()).await?)
72}
73
74#[cfg(feature = "nns-host")]
76pub async fn build_nns_neuron_info_report(
77 request: &NnsNeuronInfoRequest,
78) -> Result<NnsNeuronInfoReport, super::NnsNeuronHostError> {
79 Ok(build_nns_neuron_info_report_with_source(request, &LiveNnsSource::default()).await?)
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 NnsGovernanceReportContext::from_source(&request.governance, data.provenance),
105 ReportProvenance::live(),
106 data.value.neurons,
107 data.value.next_start_neuron_id,
108 None,
109 ))
110}
111
112pub async fn build_nns_neuron_info_report_with_source(
114 request: &NnsNeuronInfoRequest,
115 source: &dyn NnsNeuronSource,
116) -> Result<NnsNeuronInfoReport, NnsNeuronError> {
117 validate_governance_request(&request.governance)?;
118 let data = source
119 .fetch_neuron(&request.governance, request.neuron_id)
120 .await?;
121 validate_source_provenance(&request.governance.source, &data.provenance)?;
122 if data.value.neuron_id != request.neuron_id {
123 return Err(NnsNeuronError::InvalidResponse {
124 reason: format!(
125 "source returned neuron {}, expected {}",
126 data.value.neuron_id, request.neuron_id
127 ),
128 });
129 }
130 validate_neuron_rows(std::slice::from_ref(&data.value))?;
131 Ok(info_report_from_row(
132 NnsGovernanceReportContext::from_source(&request.governance, data.provenance),
133 ReportProvenance::live(),
134 data.value,
135 ))
136}
137
138pub type NnsNeuronSourceFuture<'a, T> =
145 crate::report::SourceFuture<'a, Result<NnsGovernanceSourceData<T>, NnsNeuronError>>;
146
147pub trait NnsNeuronSource: Send + Sync {
154 fn fetch_neuron_page<'a>(
156 &'a self,
157 request: &'a NnsGovernanceRequest,
158 exclusive_start_neuron_id: Option<u64>,
159 page_size: u32,
160 ) -> NnsNeuronSourceFuture<'a, NnsNeuronPage>;
161
162 fn fetch_neuron<'a>(
164 &'a self,
165 request: &'a NnsGovernanceRequest,
166 neuron_id: u64,
167 ) -> NnsNeuronSourceFuture<'a, NnsNeuronRow>;
168}
169
170pub(super) fn list_report_from_rows(
171 request: &NnsNeuronListRequest,
172 context: NnsGovernanceReportContext,
173 provenance: ReportProvenance,
174 neurons: Vec<NnsNeuronRow>,
175 next_start_neuron_id: Option<u64>,
176 total_neuron_count: Option<usize>,
177) -> NnsNeuronListReport {
178 NnsNeuronListReport {
179 context: NnsGovernanceReportContext {
180 schema_version: NNS_NEURON_LIST_REPORT_SCHEMA_VERSION,
181 ..context
182 },
183 provenance,
184 requested_limit: request.limit,
185 exclusive_start_neuron_id: request.exclusive_start_neuron_id,
186 next_start_neuron_id,
187 total_neuron_count,
188 point_in_time_guaranteed: false,
189
190 neurons,
191 }
192}
193
194pub(super) fn info_report_from_row(
195 context: NnsGovernanceReportContext,
196 provenance: ReportProvenance,
197 neuron: NnsNeuronRow,
198) -> NnsNeuronInfoReport {
199 NnsNeuronInfoReport {
200 context: NnsGovernanceReportContext {
201 schema_version: NNS_NEURON_INFO_REPORT_SCHEMA_VERSION,
202 ..context
203 },
204 provenance,
205 neuron,
206 }
207}
208
209pub(super) fn validate_page_size(page_size: u32) -> Result<(), NnsNeuronError> {
210 if (1..=NNS_NEURON_MAX_PAGE_SIZE).contains(&page_size) {
211 Ok(())
212 } else {
213 Err(NnsNeuronError::InvalidPageSize {
214 page_size,
215 max_page_size: NNS_NEURON_MAX_PAGE_SIZE,
216 })
217 }
218}
219
220pub(super) fn validate_neuron_rows(rows: &[NnsNeuronRow]) -> Result<(), NnsNeuronError> {
221 if rows
222 .windows(2)
223 .any(|pair| pair[0].neuron_id >= pair[1].neuron_id)
224 {
225 return Err(NnsNeuronError::InvalidResponse {
226 reason: "neuron ids are not strictly ascending and unique".to_string(),
227 });
228 }
229 Ok(())
230}
231
232pub(super) fn validate_neuron_page(
233 page: &NnsNeuronPage,
234 exclusive_start_neuron_id: Option<u64>,
235 page_size: u32,
236) -> Result<(), NnsNeuronError> {
237 if page.neurons.len() > page_size as usize {
238 return Err(NnsNeuronError::InvalidResponse {
239 reason: format!(
240 "source returned {} rows for page size {page_size}",
241 page.neurons.len()
242 ),
243 });
244 }
245 validate_neuron_rows(&page.neurons)?;
246 if let (Some(start), Some(first)) = (
247 exclusive_start_neuron_id,
248 page.neurons.first().map(|row| row.neuron_id),
249 ) && first <= start
250 {
251 return Err(NnsNeuronError::InvalidResponse {
252 reason: format!("first neuron id {first} is not greater than cursor {start}"),
253 });
254 }
255 let expected_next = (page.neurons.len() == page_size as usize)
256 .then(|| page.neurons.last().map(|row| row.neuron_id))
257 .flatten();
258 if page.next_start_neuron_id != expected_next {
259 return Err(NnsNeuronError::InvalidResponse {
260 reason: format!(
261 "next cursor {:?} does not match expected {:?}",
262 page.next_start_neuron_id, expected_next
263 ),
264 });
265 }
266 Ok(())
267}
268
269#[cfg(any(
270 feature = "nns-host",
271 all(feature = "canister", target_arch = "wasm32")
272))]
273pub(in crate::nns::neuron::report) fn governance_result<Response>(
274 result: Result<Response, GovernanceError>,
275) -> Result<Response, NnsNeuronError> {
276 result.map_err(|error| NnsNeuronError::GovernanceResponse {
277 error_type: error.error_type,
278 message: error.error_message,
279 })
280}
281
282#[cfg(any(
283 feature = "nns-host",
284 all(feature = "canister", target_arch = "wasm32"),
285 test
286))]
287pub(in crate::nns::neuron::report) fn map_neuron_info_error(
288 error: NnsNeuronError,
289 neuron_id: u64,
290) -> NnsNeuronError {
291 match error {
292 NnsNeuronError::GovernanceResponse { error_type, .. }
293 if error_type == GOVERNANCE_ERROR_TYPE_NOT_FOUND =>
294 {
295 NnsNeuronError::NeuronNotFound { neuron_id }
296 }
297 error => error,
298 }
299}
300
301#[cfg(any(
302 feature = "nns-host",
303 all(feature = "canister", target_arch = "wasm32")
304))]
305pub(in crate::nns::neuron::report) fn neuron_row_from_wire(
306 wire: NeuronInfoWire,
307) -> Result<NnsNeuronRow, NnsNeuronError> {
308 let neuron_id = wire.id.ok_or(NnsNeuronError::MissingNeuronId)?.id;
309 Ok(NnsNeuronRow {
310 neuron_id,
311 state: wire.state,
312 visibility: wire.visibility,
313
314 neuron_type: wire.neuron_type,
315 stake_e8s: wire.stake_e8s,
316 staked_maturity_e8s_equivalent: wire.staked_maturity_e8s_equivalent,
317 dissolve_delay_seconds: wire.dissolve_delay_seconds,
318 age_seconds: wire.age_seconds,
319 created_timestamp_seconds: wire.created_timestamp_seconds,
320 retrieved_at_timestamp_seconds: wire.retrieved_at_timestamp_seconds,
321 voting_power: wire.voting_power,
322 deciding_voting_power: wire.deciding_voting_power,
323 potential_voting_power: wire.potential_voting_power,
324 voting_power_refreshed_timestamp_seconds: wire.voting_power_refreshed_timestamp_seconds,
325 joined_community_fund_timestamp_seconds: wire.joined_community_fund_timestamp_seconds,
326 eight_year_gang_bonus_base_e8s: wire.eight_year_gang_bonus_base_e8s,
327 known_neuron_data: wire.known_neuron_data.map(|known| NnsKnownNeuronData {
328 name: known.name,
329 description: known.description,
330 links: known.links.unwrap_or_default(),
331 }),
332 recent_ballots: wire
333 .recent_ballots
334 .into_iter()
335 .map(|ballot| NnsNeuronBallotRow {
336 proposal_id: ballot.proposal_id.map(|proposal| proposal.id),
337 vote: ballot.vote,
338 })
339 .collect(),
340 })
341}
342
343#[cfg(any(
344 feature = "nns-host",
345 all(feature = "canister", target_arch = "wasm32")
346))]
347fn neuron_page_request(
348 exclusive_start_neuron_id: Option<u64>,
349 page_size: u32,
350) -> GetNeuronIndexRequestWire {
351 GetNeuronIndexRequestWire {
352 exclusive_start_neuron_id: exclusive_start_neuron_id.map(|id| NnsGovernanceNeuronId { id }),
353 page_size: Some(page_size),
354 }
355}
356
357#[cfg(any(
358 feature = "nns-host",
359 all(feature = "canister", target_arch = "wasm32")
360))]
361fn neuron_page_from_wire(
362 result: GetNeuronIndexResultWire,
363 page_size: u32,
364) -> Result<NnsNeuronPage, NnsNeuronError> {
365 let neurons = governance_result(result)?
366 .neurons
367 .into_iter()
368 .map(neuron_row_from_wire)
369 .collect::<Result<Vec<_>, _>>()?;
370 let next_start_neuron_id = (neurons.len() == page_size as usize)
371 .then(|| neurons.last().map(|row| row.neuron_id))
372 .flatten();
373 Ok(NnsNeuronPage {
374 neurons,
375 next_start_neuron_id,
376 })
377}
378
379#[cfg(any(
380 feature = "nns-host",
381 all(feature = "canister", target_arch = "wasm32")
382))]
383fn neuron_row_from_response(
384 result: GetNeuronInfoResultWire,
385 neuron_id: u64,
386) -> Result<NnsNeuronRow, NnsNeuronError> {
387 let wire =
388 governance_result(result).map_err(|error| map_neuron_info_error(error, neuron_id))?;
389 neuron_row_from_wire(*wire)
390}
391
392#[cfg(test)]
393mod tests;