Skip to main content

bee/debug/
chequebook.rs

1//! Chequebook / settlements / wallet endpoints. Mirrors bee-go's
2//! `pkg/debug/chequebook.go`.
3
4use bytes::Bytes;
5use num_bigint::BigInt;
6use reqwest::Method;
7use serde::{Deserialize, Deserializer};
8
9use crate::client::{Inner, request};
10use crate::swarm::Error;
11
12use super::DebugApi;
13
14fn de_bigint<'de, D: Deserializer<'de>>(d: D) -> Result<BigInt, D::Error> {
15    let s: String = Deserialize::deserialize(d)?;
16    if s.is_empty() {
17        return Ok(BigInt::from(0));
18    }
19    s.parse::<BigInt>().map_err(serde::de::Error::custom)
20}
21
22fn de_opt_bigint<'de, D: Deserializer<'de>>(d: D) -> Result<Option<BigInt>, D::Error> {
23    let s: Option<String> = Deserialize::deserialize(d)?;
24    match s {
25        None => Ok(None),
26        Some(s) if s.is_empty() => Ok(None),
27        Some(s) => s
28            .parse::<BigInt>()
29            .map(Some)
30            .map_err(serde::de::Error::custom),
31    }
32}
33
34/// `GET /wallet` response. Mirrors bee-go `WalletResponse`.
35///
36/// **API-version note:** Bee 2.7.2 / API 8.0.0 dropped the legacy
37/// `bzzAddress` / `nativeAddress` fields and renamed `chequebook` to
38/// `chequebookContractAddress`. We accept both spellings: the legacy
39/// fields are optional, and `chequebook_contract_address` carries an
40/// alias for the old `chequebook` key so older Bee builds still parse.
41#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct Wallet {
44    /// Legacy BZZ address field (Bee ≤ 2.7.1). `None` on 2.7.2+.
45    #[serde(default)]
46    pub bzz_address: Option<String>,
47    /// Legacy native-token address field (Bee ≤ 2.7.1). `None` on 2.7.2+.
48    #[serde(default)]
49    pub native_address: Option<String>,
50    /// Chequebook contract address. Accepts the new
51    /// `chequebookContractAddress` key (Bee 2.7.2+) and falls back to
52    /// the legacy `chequebook` key.
53    #[serde(default, alias = "chequebook")]
54    pub chequebook_contract_address: Option<String>,
55    /// BZZ balance in PLUR.
56    #[serde(default, deserialize_with = "de_opt_bigint")]
57    pub bzz_balance: Option<BigInt>,
58    /// Native token balance in wei.
59    #[serde(default, deserialize_with = "de_opt_bigint")]
60    pub native_token_balance: Option<BigInt>,
61    /// On-chain chain ID.
62    #[serde(rename = "chainID")]
63    pub chain_id: i64,
64    /// Wallet (operator) address.
65    pub wallet_address: String,
66}
67
68/// `GET /chequebook/balance` response.
69#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
70#[serde(rename_all = "camelCase")]
71pub struct ChequebookBalance {
72    /// Total balance held in the chequebook (PLUR).
73    #[serde(deserialize_with = "de_bigint")]
74    pub total_balance: BigInt,
75    /// Available (uncashed) balance (PLUR).
76    #[serde(deserialize_with = "de_bigint")]
77    pub available_balance: BigInt,
78}
79
80/// One peer settlement entry.
81#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
82pub struct Settlement {
83    /// Peer overlay address.
84    pub peer: String,
85    /// Cumulative received PLUR.
86    #[serde(default, deserialize_with = "de_opt_bigint")]
87    pub received: Option<BigInt>,
88    /// Cumulative sent PLUR.
89    #[serde(default, deserialize_with = "de_opt_bigint")]
90    pub sent: Option<BigInt>,
91}
92
93/// `GET /settlements` response.
94#[derive(Clone, Debug, PartialEq, Eq, Default, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct Settlements {
97    /// Sum of `received` across peers.
98    #[serde(default, deserialize_with = "de_opt_bigint")]
99    pub total_received: Option<BigInt>,
100    /// Sum of `sent` across peers.
101    #[serde(default, deserialize_with = "de_opt_bigint")]
102    pub total_sent: Option<BigInt>,
103    /// Per-peer breakdown.
104    #[serde(default)]
105    pub settlements: Vec<Settlement>,
106}
107
108/// One sent or received cheque.
109#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
110pub struct Cheque {
111    /// Cheque beneficiary (eth address).
112    pub beneficiary: String,
113    /// Issuing chequebook contract.
114    pub chequebook: String,
115    /// Cumulative payout (PLUR).
116    #[serde(default, deserialize_with = "de_opt_bigint")]
117    pub payout: Option<BigInt>,
118}
119
120/// One row of `GET /chequebook/cheque`. `last_received` may be `None`.
121#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
122pub struct LastCheque {
123    /// Peer overlay.
124    pub peer: String,
125    /// Last received cheque or `None` if no cheques yet.
126    #[serde(default, rename = "lastreceived")]
127    pub last_received: Option<Cheque>,
128}
129
130/// Sent + received cheques for one peer (`GET /chequebook/cheque/{peer}`).
131#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
132pub struct PeerCheques {
133    /// Peer overlay.
134    pub peer: String,
135    /// Last received cheque (if any).
136    #[serde(default, rename = "lastreceived")]
137    pub last_received: Option<Cheque>,
138    /// Last sent cheque (if any).
139    #[serde(default, rename = "lastsent")]
140    pub last_sent: Option<Cheque>,
141}
142
143/// On-chain outcome of a previous cashout.
144#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
145pub struct CashoutResult {
146    /// Recipient eth address.
147    pub recipient: String,
148    /// Last on-chain payout (PLUR).
149    #[serde(default, rename = "lastPayout", deserialize_with = "de_opt_bigint")]
150    pub last_payout: Option<BigInt>,
151    /// Whether the cashout transaction bounced.
152    #[serde(default)]
153    pub bounced: bool,
154}
155
156/// `GET /chequebook/cashout/{peer}` snapshot.
157#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
158pub struct LastCashoutAction {
159    /// Peer overlay.
160    pub peer: String,
161    /// Uncashed amount (PLUR).
162    #[serde(default, rename = "uncashedAmount", deserialize_with = "de_opt_bigint")]
163    pub uncashed_amount: Option<BigInt>,
164    /// Last cashout transaction hash, if any.
165    #[serde(default, rename = "transactionHash")]
166    pub transaction_hash: Option<String>,
167    /// Last cashed cheque, if any.
168    #[serde(default, rename = "lastCashedCheque")]
169    pub last_cashed_cheque: Option<Cheque>,
170    /// On-chain result, if any.
171    #[serde(default)]
172    pub result: Option<CashoutResult>,
173}
174
175impl DebugApi {
176    // ---- wallet -------------------------------------------------------
177
178    /// `GET /wallet` — node operator wallet snapshot.
179    pub async fn wallet(&self) -> Result<Wallet, Error> {
180        let builder = request(&self.inner, Method::GET, "wallet")?;
181        self.inner.send_json(builder).await
182    }
183
184    /// `POST /wallet/withdraw/bzz?amount=&address=` — withdraw BZZ to
185    /// an external address. Returns the on-chain transaction hash.
186    pub async fn withdraw_bzz(&self, amount: &BigInt, address: &str) -> Result<String, Error> {
187        let builder = request(&self.inner, Method::POST, "wallet/withdraw/bzz")?.query(&[
188            ("amount", amount.to_string()),
189            ("address", address.to_string()),
190        ]);
191        tx_hash(&self.inner, builder).await
192    }
193
194    /// `POST /wallet/withdraw/nativetoken?amount=&address=` — withdraw
195    /// the native settlement token (xDAI / ETH).
196    pub async fn withdraw_native_token(
197        &self,
198        amount: &BigInt,
199        address: &str,
200    ) -> Result<String, Error> {
201        let builder = request(&self.inner, Method::POST, "wallet/withdraw/nativetoken")?.query(&[
202            ("amount", amount.to_string()),
203            ("address", address.to_string()),
204        ]);
205        tx_hash(&self.inner, builder).await
206    }
207
208    // ---- chequebook ---------------------------------------------------
209
210    /// `GET /chequebook/balance` — total + available chequebook PLUR.
211    pub async fn chequebook_balance(&self) -> Result<ChequebookBalance, Error> {
212        let builder = request(&self.inner, Method::GET, "chequebook/balance")?;
213        self.inner.send_json(builder).await
214    }
215
216    /// `POST /chequebook/deposit?amount=` — deposit BZZ into the
217    /// chequebook from the operator wallet.
218    pub async fn chequebook_deposit(&self, amount: &BigInt) -> Result<String, Error> {
219        let builder = request(&self.inner, Method::POST, "chequebook/deposit")?
220            .query(&[("amount", amount.to_string())]);
221        tx_hash(&self.inner, builder).await
222    }
223
224    /// `POST /chequebook/withdraw?amount=` — withdraw BZZ from the
225    /// chequebook back to the operator wallet.
226    pub async fn chequebook_withdraw(&self, amount: &BigInt) -> Result<String, Error> {
227        let builder = request(&self.inner, Method::POST, "chequebook/withdraw")?
228            .query(&[("amount", amount.to_string())]);
229        tx_hash(&self.inner, builder).await
230    }
231
232    /// `GET /chequebook/cheque` — the last received cheque per peer.
233    pub async fn last_cheques(&self) -> Result<Vec<LastCheque>, Error> {
234        let builder = request(&self.inner, Method::GET, "chequebook/cheque")?;
235        #[derive(Deserialize)]
236        struct Resp {
237            #[serde(rename = "lastcheques")]
238            last_cheques: Vec<LastCheque>,
239        }
240        let r: Resp = self.inner.send_json(builder).await?;
241        Ok(r.last_cheques)
242    }
243
244    /// `GET /chequebook/cheque/{peer}` — last sent + received cheque
245    /// for one peer.
246    pub async fn peer_cheques(&self, peer: &str) -> Result<PeerCheques, Error> {
247        let path = format!("chequebook/cheque/{peer}");
248        let builder = request(&self.inner, Method::GET, &path)?;
249        self.inner.send_json(builder).await
250    }
251
252    /// `GET /chequebook/cashout/{peer}` — last cashout action snapshot
253    /// for one peer.
254    pub async fn last_cashout_action(&self, peer: &str) -> Result<LastCashoutAction, Error> {
255        let path = format!("chequebook/cashout/{peer}");
256        let builder = request(&self.inner, Method::GET, &path)?;
257        self.inner.send_json(builder).await
258    }
259
260    /// `POST /chequebook/cashout/{peer}` — cash out the last received
261    /// cheque from a peer. `gas_price` is optional (sent in the
262    /// `gas-price` header when present). Returns the cashout
263    /// transaction hash.
264    pub async fn cashout_last_cheque(
265        &self,
266        peer: &str,
267        gas_price: Option<&BigInt>,
268    ) -> Result<String, Error> {
269        let path = format!("chequebook/cashout/{peer}");
270        let mut builder = request(&self.inner, Method::POST, &path)?;
271        if let Some(gp) = gas_price {
272            builder = builder.header("gas-price", gp.to_string());
273        }
274        tx_hash(&self.inner, builder).await
275    }
276
277    // ---- settlements --------------------------------------------------
278
279    /// `GET /settlements` — totals + per-peer settlement breakdown.
280    pub async fn settlements(&self) -> Result<Settlements, Error> {
281        let builder = request(&self.inner, Method::GET, "settlements")?;
282        self.inner.send_json(builder).await
283    }
284
285    /// `GET /settlements/{peer}` — settlement totals for one peer.
286    pub async fn peer_settlement(&self, peer: &str) -> Result<Settlement, Error> {
287        let path = format!("settlements/{peer}");
288        let builder = request(&self.inner, Method::GET, &path)?;
289        self.inner.send_json(builder).await
290    }
291}
292
293async fn tx_hash(inner: &Inner, builder: reqwest::RequestBuilder) -> Result<String, Error> {
294    #[derive(Deserialize)]
295    struct Resp {
296        #[serde(rename = "transactionHash")]
297        transaction_hash: String,
298    }
299    let resp = inner.send(builder).await?;
300    let bytes: Bytes = resp.bytes().await?;
301    let r: Resp = serde_json::from_slice(&bytes)?;
302    Ok(r.transaction_hash)
303}