Skip to main content

celestia_types/state/
query_delegation.rs

1use celestia_proto::cosmos::base::query::v1beta1::{
2    PageRequest as RawPageRequest, PageResponse as RawPageResponse,
3};
4use celestia_proto::cosmos::staking::v1beta1::{
5    Delegation as RawDelegation, DelegationResponse as RawDelegationResponse,
6    QueryDelegationResponse as RawQueryDelegationResponse,
7    QueryRedelegationsResponse as RawQueryRedelegationsResponse,
8    QueryUnbondingDelegationResponse as RawQueryUnbondingDelegationResponse,
9    Redelegation as RawRedelegation, RedelegationEntry as RawRedelegationEntry,
10    RedelegationEntryResponse as RawRedelegationEntryResponse,
11    RedelegationResponse as RawRedelegationResponse, UnbondingDelegation as RawUnbondingDelegation,
12    UnbondingDelegationEntry as RawUnbondingDelegationEntry,
13};
14use rust_decimal::Decimal;
15use serde::{Deserialize, Serialize};
16use tendermint::Time;
17
18use crate::Height;
19use crate::error::{Error, Result};
20use crate::state::{AccAddress, Coin, ValAddress};
21
22/// Pagination details for the request.
23pub type PageRequest = RawPageRequest;
24/// Pagination details of the response.
25pub type PageResponse = RawPageResponse;
26
27/// Response type for the `QueryDelegation` RPC method.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(
30    try_from = "RawQueryDelegationResponse",
31    into = "RawQueryDelegationResponse"
32)]
33pub struct QueryDelegationResponse {
34    /// Delegation details including shares and current balance.
35    pub response: DelegationResponse,
36}
37
38/// Contains a delegation and its corresponding token balance.
39///
40/// Used in client responses to show both the delegation data and the current
41/// token amount derived from shares using the validator's exchange rate.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct DelegationResponse {
44    /// Delegation data.
45    pub delegation: Delegation,
46    /// Token amount currently represented by the delegation shares.
47    pub balance: u64,
48}
49
50/// Represents a bond with tokens held by an account.
51///
52/// A delegation is owned by one delegator and is associated with the voting power
53/// of a single validator. It encapsulates the relationship and stake details between
54/// a delegator and a validator.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct Delegation {
57    /// Address of the delegator.
58    pub delegator_address: AccAddress,
59    /// Address of the validator.
60    pub validator_address: ValAddress,
61    /// Amount of delegation shares received.
62    pub shares: Decimal,
63}
64
65impl TryFrom<RawQueryDelegationResponse> for QueryDelegationResponse {
66    type Error = Error;
67
68    fn try_from(value: RawQueryDelegationResponse) -> Result<QueryDelegationResponse> {
69        let resp = value
70            .delegation_response
71            .ok_or(Error::MissingDelegationResponse)?;
72
73        let delegation = resp
74            .delegation
75            .ok_or(Error::MissingDelegation)
76            .and_then(|val| {
77                Ok(Delegation {
78                    delegator_address: val.delegator_address.parse()?,
79                    validator_address: val.validator_address.parse()?,
80                    shares: parse_cosmos_dec(&val.shares)?,
81                })
82            })?;
83
84        let balance: Coin = resp.balance.ok_or(Error::MissingBalance)?.try_into()?;
85
86        Ok(QueryDelegationResponse {
87            response: DelegationResponse {
88                delegation,
89                balance: balance.amount(),
90            },
91        })
92    }
93}
94
95impl From<QueryDelegationResponse> for RawQueryDelegationResponse {
96    fn from(value: QueryDelegationResponse) -> Self {
97        let balance = Coin::utia(value.response.balance);
98        let delegation = value.response.delegation;
99        RawQueryDelegationResponse {
100            delegation_response: Some(RawDelegationResponse {
101                delegation: Some(RawDelegation {
102                    delegator_address: delegation.delegator_address.to_string(),
103                    validator_address: delegation.validator_address.to_string(),
104                    shares: cosmos_dec_to_string(&delegation.shares),
105                }),
106                balance: Some(balance.into()),
107            }),
108        }
109    }
110}
111
112/// Response type for the `QueryUnbondingDelegation` RPC method.
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(
115    try_from = "RawQueryUnbondingDelegationResponse",
116    into = "RawQueryUnbondingDelegationResponse"
117)]
118pub struct QueryUnbondingDelegationResponse {
119    /// Unbonding data for the delegator–validator pair.
120    pub unbond: UnbondingDelegation,
121}
122
123/// Represents all unbonding entries between a delegator and a validator, ordered by time.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct UnbondingDelegation {
126    /// Address of the delegator.
127    pub delegator_address: AccAddress,
128    /// Address of the validator.
129    pub validator_address: ValAddress,
130    /// List of unbonding entries, ordered by completion time.
131    pub entries: Vec<UnbondingDelegationEntry>,
132}
133
134/// Represents a single unbonding entry with associated metadata.
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct UnbondingDelegationEntry {
137    /// Block height at which unbonding began.
138    pub creation_height: Height,
139    /// Time when the unbonding will complete.
140    pub completion_time: Time,
141    /// Amount of tokens originally scheduled for unbonding.
142    pub initial_balance: u64,
143    /// Remaining tokens to be released at completion.
144    pub balance: u64,
145    /// Incrementing id that uniquely identifies this entry
146    pub unbonding_id: u64,
147    /// Strictly positive if this entry's unbonding has been stopped by external modules
148    pub unbonding_on_hold_ref_count: i64,
149}
150
151impl From<QueryUnbondingDelegationResponse> for RawQueryUnbondingDelegationResponse {
152    fn from(value: QueryUnbondingDelegationResponse) -> Self {
153        RawQueryUnbondingDelegationResponse {
154            unbond: Some(RawUnbondingDelegation {
155                delegator_address: value.unbond.delegator_address.to_string(),
156                validator_address: value.unbond.validator_address.to_string(),
157                entries: value.unbond.entries.into_iter().map(Into::into).collect(),
158            }),
159        }
160    }
161}
162
163impl TryFrom<RawQueryUnbondingDelegationResponse> for QueryUnbondingDelegationResponse {
164    type Error = Error;
165
166    fn try_from(value: RawQueryUnbondingDelegationResponse) -> Result<Self, Self::Error> {
167        let unbond = value.unbond.ok_or(Error::MissingUnbond)?;
168
169        let delegator_address = unbond.delegator_address.parse()?;
170        let validator_address = unbond.validator_address.parse()?;
171
172        let entries = unbond
173            .entries
174            .into_iter()
175            .map(|entry| {
176                let creation_height = entry.creation_height.try_into()?;
177
178                let completion_time = entry
179                    .completion_time
180                    .ok_or(Error::MissingCompletionTime)?
181                    .try_into()?;
182
183                let initial_balance = entry
184                    .initial_balance
185                    .parse()
186                    .map_err(|_| Error::InvalidBalance(entry.initial_balance))?;
187
188                let balance = entry
189                    .balance
190                    .parse()
191                    .map_err(|_| Error::InvalidBalance(entry.balance))?;
192
193                Ok(UnbondingDelegationEntry {
194                    creation_height,
195                    completion_time,
196                    initial_balance,
197                    balance,
198                    unbonding_id: entry.unbonding_id,
199                    unbonding_on_hold_ref_count: entry.unbonding_on_hold_ref_count,
200                })
201            })
202            .collect::<Result<Vec<_>, Error>>()?;
203
204        Ok(QueryUnbondingDelegationResponse {
205            unbond: UnbondingDelegation {
206                delegator_address,
207                validator_address,
208                entries,
209            },
210        })
211    }
212}
213
214impl From<UnbondingDelegationEntry> for RawUnbondingDelegationEntry {
215    fn from(value: UnbondingDelegationEntry) -> Self {
216        RawUnbondingDelegationEntry {
217            creation_height: value.creation_height.into(),
218            completion_time: Some(value.completion_time.into()),
219            initial_balance: value.initial_balance.to_string(),
220            balance: value.balance.to_string(),
221            unbonding_id: value.unbonding_id,
222            unbonding_on_hold_ref_count: value.unbonding_on_hold_ref_count,
223        }
224    }
225}
226
227/// Response type for the `QueryRedelegations` RPC method.
228#[derive(Debug, Clone, Serialize, Deserialize)]
229#[serde(
230    try_from = "RawQueryRedelegationsResponse",
231    into = "RawQueryRedelegationsResponse"
232)]
233pub struct QueryRedelegationsResponse {
234    /// List of redelegation responses, one per delegator–validator pair.
235    pub responses: Vec<RedelegationResponse>,
236    /// Pagination details of the response.
237    pub pagination: Option<PageResponse>,
238}
239
240/// A redelegation record formatted for client responses.
241///
242/// Similar to [`Redelegation`], but each entry in `entries` includes a token balance
243/// in addition to the original redelegation metadata.
244#[derive(Debug, Clone, Serialize, Deserialize)]
245pub struct RedelegationResponse {
246    /// Redelegation metadata (delegator, source, and destination validators).
247    pub redelegation: Redelegation,
248
249    /// Redelegation entries with token balances.
250    ///
251    /// Mirrors `redelegation.entries`, but each entry includes a `balance`
252    /// field representing the current token amount.
253    pub entries: Vec<RedelegationEntryResponse>,
254}
255
256/// Represents redelegation activity from one validator to another for a given delegator.
257///
258/// Contains all redelegation entries between a specific source and destination validator.
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct Redelegation {
261    /// Address of the delegator.
262    pub delegator_address: AccAddress,
263    /// Address of the source validator.
264    pub src_validator_address: ValAddress,
265    /// Address of the destination validator.
266    pub dest_validator_address: ValAddress,
267    /// List of individual redelegation entries.
268    pub entries: Vec<RedelegationEntry>,
269}
270
271/// A redelegation entry along with the token balance it represents.
272///
273/// Used in client responses to include both the redelegation metadata and
274/// the token amount associated with the destination validator.
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct RedelegationEntryResponse {
277    /// Original redelegation entry.
278    pub redelegation_entry: RedelegationEntry,
279    /// Token amount represented by this redelegation entry.
280    pub balance: u64,
281}
282
283/// Represents a single redelegation entry with related metadata.
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct RedelegationEntry {
286    /// Block height at which redelegation began.
287    pub creation_height: Height,
288    /// Time when the redelegation will complete.
289    pub completion_time: Time,
290    /// Token amount at the start of redelegation.
291    pub initial_balance: u64,
292    /// Amount of shares created in the destination validator.
293    pub dest_shares: Decimal,
294    /// Incrementing id that uniquely identifies this entry
295    pub unbonding_id: u64,
296    /// Strictly positive if this entry's unbonding has been stopped by external modules
297    pub unbonding_on_hold_ref_count: i64,
298}
299
300impl TryFrom<RawRedelegation> for Redelegation {
301    type Error = Error;
302
303    fn try_from(value: RawRedelegation) -> Result<Self, Self::Error> {
304        let entries = value
305            .entries
306            .into_iter()
307            .map(|entry| entry.try_into())
308            .collect::<Result<Vec<_>>>()?;
309
310        Ok(Redelegation {
311            delegator_address: value.delegator_address.parse()?,
312            src_validator_address: value.validator_src_address.parse()?,
313            dest_validator_address: value.validator_dst_address.parse()?,
314            entries,
315        })
316    }
317}
318
319impl From<RedelegationEntry> for RawRedelegationEntry {
320    fn from(value: RedelegationEntry) -> Self {
321        RawRedelegationEntry {
322            creation_height: value.creation_height.into(),
323            completion_time: Some(value.completion_time.into()),
324            initial_balance: value.initial_balance.to_string(),
325            shares_dst: cosmos_dec_to_string(&value.dest_shares),
326            unbonding_id: value.unbonding_id,
327            unbonding_on_hold_ref_count: value.unbonding_on_hold_ref_count,
328        }
329    }
330}
331
332impl TryFrom<RawRedelegationEntry> for RedelegationEntry {
333    type Error = Error;
334
335    fn try_from(value: RawRedelegationEntry) -> Result<Self, Self::Error> {
336        let initial_balance = value
337            .initial_balance
338            .as_str()
339            .parse::<u64>()
340            .map_err(|_| Error::InvalidBalance(value.initial_balance))?;
341
342        Ok(RedelegationEntry {
343            creation_height: value.creation_height.try_into()?,
344            completion_time: value
345                .completion_time
346                .ok_or(Error::MissingCompletionTime)?
347                .try_into()?,
348            initial_balance,
349            dest_shares: parse_cosmos_dec(&value.shares_dst)?,
350            unbonding_id: value.unbonding_id,
351            unbonding_on_hold_ref_count: value.unbonding_on_hold_ref_count,
352        })
353    }
354}
355
356impl From<QueryRedelegationsResponse> for RawQueryRedelegationsResponse {
357    fn from(value: QueryRedelegationsResponse) -> Self {
358        RawQueryRedelegationsResponse {
359            redelegation_responses: value
360                .responses
361                .into_iter()
362                .map(|response| RawRedelegationResponse {
363                    redelegation: Some(RawRedelegation {
364                        delegator_address: response.redelegation.delegator_address.to_string(),
365                        validator_src_address: response
366                            .redelegation
367                            .src_validator_address
368                            .to_string(),
369                        validator_dst_address: response
370                            .redelegation
371                            .dest_validator_address
372                            .to_string(),
373                        entries: response
374                            .redelegation
375                            .entries
376                            .into_iter()
377                            .map(Into::into)
378                            .collect(),
379                    }),
380                    entries: response
381                        .entries
382                        .into_iter()
383                        .map(|entry| RawRedelegationEntryResponse {
384                            redelegation_entry: Some(entry.redelegation_entry.into()),
385                            balance: entry.balance.to_string(),
386                        })
387                        .collect(),
388                })
389                .collect(),
390            pagination: value.pagination,
391        }
392    }
393}
394
395impl TryFrom<RawQueryRedelegationsResponse> for QueryRedelegationsResponse {
396    type Error = Error;
397
398    fn try_from(value: RawQueryRedelegationsResponse) -> Result<Self, Self::Error> {
399        let redelegation_responses = value
400            .redelegation_responses
401            .into_iter()
402            .map(|resp| {
403                let redelegation = resp
404                    .redelegation
405                    .ok_or(Error::MissingRedelegation)?
406                    .try_into()?;
407
408                let entries = resp
409                    .entries
410                    .into_iter()
411                    .map(|resp| {
412                        let redelegation_entry = resp
413                            .redelegation_entry
414                            .ok_or(Error::MissingRedelegationEntry)?
415                            .try_into()?;
416
417                        let balance = resp
418                            .balance
419                            .as_str()
420                            .parse::<u64>()
421                            .map_err(|_| Error::InvalidBalance(resp.balance))?;
422
423                        Ok(RedelegationEntryResponse {
424                            redelegation_entry,
425                            balance,
426                        })
427                    })
428                    .collect::<Result<Vec<_>>>()?;
429
430                Ok(RedelegationResponse {
431                    redelegation,
432                    entries,
433                })
434            })
435            .collect::<Result<Vec<_>>>()?;
436
437        Ok(QueryRedelegationsResponse {
438            responses: redelegation_responses,
439            pagination: value.pagination,
440        })
441    }
442}
443
444const FIXED_POINT_EXPONENT: u32 = 18;
445
446/// Parse Cosmos decimal
447///
448/// A Cosmos decimal is serialized as string and has 18 decimal places.
449///
450/// Ref: https://github.com/celestiaorg/cosmos-sdk/blob/5259747ebf054c2148032202d946945a2d1896c7/math/dec.go#L21
451fn parse_cosmos_dec(s: &str) -> Result<Decimal> {
452    let val = s
453        .parse::<i128>()
454        .map_err(|_| Error::InvalidCosmosDecimal(s.to_owned()))?;
455    let val = Decimal::try_from_i128_with_scale(val, FIXED_POINT_EXPONENT)
456        .map_err(|_| Error::InvalidCosmosDecimal(s.to_owned()))?;
457    Ok(val)
458}
459
460/// Encode Cosmos decimal. Should be inverse of [`parse_cosmos_dec`]
461fn cosmos_dec_to_string(d: &Decimal) -> String {
462    let mantissa = d.mantissa();
463    debug_assert_eq!(d.scale(), FIXED_POINT_EXPONENT);
464    format!("{mantissa}")
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn string_decimal_round_trip() {
473        let epsilon = "1";
474        let v = parse_cosmos_dec(epsilon).unwrap();
475        assert_eq!(cosmos_dec_to_string(&v), epsilon);
476        assert_eq!(v, Decimal::new(1, FIXED_POINT_EXPONENT));
477
478        let unit = "1000000000000000000";
479        let v = parse_cosmos_dec(unit).unwrap();
480        assert_eq!(cosmos_dec_to_string(&v), unit);
481        assert_eq!(u32::try_from(v).unwrap(), 1);
482    }
483}