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, Eq, 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.
114    pub last_sample_duration_seconds: u64,
115    /// Latest seen block.
116    pub block: u64,
117    /// Cumulative reward (PLUR).
118    #[serde(default, deserialize_with = "deserialize_opt_bigint")]
119    pub reward: Option<BigInt>,
120    /// Cumulative fees (PLUR).
121    #[serde(default, deserialize_with = "deserialize_opt_bigint")]
122    pub fees: Option<BigInt>,
123    /// Whether the redistribution worker is healthy.
124    pub is_healthy: bool,
125}
126
127impl DebugApi {
128    /// `GET /balances` — settlement balances with every known peer.
129    pub async fn balances(&self) -> Result<Vec<Balance>, Error> {
130        let builder = request(&self.inner, Method::GET, "balances")?;
131        #[derive(Deserialize)]
132        struct Resp {
133            balances: Vec<Balance>,
134        }
135        let r: Resp = self.inner.send_json(builder).await?;
136        Ok(r.balances)
137    }
138
139    /// `GET /balances/{address}` — settlement balance with one peer.
140    pub async fn peer_balance(&self, address: &str) -> Result<Balance, Error> {
141        let path = format!("balances/{address}");
142        let builder = request(&self.inner, Method::GET, &path)?;
143        self.inner.send_json(builder).await
144    }
145
146    /// `GET /consumed` — past-due consumption balances with every peer.
147    pub async fn consumed_balances(&self) -> Result<Vec<Balance>, Error> {
148        let builder = request(&self.inner, Method::GET, "consumed")?;
149        #[derive(Deserialize)]
150        struct Resp {
151            balances: Vec<Balance>,
152        }
153        let r: Resp = self.inner.send_json(builder).await?;
154        Ok(r.balances)
155    }
156
157    /// `GET /consumed/{address}` — past-due consumption balance with
158    /// one peer.
159    pub async fn peer_consumed_balance(&self, address: &str) -> Result<Balance, Error> {
160        let path = format!("consumed/{address}");
161        let builder = request(&self.inner, Method::GET, &path)?;
162        self.inner.send_json(builder).await
163    }
164
165    /// `GET /accounting` — full per-peer accounting snapshot keyed by
166    /// peer overlay address.
167    pub async fn accounting(&self) -> Result<HashMap<String, PeerAccounting>, Error> {
168        let builder = request(&self.inner, Method::GET, "accounting")?;
169        #[derive(Deserialize)]
170        struct Resp {
171            #[serde(rename = "peerData")]
172            peer_data: HashMap<String, PeerAccounting>,
173        }
174        let r: Resp = self.inner.send_json(builder).await?;
175        Ok(r.peer_data)
176    }
177
178    // ---- stake -------------------------------------------------------
179
180    /// `GET /stake` — staked BZZ amount (PLUR).
181    pub async fn stake(&self) -> Result<BigInt, Error> {
182        let builder = request(&self.inner, Method::GET, "stake")?;
183        #[derive(Deserialize)]
184        struct Resp {
185            #[serde(rename = "stakedAmount")]
186            staked_amount: String,
187        }
188        let r: Resp = self.inner.send_json(builder).await?;
189        r.staked_amount.parse::<BigInt>().map_err(|e| {
190            Error::argument(format!("invalid stakedAmount {:?}: {e}", r.staked_amount))
191        })
192    }
193
194    /// `POST /stake/{amount}` — stake the given amount (PLUR). Returns
195    /// the on-chain transaction hash.
196    pub async fn deposit_stake(&self, amount: &BigInt) -> Result<String, Error> {
197        let path = format!("stake/{amount}");
198        let builder = request(&self.inner, Method::POST, &path)?;
199        tx_hash_response(&self.inner, builder).await
200    }
201
202    /// `GET /stake/withdrawable` — withdrawable staked BZZ (PLUR).
203    pub async fn withdrawable_stake(&self) -> Result<BigInt, Error> {
204        let builder = request(&self.inner, Method::GET, "stake/withdrawable")?;
205        #[derive(Deserialize)]
206        struct Resp {
207            #[serde(rename = "withdrawableAmount")]
208            withdrawable_amount: String,
209        }
210        let r: Resp = self.inner.send_json(builder).await?;
211        r.withdrawable_amount.parse::<BigInt>().map_err(|e| {
212            Error::argument(format!(
213                "invalid withdrawableAmount {:?}: {e}",
214                r.withdrawable_amount
215            ))
216        })
217    }
218
219    /// `DELETE /stake/withdrawable` — withdraw surplus stake.
220    pub async fn withdraw_surplus_stake(&self) -> Result<String, Error> {
221        let builder = request(&self.inner, Method::DELETE, "stake/withdrawable")?;
222        tx_hash_response(&self.inner, builder).await
223    }
224
225    /// `DELETE /stake` — migrate the stake. Returns the transaction
226    /// hash.
227    pub async fn migrate_stake(&self) -> Result<String, Error> {
228        let builder = request(&self.inner, Method::DELETE, "stake")?;
229        tx_hash_response(&self.inner, builder).await
230    }
231
232    /// `GET /redistributionstate` — redistribution worker snapshot.
233    pub async fn redistribution_state(&self) -> Result<RedistributionState, Error> {
234        let builder = request(&self.inner, Method::GET, "redistributionstate")?;
235        self.inner.send_json(builder).await
236    }
237}
238
239async fn tx_hash_response(
240    inner: &crate::client::Inner,
241    builder: reqwest::RequestBuilder,
242) -> Result<String, Error> {
243    #[derive(Deserialize)]
244    struct Resp {
245        #[serde(rename = "txHash")]
246        tx_hash: String,
247    }
248    let resp = inner.send(builder).await?;
249    let bytes: Bytes = resp.bytes().await?;
250    let r: Resp = serde_json::from_slice(&bytes)?;
251    Ok(r.tx_hash)
252}