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, 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    // ---- tags ---------------------------------------------------------
68
69    /// Create a new tag — `POST /tags`.
70    pub async fn create_tag(&self) -> Result<Tag, Error> {
71        let builder = request(&self.inner, Method::POST, "tags")?;
72        self.inner.send_json(builder).await
73    }
74
75    /// Get a tag by UID — `GET /tags/{uid}`.
76    pub async fn get_tag(&self, uid: u32) -> Result<Tag, Error> {
77        let path = format!("tags/{uid}");
78        let builder = request(&self.inner, Method::GET, &path)?;
79        self.inner.send_json(builder).await
80    }
81
82    /// bee-js name for [`ApiService::get_tag`].
83    pub async fn retrieve_tag(&self, uid: u32) -> Result<Tag, Error> {
84        self.get_tag(uid).await
85    }
86
87    /// List tags with optional pagination — `GET /tags`.
88    pub async fn list_tags(
89        &self,
90        offset: Option<u32>,
91        limit: Option<u32>,
92    ) -> Result<Vec<Tag>, Error> {
93        let mut builder = request(&self.inner, Method::GET, "tags")?;
94        if let Some(o) = offset {
95            if o > 0 {
96                builder = builder.query(&[("offset", o.to_string())]);
97            }
98        }
99        if let Some(l) = limit {
100            if l > 0 {
101                builder = builder.query(&[("limit", l.to_string())]);
102            }
103        }
104        #[derive(Deserialize)]
105        struct Resp {
106            tags: Vec<Tag>,
107        }
108        let r: Resp = self.inner.send_json(builder).await?;
109        Ok(r.tags)
110    }
111
112    /// Delete a tag — `DELETE /tags/{uid}`.
113    pub async fn delete_tag(&self, uid: u32) -> Result<(), Error> {
114        let path = format!("tags/{uid}");
115        let builder = request(&self.inner, Method::DELETE, &path)?;
116        self.inner.send(builder).await?;
117        Ok(())
118    }
119
120    /// Update a tag — `PATCH /tags/{uid}`.
121    pub async fn update_tag(&self, uid: u32, tag: &Tag) -> Result<(), Error> {
122        let path = format!("tags/{uid}");
123        let body = serde_json::to_vec(tag)?;
124        let builder = request(&self.inner, Method::PATCH, &path)?
125            .header("Content-Type", "application/json")
126            .body(Bytes::from(body));
127        self.inner.send(builder).await?;
128        Ok(())
129    }
130
131    // ---- stewardship --------------------------------------------------
132
133    /// Re-upload locally pinned data — `PUT /stewardship/{ref}`.
134    pub async fn reupload(&self, reference: &Reference, batch_id: &BatchId) -> Result<(), Error> {
135        let path = format!("stewardship/{}", reference.to_hex());
136        let builder = request(&self.inner, Method::PUT, &path)?
137            .header("swarm-postage-batch-id", batch_id.to_hex());
138        self.inner.send(builder).await?;
139        Ok(())
140    }
141
142    /// Check whether a reference is currently retrievable from the
143    /// network — `GET /stewardship/{ref}`. Mirrors bee-js
144    /// `Bee.isRetrievable`.
145    pub async fn is_retrievable(&self, reference: &Reference) -> Result<bool, Error> {
146        let path = format!("stewardship/{}", reference.to_hex());
147        let builder = request(&self.inner, Method::GET, &path)?;
148        #[derive(Deserialize)]
149        struct Resp {
150            is_retrievable: bool,
151        }
152        #[derive(Deserialize)]
153        struct CamelResp {
154            #[serde(rename = "isRetrievable")]
155            is_retrievable: bool,
156        }
157        // Bee uses camelCase here; accept either via try_from on the
158        // body bytes.
159        let resp = self.inner.send(builder).await?;
160        let bytes = resp.bytes().await?;
161        if let Ok(r) = serde_json::from_slice::<CamelResp>(&bytes) {
162            return Ok(r.is_retrievable);
163        }
164        let r: Resp = serde_json::from_slice(&bytes)?;
165        Ok(r.is_retrievable)
166    }
167
168    // ---- grantee ------------------------------------------------------
169
170    /// Get the grantees for a reference — `GET /grantee/{ref}`.
171    pub async fn get_grantees(&self, reference: &Reference) -> Result<Vec<String>, Error> {
172        let path = format!("grantee/{}", reference.to_hex());
173        let builder = request(&self.inner, Method::GET, &path)?;
174        // Live Bee returns a bare JSON array `["pk1", "pk2", …]`. The
175        // earlier `{ "grantees": [...] }` wrapper shape never shipped.
176        let v: Vec<String> = self.inner.send_json(builder).await?;
177        Ok(v)
178    }
179
180    /// Create a new grantee list — `POST /grantee`.
181    pub async fn create_grantees(
182        &self,
183        batch_id: &BatchId,
184        grantees: &[String],
185    ) -> Result<GranteeResponse, Error> {
186        #[derive(Serialize)]
187        struct Body<'a> {
188            grantees: &'a [String],
189        }
190        let body = serde_json::to_vec(&Body { grantees })?;
191        let builder = request(&self.inner, Method::POST, "grantee")?
192            .header("Content-Type", "application/json")
193            .header("Swarm-Postage-Batch-Id", batch_id.to_hex())
194            .body(Bytes::from(body));
195        self.inner.send_json(builder).await
196    }
197
198    /// Patch the grantees for a reference — `PATCH /grantee/{ref}`.
199    pub async fn patch_grantees(
200        &self,
201        batch_id: &BatchId,
202        reference: &Reference,
203        history_address: &Reference,
204        add: &[String],
205        revoke: &[String],
206    ) -> Result<GranteeResponse, Error> {
207        #[derive(Serialize)]
208        struct Body<'a> {
209            #[serde(skip_serializing_if = "<[String]>::is_empty")]
210            add: &'a [String],
211            #[serde(skip_serializing_if = "<[String]>::is_empty")]
212            revoke: &'a [String],
213        }
214        let body = serde_json::to_vec(&Body { add, revoke })?;
215        let path = format!("grantee/{}", reference.to_hex());
216        let builder = request(&self.inner, Method::PATCH, &path)?
217            .header("Content-Type", "application/json")
218            .header("Swarm-Postage-Batch-Id", batch_id.to_hex())
219            .header("Swarm-Act-History-Address", history_address.to_hex())
220            .body(Bytes::from(body));
221        self.inner.send_json(builder).await
222    }
223
224    // ---- envelope -----------------------------------------------------
225
226    /// Build an envelope (postage stamp signature triple) for a
227    /// reference — `POST /envelope/{ref}`. Returns the issuer / index /
228    /// timestamp / signature quadruple.
229    pub async fn post_envelope(
230        &self,
231        batch_id: &BatchId,
232        reference: &Reference,
233    ) -> Result<EnvelopeResponse, Error> {
234        let path = format!("envelope/{}", reference.to_hex());
235        let builder = request(&self.inner, Method::POST, &path)?
236            .header("Swarm-Postage-Batch-Id", batch_id.to_hex());
237        self.inner.send_json(builder).await
238    }
239}
240
241/// Response from [`ApiService::create_grantees`] /
242/// [`ApiService::patch_grantees`]: the new grantee-set reference and
243/// the ACT history root.
244#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
245pub struct GranteeResponse {
246    /// Reference of the grantee set.
247    #[serde(rename = "ref")]
248    pub reference: String,
249    /// ACT history reference.
250    #[serde(rename = "historyref")]
251    pub history_reference: String,
252}
253
254/// Response from [`ApiService::post_envelope`].
255#[derive(Clone, Debug, PartialEq, Eq, Deserialize)]
256pub struct EnvelopeResponse {
257    /// Stamper public key (hex).
258    pub issuer: String,
259    /// Stamp index (hex).
260    pub index: String,
261    /// Stamp timestamp (decimal seconds, hex string).
262    pub timestamp: String,
263    /// Stamp signature (hex).
264    pub signature: String,
265}
266
267/// A Swarm tag — tracks sync progress for an upload.
268#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
269pub struct Tag {
270    /// Tag UID (Bee returns this as `"uid"` on the wire).
271    #[serde(default)]
272    pub uid: u32,
273    /// Display name.
274    #[serde(default)]
275    pub name: String,
276    /// Total chunks expected.
277    #[serde(default)]
278    pub total: i64,
279    /// Chunks split.
280    #[serde(default)]
281    pub split: i64,
282    /// Chunks already known to Bee.
283    #[serde(default)]
284    pub seen: i64,
285    /// Chunks stored locally.
286    #[serde(default)]
287    pub stored: i64,
288    /// Chunks sent over the network.
289    #[serde(default)]
290    pub sent: i64,
291    /// Chunks confirmed synced.
292    #[serde(default)]
293    pub synced: i64,
294    /// Address (root reference of the upload).
295    #[serde(default)]
296    pub address: String,
297    /// Tag start timestamp (RFC 3339).
298    #[serde(default, rename = "startedAt")]
299    pub started_at: String,
300}