Skip to main content

bee/api/
endpoints.rs

1//! Network endpoint methods on [`ApiService`]: pin, tag, stewardship.
2//! Mirrors bee-go's `pkg/api/{pin,tag,stewardship}.go`.
3
4use std::sync::Arc;
5
6use bytes::Bytes;
7use reqwest::Method;
8use serde::{Deserialize, Serialize};
9
10use crate::client::{Inner, MAX_JSON_RESPONSE_BYTES, request};
11use crate::swarm::{BatchId, Error, Reference};
12
13/// Handle exposing the generic `api/*` endpoints (pin, tag,
14/// stewardship). Cheap to clone.
15#[derive(Clone, Debug)]
16pub struct ApiService {
17    pub(crate) inner: Arc<Inner>,
18}
19
20impl ApiService {
21    pub(crate) fn new(inner: Arc<Inner>) -> Self {
22        Self { inner }
23    }
24
25    // ---- pins ---------------------------------------------------------
26
27    /// Pin a reference — `POST /pins/{ref}`.
28    pub async fn pin(&self, reference: &Reference) -> Result<(), Error> {
29        let path = format!("pins/{}", reference.to_hex());
30        let builder = request(&self.inner, Method::POST, &path)?;
31        self.inner.send(builder).await?;
32        Ok(())
33    }
34
35    /// Unpin a reference — `DELETE /pins/{ref}`.
36    pub async fn unpin(&self, reference: &Reference) -> Result<(), Error> {
37        let path = format!("pins/{}", reference.to_hex());
38        let builder = request(&self.inner, Method::DELETE, &path)?;
39        self.inner.send(builder).await?;
40        Ok(())
41    }
42
43    /// Check whether a reference is pinned — `GET /pins/{ref}`.
44    /// Returns `true` on `200`, `false` on `404`, otherwise the
45    /// underlying response error.
46    pub async fn get_pin(&self, reference: &Reference) -> Result<bool, Error> {
47        let path = format!("pins/{}", reference.to_hex());
48        let builder = request(&self.inner, Method::GET, &path)?;
49        match self.inner.send(builder).await {
50            Ok(_) => Ok(true),
51            Err(e) if e.status() == Some(404) => Ok(false),
52            Err(e) => Err(e),
53        }
54    }
55
56    /// List every pinned reference — `GET /pins`.
57    pub async fn list_pins(&self) -> Result<Vec<Reference>, Error> {
58        let builder = request(&self.inner, Method::GET, "pins")?;
59        #[derive(Deserialize)]
60        struct Resp {
61            references: Vec<Reference>,
62        }
63        let r: Resp = self.inner.send_json(builder).await?;
64        Ok(r.references)
65    }
66
67    /// Validate the integrity of pinned chunks — `GET /pins/check`.
68    /// When `reference` is `Some`, only that pin is checked; when
69    /// `None`, every pinned root is walked.
70    ///
71    /// Bee streams the result as **NDJSON** (one
72    /// [`PinIntegrity`] object per line under chunked
73    /// transfer-encoding) so it can flush progress for large pin
74    /// sets. We collect the stream into a `Vec` once the response
75    /// completes — fine for operator dashboards that only need a
76    /// single point-in-time view; if you need progressive reporting
77    /// against a node with thousands of pins, drop down to
78    /// [`crate::Client::http`] and stream the body yourself.
79    pub async fn check_pins(
80        &self,
81        reference: Option<&Reference>,
82    ) -> Result<Vec<PinIntegrity>, Error> {
83        let mut builder = request(&self.inner, Method::GET, "pins/check")?;
84        if let Some(r) = reference {
85            builder = builder.query(&[("ref", r.to_hex())]);
86        }
87        let resp = self.inner.send(builder).await?;
88        let bytes = Inner::read_capped(resp, MAX_JSON_RESPONSE_BYTES).await?;
89        let mut out = Vec::new();
90        for line in bytes.split(|&b| b == b'\n') {
91            let trimmed = trim_ws(line);
92            if trimmed.is_empty() {
93                continue;
94            }
95            let entry: PinIntegrity = serde_json::from_slice(trimmed)?;
96            out.push(entry);
97        }
98        Ok(out)
99    }
100
101    // ---- tags ---------------------------------------------------------
102
103    /// Create a new tag — `POST /tags`.
104    pub async fn create_tag(&self) -> Result<Tag, Error> {
105        let builder = request(&self.inner, Method::POST, "tags")?;
106        self.inner.send_json(builder).await
107    }
108
109    /// Get a tag by UID — `GET /tags/{uid}`.
110    pub async fn get_tag(&self, uid: u32) -> Result<Tag, Error> {
111        let path = format!("tags/{uid}");
112        let builder = request(&self.inner, Method::GET, &path)?;
113        self.inner.send_json(builder).await
114    }
115
116    /// bee-js name for [`ApiService::get_tag`].
117    pub async fn retrieve_tag(&self, uid: u32) -> Result<Tag, Error> {
118        self.get_tag(uid).await
119    }
120
121    /// List tags with optional pagination — `GET /tags`.
122    pub async fn list_tags(
123        &self,
124        offset: Option<u32>,
125        limit: Option<u32>,
126    ) -> Result<Vec<Tag>, Error> {
127        let mut builder = request(&self.inner, Method::GET, "tags")?;
128        if let Some(o) = offset {
129            if o > 0 {
130                builder = builder.query(&[("offset", o.to_string())]);
131            }
132        }
133        if let Some(l) = limit {
134            if l > 0 {
135                builder = builder.query(&[("limit", l.to_string())]);
136            }
137        }
138        #[derive(Deserialize)]
139        struct Resp {
140            tags: Vec<Tag>,
141        }
142        let r: Resp = self.inner.send_json(builder).await?;
143        Ok(r.tags)
144    }
145
146    /// Delete a tag — `DELETE /tags/{uid}`.
147    pub async fn delete_tag(&self, uid: u32) -> Result<(), Error> {
148        let path = format!("tags/{uid}");
149        let builder = request(&self.inner, Method::DELETE, &path)?;
150        self.inner.send(builder).await?;
151        Ok(())
152    }
153
154    /// Update a tag — `PATCH /tags/{uid}`.
155    pub async fn update_tag(&self, uid: u32, tag: &Tag) -> Result<(), Error> {
156        let path = format!("tags/{uid}");
157        let body = serde_json::to_vec(tag)?;
158        let builder = request(&self.inner, Method::PATCH, &path)?
159            .header("Content-Type", "application/json")
160            .body(Bytes::from(body));
161        self.inner.send(builder).await?;
162        Ok(())
163    }
164
165    // ---- stewardship --------------------------------------------------
166
167    /// Re-upload locally pinned data — `PUT /stewardship/{ref}`.
168    pub async fn reupload(&self, reference: &Reference, batch_id: &BatchId) -> Result<(), Error> {
169        let path = format!("stewardship/{}", reference.to_hex());
170        let builder = request(&self.inner, Method::PUT, &path)?
171            .header("swarm-postage-batch-id", batch_id.to_hex());
172        self.inner.send(builder).await?;
173        Ok(())
174    }
175
176    /// Check whether a reference is currently retrievable from the
177    /// network — `GET /stewardship/{ref}`. Mirrors bee-js
178    /// `Bee.isRetrievable`.
179    pub async fn is_retrievable(&self, reference: &Reference) -> Result<bool, Error> {
180        let path = format!("stewardship/{}", reference.to_hex());
181        let builder = request(&self.inner, Method::GET, &path)?;
182        #[derive(Deserialize)]
183        struct Resp {
184            is_retrievable: bool,
185        }
186        #[derive(Deserialize)]
187        struct CamelResp {
188            #[serde(rename = "isRetrievable")]
189            is_retrievable: bool,
190        }
191        // Bee uses camelCase here; accept either via try_from on the
192        // body bytes.
193        let resp = self.inner.send(builder).await?;
194        let bytes = Inner::read_capped(resp, MAX_JSON_RESPONSE_BYTES).await?;
195        if let Ok(r) = serde_json::from_slice::<CamelResp>(&bytes) {
196            return Ok(r.is_retrievable);
197        }
198        let r: Resp = serde_json::from_slice(&bytes)?;
199        Ok(r.is_retrievable)
200    }
201
202    // ---- grantee ------------------------------------------------------
203
204    /// Get the grantees for a reference — `GET /grantee/{ref}`.
205    pub async fn get_grantees(&self, reference: &Reference) -> Result<Vec<String>, Error> {
206        let path = format!("grantee/{}", reference.to_hex());
207        let builder = request(&self.inner, Method::GET, &path)?;
208        // Live Bee returns a bare JSON array `["pk1", "pk2", …]`. The
209        // earlier `{ "grantees": [...] }` wrapper shape never shipped.
210        let v: Vec<String> = self.inner.send_json(builder).await?;
211        Ok(v)
212    }
213
214    /// Create a new grantee list — `POST /grantee`.
215    pub async fn create_grantees(
216        &self,
217        batch_id: &BatchId,
218        grantees: &[String],
219    ) -> Result<GranteeResponse, Error> {
220        #[derive(Serialize)]
221        struct Body<'a> {
222            grantees: &'a [String],
223        }
224        let body = serde_json::to_vec(&Body { grantees })?;
225        let builder = request(&self.inner, Method::POST, "grantee")?
226            .header("Content-Type", "application/json")
227            .header("Swarm-Postage-Batch-Id", batch_id.to_hex())
228            .body(Bytes::from(body));
229        self.inner.send_json(builder).await
230    }
231
232    /// Patch the grantees for a reference — `PATCH /grantee/{ref}`.
233    pub async fn patch_grantees(
234        &self,
235        batch_id: &BatchId,
236        reference: &Reference,
237        history_address: &Reference,
238        add: &[String],
239        revoke: &[String],
240    ) -> Result<GranteeResponse, Error> {
241        #[derive(Serialize)]
242        struct Body<'a> {
243            #[serde(skip_serializing_if = "<[String]>::is_empty")]
244            add: &'a [String],
245            #[serde(skip_serializing_if = "<[String]>::is_empty")]
246            revoke: &'a [String],
247        }
248        let body = serde_json::to_vec(&Body { add, revoke })?;
249        let path = format!("grantee/{}", reference.to_hex());
250        let builder = request(&self.inner, Method::PATCH, &path)?
251            .header("Content-Type", "application/json")
252            .header("Swarm-Postage-Batch-Id", batch_id.to_hex())
253            .header("Swarm-Act-History-Address", history_address.to_hex())
254            .body(Bytes::from(body));
255        self.inner.send_json(builder).await
256    }
257
258    // ---- envelope -----------------------------------------------------
259
260    /// Build an envelope (postage stamp signature triple) for a
261    /// reference — `POST /envelope/{ref}`. Returns the issuer / index /
262    /// timestamp / signature quadruple.
263    pub async fn post_envelope(
264        &self,
265        batch_id: &BatchId,
266        reference: &Reference,
267    ) -> Result<EnvelopeResponse, Error> {
268        let path = format!("envelope/{}", reference.to_hex());
269        let builder = request(&self.inner, Method::POST, &path)?
270            .header("Swarm-Postage-Batch-Id", batch_id.to_hex());
271        self.inner.send_json(builder).await
272    }
273}
274
275/// One entry of the [`ApiService::check_pins`] result. Mirrors
276/// bee-go's `PinIntegrityResponse`.
277#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
278pub struct PinIntegrity {
279    /// Root reference of the pin that was checked.
280    pub reference: Reference,
281    /// Total chunks reachable from the pin.
282    pub total: u64,
283    /// Chunks that should be reachable but are missing locally.
284    pub missing: u64,
285    /// Chunks that are present but failed integrity validation.
286    pub invalid: u64,
287}
288
289impl PinIntegrity {
290    /// True when no chunks are missing or invalid — the pin is
291    /// fully retrievable from local storage.
292    pub fn is_healthy(&self) -> bool {
293        self.missing == 0 && self.invalid == 0
294    }
295}
296
297fn trim_ws(mut s: &[u8]) -> &[u8] {
298    while let [first, rest @ ..] = s {
299        if first.is_ascii_whitespace() {
300            s = rest;
301        } else {
302            break;
303        }
304    }
305    while let [rest @ .., last] = s {
306        if last.is_ascii_whitespace() {
307            s = rest;
308        } else {
309            break;
310        }
311    }
312    s
313}
314
315/// Response from [`ApiService::create_grantees`] /
316/// [`ApiService::patch_grantees`]: the new grantee-set reference and
317/// the ACT history root.
318#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
319pub struct GranteeResponse {
320    /// Reference of the grantee set.
321    #[serde(rename = "ref")]
322    pub reference: String,
323    /// ACT history reference.
324    #[serde(rename = "historyref")]
325    pub history_reference: String,
326}
327
328/// Response from [`ApiService::post_envelope`].
329#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
330pub struct EnvelopeResponse {
331    /// Stamper public key (hex).
332    pub issuer: String,
333    /// Stamp index (hex).
334    pub index: String,
335    /// Stamp timestamp (decimal seconds, hex string).
336    pub timestamp: String,
337    /// Stamp signature (hex).
338    pub signature: String,
339}
340
341/// A Swarm tag — tracks sync progress for an upload.
342#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
343pub struct Tag {
344    /// Tag UID (Bee returns this as `"uid"` on the wire).
345    #[serde(default)]
346    pub uid: u32,
347    /// Display name.
348    #[serde(default)]
349    pub name: String,
350    /// Total chunks expected.
351    #[serde(default)]
352    pub total: i64,
353    /// Chunks split.
354    #[serde(default)]
355    pub split: i64,
356    /// Chunks already known to Bee.
357    #[serde(default)]
358    pub seen: i64,
359    /// Chunks stored locally.
360    #[serde(default)]
361    pub stored: i64,
362    /// Chunks sent over the network.
363    #[serde(default)]
364    pub sent: i64,
365    /// Chunks confirmed synced.
366    #[serde(default)]
367    pub synced: i64,
368    /// Address (root reference of the upload).
369    #[serde(default)]
370    pub address: String,
371    /// Tag start timestamp (RFC 3339).
372    #[serde(default, rename = "startedAt")]
373    pub started_at: String,
374}