Skip to main content

bee/debug/
accounting.rs

1//! Accounting / balances / stake endpoints. Mirrors bee-go's
2//! `pkg/debug/{accounting,stake}.go`.
3
4use std::collections::HashMap;
5
6use bytes::Bytes;
7use num_bigint::BigInt;
8use reqwest::Method;
9use serde::{Deserialize, Deserializer};
10
11use crate::client::request;
12use crate::swarm::Error;
13
14use super::DebugApi;
15
16/// Settlement balance with one peer. Mirrors bee-go `Balance`.
17#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
18pub struct Balance {
19    /// Peer overlay address.
20    pub peer: String,
21    /// Settlement balance (PLUR).
22    #[serde(deserialize_with = "deserialize_bigint")]
23    pub balance: BigInt,
24}
25
26fn deserialize_bigint<'de, D>(d: D) -> Result<BigInt, D::Error>
27where
28    D: Deserializer<'de>,
29{
30    let s: String = Deserialize::deserialize(d)?;
31    if s.is_empty() {
32        return Ok(BigInt::from(0));
33    }
34    s.parse::<BigInt>().map_err(serde::de::Error::custom)
35}
36
37fn deserialize_opt_bigint<'de, D>(d: D) -> Result<Option<BigInt>, D::Error>
38where
39    D: Deserializer<'de>,
40{
41    let s: String = Deserialize::deserialize(d)?;
42    if s.is_empty() {
43        return Ok(None);
44    }
45    s.parse::<BigInt>()
46        .map(Some)
47        .map_err(serde::de::Error::custom)
48}
49
50/// Full per-peer accounting state (richer than [`Balance`]). All
51/// monetary fields are PLUR.
52#[derive(Clone, Debug, PartialEq, Eq, Default, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct PeerAccounting {
55    /// Live settlement balance.
56    #[serde(default, deserialize_with = "deserialize_opt_bigint")]
57    pub balance: Option<BigInt>,
58    /// Past-due consumption balance.
59    #[serde(default, deserialize_with = "deserialize_opt_bigint")]
60    pub consumed_balance: Option<BigInt>,
61    /// Configured received-credit threshold.
62    #[serde(default, deserialize_with = "deserialize_opt_bigint")]
63    pub threshold_received: Option<BigInt>,
64    /// Configured given-credit threshold.
65    #[serde(default, deserialize_with = "deserialize_opt_bigint")]
66    pub threshold_given: Option<BigInt>,
67    /// Dynamic received-credit threshold.
68    #[serde(default, deserialize_with = "deserialize_opt_bigint")]
69    pub current_threshold_received: Option<BigInt>,
70    /// Dynamic given-credit threshold.
71    #[serde(default, deserialize_with = "deserialize_opt_bigint")]
72    pub current_threshold_given: Option<BigInt>,
73    /// Surplus balance.
74    #[serde(default, deserialize_with = "deserialize_opt_bigint")]
75    pub surplus_balance: Option<BigInt>,
76    /// Reserved-balance (in-flight credits).
77    #[serde(default, deserialize_with = "deserialize_opt_bigint")]
78    pub reserved_balance: Option<BigInt>,
79    /// Shadow-reserved balance.
80    #[serde(default, deserialize_with = "deserialize_opt_bigint")]
81    pub shadow_reserved_balance: Option<BigInt>,
82    /// Ghost balance (recovered after disconnect).
83    #[serde(default, deserialize_with = "deserialize_opt_bigint")]
84    pub ghost_balance: Option<BigInt>,
85}
86
87/// Redistribution-state snapshot. Mirrors bee-go
88/// `RedistributionStateResponse`.
89#[derive(Clone, Debug, PartialEq, Default, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct RedistributionState {
92    /// Minimum gas funds to play a round.
93    #[serde(default, deserialize_with = "deserialize_opt_bigint")]
94    pub minimum_gas_funds: Option<BigInt>,
95    /// Whether the node currently has those funds.
96    pub has_sufficient_funds: bool,
97    /// Whether the node is frozen out of redistribution.
98    pub is_frozen: bool,
99    /// Whether the node believes it is fully synced.
100    pub is_fully_synced: bool,
101    /// Current phase string.
102    pub phase: String,
103    /// Current round.
104    pub round: u64,
105    /// Last round won.
106    pub last_won_round: u64,
107    /// Last round played.
108    pub last_played_round: u64,
109    /// Last round frozen.
110    pub last_frozen_round: u64,
111    /// Last round selected.
112    pub last_selected_round: u64,
113    /// Last sample duration in seconds. Bee returns this as a
114    /// fractional float (e.g. `37.96302`), not an integer.
115    pub last_sample_duration_seconds: f64,
116    /// Latest seen block.
117    pub block: u64,
118    /// Cumulative reward (PLUR).
119    #[serde(default, deserialize_with = "deserialize_opt_bigint")]
120    pub reward: Option<BigInt>,
121    /// Cumulative fees (PLUR).
122    #[serde(default, deserialize_with = "deserialize_opt_bigint")]
123    pub fees: Option<BigInt>,
124    /// Whether the redistribution worker is healthy.
125    pub is_healthy: bool,
126}
127
128impl DebugApi {
129    /// `GET /balances` — settlement balances with every known peer.
130    pub async fn balances(&self) -> Result<Vec<Balance>, Error> {
131        let builder = request(&self.inner, Method::GET, "balances")?;
132        #[derive(Deserialize)]
133        struct Resp {
134            balances: Vec<Balance>,
135        }
136        let r: Resp = self.inner.send_json(builder).await?;
137        Ok(r.balances)
138    }
139
140    /// `GET /balances/{address}` — settlement balance with one peer.
141    pub async fn peer_balance(&self, address: &str) -> Result<Balance, Error> {
142        let path = format!("balances/{address}");
143        let builder = request(&self.inner, Method::GET, &path)?;
144        self.inner.send_json(builder).await
145    }
146
147    /// `GET /consumed` — past-due consumption balances with every peer.
148    pub async fn consumed_balances(&self) -> Result<Vec<Balance>, Error> {
149        let builder = request(&self.inner, Method::GET, "consumed")?;
150        #[derive(Deserialize)]
151        struct Resp {
152            balances: Vec<Balance>,
153        }
154        let r: Resp = self.inner.send_json(builder).await?;
155        Ok(r.balances)
156    }
157
158    /// `GET /consumed/{address}` — past-due consumption balance with
159    /// one peer.
160    pub async fn peer_consumed_balance(&self, address: &str) -> Result<Balance, Error> {
161        let path = format!("consumed/{address}");
162        let builder = request(&self.inner, Method::GET, &path)?;
163        self.inner.send_json(builder).await
164    }
165
166    /// `GET /accounting` — full per-peer accounting snapshot keyed by
167    /// peer overlay address.
168    pub async fn accounting(&self) -> Result<HashMap<String, PeerAccounting>, Error> {
169        let builder = request(&self.inner, Method::GET, "accounting")?;
170        #[derive(Deserialize)]
171        struct Resp {
172            #[serde(rename = "peerData")]
173            peer_data: HashMap<String, PeerAccounting>,
174        }
175        let r: Resp = self.inner.send_json(builder).await?;
176        Ok(r.peer_data)
177    }
178
179    // ---- stake -------------------------------------------------------
180
181    /// `GET /stake` — staked BZZ amount (PLUR).
182    pub async fn stake(&self) -> Result<BigInt, Error> {
183        let builder = request(&self.inner, Method::GET, "stake")?;
184        #[derive(Deserialize)]
185        struct Resp {
186            #[serde(rename = "stakedAmount")]
187            staked_amount: String,
188        }
189        let r: Resp = self.inner.send_json(builder).await?;
190        r.staked_amount.parse::<BigInt>().map_err(|e| {
191            Error::argument(format!("invalid stakedAmount {:?}: {e}", r.staked_amount))
192        })
193    }
194
195    /// `POST /stake/{amount}` — stake the given amount (PLUR). Returns
196    /// the on-chain transaction hash.
197    pub async fn deposit_stake(&self, amount: &BigInt) -> Result<String, Error> {
198        let path = format!("stake/{amount}");
199        let builder = request(&self.inner, Method::POST, &path)?;
200        tx_hash_response(&self.inner, builder).await
201    }
202
203    /// `GET /stake/withdrawable` — withdrawable staked BZZ (PLUR).
204    pub async fn withdrawable_stake(&self) -> Result<BigInt, Error> {
205        let builder = request(&self.inner, Method::GET, "stake/withdrawable")?;
206        #[derive(Deserialize)]
207        struct Resp {
208            #[serde(rename = "withdrawableAmount")]
209            withdrawable_amount: String,
210        }
211        let r: Resp = self.inner.send_json(builder).await?;
212        r.withdrawable_amount.parse::<BigInt>().map_err(|e| {
213            Error::argument(format!(
214                "invalid withdrawableAmount {:?}: {e}",
215                r.withdrawable_amount
216            ))
217        })
218    }
219
220    /// `DELETE /stake/withdrawable` — withdraw surplus stake.
221    pub async fn withdraw_surplus_stake(&self) -> Result<String, Error> {
222        let builder = request(&self.inner, Method::DELETE, "stake/withdrawable")?;
223        tx_hash_response(&self.inner, builder).await
224    }
225
226    /// `DELETE /stake` — migrate the stake. Returns the transaction
227    /// hash.
228    pub async fn migrate_stake(&self) -> Result<String, Error> {
229        let builder = request(&self.inner, Method::DELETE, "stake")?;
230        tx_hash_response(&self.inner, builder).await
231    }
232
233    /// `GET /redistributionstate` — redistribution worker snapshot.
234    pub async fn redistribution_state(&self) -> Result<RedistributionState, Error> {
235        let builder = request(&self.inner, Method::GET, "redistributionstate")?;
236        self.inner.send_json(builder).await
237    }
238}
239
240async fn tx_hash_response(
241    inner: &crate::client::Inner,
242    builder: reqwest::RequestBuilder,
243) -> Result<String, Error> {
244    #[derive(Deserialize)]
245    struct Resp {
246        #[serde(rename = "txHash")]
247        tx_hash: String,
248    }
249    let resp = inner.send(builder).await?;
250    let bytes: Bytes = resp.bytes().await?;
251    let r: Resp = serde_json::from_slice(&bytes)?;
252    Ok(r.tx_hash)
253}