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
128/// Reserve-commitment hash + sample inclusion proofs returned by
129/// `GET /rchash/{depth}/{anchor1}/{anchor2}`. Mirrors bee-go
130/// `ApiRCHashResponse`.
131#[derive(Clone, Debug, PartialEq, Default, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct RCHashResponse {
134    /// Time the sampler took to produce the commitment, in seconds.
135    /// Returned as a fractional float (e.g. `12.4`).
136    pub duration_seconds: f64,
137    /// Reserve commitment hash (64-char lowercase hex).
138    pub hash: String,
139    /// Inclusion proofs for the first, second, and last chunks of
140    /// the reserve sample.
141    #[serde(default)]
142    pub proofs: ChunkInclusionProofs,
143}
144
145/// Trio of chunk-inclusion proofs that backs the reserve commitment.
146#[derive(Clone, Debug, PartialEq, Default, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct ChunkInclusionProofs {
149    /// Inclusion proof for the first sampled chunk.
150    #[serde(default)]
151    pub proof1: ChunkInclusionProof,
152    /// Inclusion proof for the second sampled chunk.
153    #[serde(default)]
154    pub proof2: ChunkInclusionProof,
155    /// Inclusion proof for the last sampled chunk.
156    #[serde(default)]
157    pub proof_last: ChunkInclusionProof,
158}
159
160/// Inclusion proof for one chunk in the reserve sample.
161#[derive(Clone, Debug, PartialEq, Default, Deserialize)]
162#[serde(rename_all = "camelCase")]
163pub struct ChunkInclusionProof {
164    /// Chunk span (declared payload length, in bytes).
165    #[serde(default)]
166    pub chunk_span: u64,
167    /// Postage stamp proof for this chunk.
168    #[serde(default)]
169    pub postage_proof: PostageProof,
170    /// First Merkle path of segment hashes; nullable when not produced.
171    pub proof_segments: Option<Vec<String>>,
172    /// Second Merkle path of segment hashes; nullable.
173    pub proof_segments2: Option<Vec<String>>,
174    /// Third Merkle path of segment hashes; nullable.
175    pub proof_segments3: Option<Vec<String>>,
176    /// First leaf segment proven against the chunk root.
177    #[serde(default)]
178    pub prove_segment: String,
179    /// Second leaf segment proven against the chunk root.
180    #[serde(default)]
181    pub prove_segment2: String,
182    /// Single-owner-chunk proof; present iff the chunk is a SOC.
183    pub soc_proof: Option<Vec<SocProof>>,
184}
185
186/// Postage stamp proof embedded in a [`ChunkInclusionProof`].
187#[derive(Clone, Debug, PartialEq, Default, Deserialize)]
188#[serde(rename_all = "camelCase")]
189pub struct PostageProof {
190    /// Bucket index used by the stamp.
191    #[serde(default)]
192    pub index: String,
193    /// Postage batch ID this stamp was issued from.
194    #[serde(default)]
195    pub postage_id: String,
196    /// Stamp signature (hex).
197    #[serde(default)]
198    pub signature: String,
199    /// Stamp creation timestamp (decimal string of unix nanoseconds).
200    #[serde(default)]
201    pub time_stamp: String,
202}
203
204/// Single-owner-chunk proof embedded in a [`ChunkInclusionProof`].
205#[derive(Clone, Debug, PartialEq, Default, Deserialize)]
206#[serde(rename_all = "camelCase")]
207pub struct SocProof {
208    /// Address of the SOC chunk.
209    #[serde(default)]
210    pub chunk_addr: String,
211    /// SOC identifier.
212    #[serde(default)]
213    pub identifier: String,
214    /// SOC signature (hex).
215    #[serde(default)]
216    pub signature: String,
217    /// SOC signer (Ethereum address, hex).
218    #[serde(default)]
219    pub signer: String,
220}
221
222impl DebugApi {
223    /// `GET /balances` — settlement balances with every known peer.
224    pub async fn balances(&self) -> Result<Vec<Balance>, Error> {
225        let builder = request(&self.inner, Method::GET, "balances")?;
226        #[derive(Deserialize)]
227        struct Resp {
228            balances: Vec<Balance>,
229        }
230        let r: Resp = self.inner.send_json(builder).await?;
231        Ok(r.balances)
232    }
233
234    /// `GET /balances/{address}` — settlement balance with one peer.
235    pub async fn peer_balance(&self, address: &str) -> Result<Balance, Error> {
236        let path = format!("balances/{address}");
237        let builder = request(&self.inner, Method::GET, &path)?;
238        self.inner.send_json(builder).await
239    }
240
241    /// `GET /consumed` — past-due consumption balances with every peer.
242    pub async fn consumed_balances(&self) -> Result<Vec<Balance>, Error> {
243        let builder = request(&self.inner, Method::GET, "consumed")?;
244        #[derive(Deserialize)]
245        struct Resp {
246            balances: Vec<Balance>,
247        }
248        let r: Resp = self.inner.send_json(builder).await?;
249        Ok(r.balances)
250    }
251
252    /// `GET /consumed/{address}` — past-due consumption balance with
253    /// one peer.
254    pub async fn peer_consumed_balance(&self, address: &str) -> Result<Balance, Error> {
255        let path = format!("consumed/{address}");
256        let builder = request(&self.inner, Method::GET, &path)?;
257        self.inner.send_json(builder).await
258    }
259
260    /// `GET /accounting` — full per-peer accounting snapshot keyed by
261    /// peer overlay address.
262    pub async fn accounting(&self) -> Result<HashMap<String, PeerAccounting>, Error> {
263        let builder = request(&self.inner, Method::GET, "accounting")?;
264        #[derive(Deserialize)]
265        struct Resp {
266            #[serde(rename = "peerData")]
267            peer_data: HashMap<String, PeerAccounting>,
268        }
269        let r: Resp = self.inner.send_json(builder).await?;
270        Ok(r.peer_data)
271    }
272
273    // ---- stake -------------------------------------------------------
274
275    /// `GET /stake` — staked BZZ amount (PLUR).
276    pub async fn stake(&self) -> Result<BigInt, Error> {
277        let builder = request(&self.inner, Method::GET, "stake")?;
278        #[derive(Deserialize)]
279        struct Resp {
280            #[serde(rename = "stakedAmount")]
281            staked_amount: String,
282        }
283        let r: Resp = self.inner.send_json(builder).await?;
284        r.staked_amount.parse::<BigInt>().map_err(|e| {
285            Error::argument(format!("invalid stakedAmount {:?}: {e}", r.staked_amount))
286        })
287    }
288
289    /// `POST /stake/{amount}` — stake the given amount (PLUR). Returns
290    /// the on-chain transaction hash.
291    pub async fn deposit_stake(&self, amount: &BigInt) -> Result<String, Error> {
292        let path = format!("stake/{amount}");
293        let builder = request(&self.inner, Method::POST, &path)?;
294        tx_hash_response(&self.inner, builder).await
295    }
296
297    /// `GET /stake/withdrawable` — withdrawable staked BZZ (PLUR).
298    pub async fn withdrawable_stake(&self) -> Result<BigInt, Error> {
299        let builder = request(&self.inner, Method::GET, "stake/withdrawable")?;
300        #[derive(Deserialize)]
301        struct Resp {
302            #[serde(rename = "withdrawableAmount")]
303            withdrawable_amount: String,
304        }
305        let r: Resp = self.inner.send_json(builder).await?;
306        r.withdrawable_amount.parse::<BigInt>().map_err(|e| {
307            Error::argument(format!(
308                "invalid withdrawableAmount {:?}: {e}",
309                r.withdrawable_amount
310            ))
311        })
312    }
313
314    /// `DELETE /stake/withdrawable` — withdraw surplus stake.
315    pub async fn withdraw_surplus_stake(&self) -> Result<String, Error> {
316        let builder = request(&self.inner, Method::DELETE, "stake/withdrawable")?;
317        tx_hash_response(&self.inner, builder).await
318    }
319
320    /// `DELETE /stake` — migrate the stake. Returns the transaction
321    /// hash.
322    pub async fn migrate_stake(&self) -> Result<String, Error> {
323        let builder = request(&self.inner, Method::DELETE, "stake")?;
324        tx_hash_response(&self.inner, builder).await
325    }
326
327    /// `GET /redistributionstate` — redistribution worker snapshot.
328    pub async fn redistribution_state(&self) -> Result<RedistributionState, Error> {
329        let builder = request(&self.inner, Method::GET, "redistributionstate")?;
330        self.inner.send_json(builder).await
331    }
332
333    /// `GET /rchash/{depth}/{anchor1}/{anchor2}` — reserve-commitment
334    /// hash with sample inclusion proofs. Used by the redistribution
335    /// game; in a TUI / dashboard this is also the natural "sampler
336    /// benchmark" — the returned `duration_seconds` tells operators
337    /// whether their hardware can complete a sample within the round
338    /// deadline.
339    pub async fn r_chash(
340        &self,
341        depth: u8,
342        anchor1: &str,
343        anchor2: &str,
344    ) -> Result<RCHashResponse, Error> {
345        let path = format!("rchash/{depth}/{anchor1}/{anchor2}");
346        let builder = request(&self.inner, Method::GET, &path)?;
347        self.inner.send_json(builder).await
348    }
349}
350
351async fn tx_hash_response(
352    inner: &crate::client::Inner,
353    builder: reqwest::RequestBuilder,
354) -> Result<String, Error> {
355    #[derive(Deserialize)]
356    struct Resp {
357        #[serde(rename = "txHash")]
358        tx_hash: String,
359    }
360    let resp = inner.send(builder).await?;
361    let bytes: Bytes = resp.bytes().await?;
362    let r: Resp = serde_json::from_slice(&bytes)?;
363    Ok(r.tx_hash)
364}