Skip to main content

scout/
lib.rs

1//! scout — a light Swarm client.
2//!
3//! [`LiteClient`] is a thin façade over [`bee`]. Reads (`/bzz`, `/bytes`)
4//! go to a gateway or any Bee endpoint and need no node, stake or stamp —
5//! the "light node" idea. Writes (uploads) need a Bee node holding a
6//! usable postage batch; configure one with [`LiteClient::with_write`] and
7//! they go there while reads still use the read endpoint.
8
9use bee::api::{CollectionUploadOptions, DownloadOptions, FileUploadOptions, UploadOptions};
10use bee::swarm::{BatchId, PrivateKey, PublicKey, Reference, Topic};
11use bee::Client;
12
13/// A light Swarm client: a read endpoint, plus an optional stamped node
14/// for uploads.
15pub struct LiteClient {
16    client: Client,
17    write: Option<Writer>,
18}
19
20struct Writer {
21    client: Client,
22    batch: BatchId,
23}
24
25/// Everything a `share` produces — the refs to download it and to manage
26/// access.
27pub struct ShareInfo {
28    pub file_ref: String,
29    pub history: String,
30    pub grantee_ref: String,
31    pub grantee_history: String,
32    pub publisher: String,
33}
34
35impl LiteClient {
36    /// Build a read client pointed at `endpoint` (a public gateway or any
37    /// Bee HTTP base URL).
38    pub fn read(endpoint: &str) -> anyhow::Result<Self> {
39        Ok(Self {
40            client: Client::new(endpoint)?,
41            write: None,
42        })
43    }
44
45    /// Attach a write endpoint: a Bee node holding a usable postage batch
46    /// (`stamp`, hex). Uploads go here; reads keep using the read endpoint.
47    pub fn with_write(mut self, node_endpoint: &str, stamp: &str) -> anyhow::Result<Self> {
48        let batch: BatchId = stamp.parse()?;
49        self.write = Some(Writer {
50            client: Client::new(node_endpoint)?,
51            batch,
52        });
53        Ok(self)
54    }
55
56    // ---------- read (no node required) ----------
57
58    /// Download manifest-aware content at `reference` (`GET /bzz/{ref}`).
59    pub async fn cat(&self, reference: &str) -> anyhow::Result<Vec<u8>> {
60        let r: Reference = reference.parse()?;
61        let (bytes, _headers) = self.client.file().download_file(&r, None).await?;
62        Ok(bytes.to_vec())
63    }
64
65    /// Download one path inside a collection (`GET /bzz/{ref}/{path}`).
66    pub async fn cat_path(&self, reference: &str, path: &str) -> anyhow::Result<Vec<u8>> {
67        let r: Reference = reference.parse()?;
68        let (bytes, _headers) = self.client.file().download_file_path(&r, path, None).await?;
69        Ok(bytes.to_vec())
70    }
71
72    /// Download raw bytes (`GET /bytes/{ref}`).
73    pub async fn bytes(&self, reference: &str) -> anyhow::Result<Vec<u8>> {
74        let r: Reference = reference.parse()?;
75        let bytes = self.client.file().download_data(&r, None).await?;
76        Ok(bytes.to_vec())
77    }
78
79    // ---------- write (needs a stamped node) ----------
80
81    fn writer(&self) -> anyhow::Result<&Writer> {
82        self.write.as_ref().ok_or_else(|| {
83            anyhow::anyhow!("uploads need a write endpoint — set --node and --stamp (or $BEE_NODE/$BEE_STAMP)")
84        })
85    }
86
87    /// Upload a file (manifest-wrapped, `POST /bzz`). Returns the bzz
88    /// reference as hex.
89    pub async fn up_file(&self, name: &str, content_type: &str, data: Vec<u8>) -> anyhow::Result<String> {
90        let w = self.writer()?;
91        let res = w
92            .client
93            .file()
94            .upload_file(&w.batch, data, name, content_type, None)
95            .await?;
96        Ok(res.reference.to_hex())
97    }
98
99    /// Upload raw bytes (`POST /bytes`). Returns the bytes reference as hex.
100    pub async fn up_bytes(&self, data: Vec<u8>) -> anyhow::Result<String> {
101        let w = self.writer()?;
102        let res = w.client.file().upload_data(&w.batch, data, None).await?;
103        Ok(res.reference.to_hex())
104    }
105
106    /// Upload a whole directory as a browsable Swarm collection (a
107    /// mantaray manifest). `index_document` (e.g. `index.html`) is served
108    /// as the default document at `bzz/<ref>/`. Returns the manifest
109    /// reference. Needs a write endpoint (`--stamp`).
110    pub async fn up_dir(&self, folder: &str, index_document: Option<&str>) -> anyhow::Result<String> {
111        let w = self.writer()?;
112        let opts = CollectionUploadOptions {
113            index_document: index_document.map(|s| s.to_string()),
114            ..Default::default()
115        };
116        let res = w.client.file().upload_collection(&w.batch, folder, Some(&opts)).await?;
117        Ok(res.reference.to_hex())
118    }
119
120    // ---------- feeds (mutable pointers) ----------
121
122    /// Point a feed (`key` owner + `topic`) at `reference` and ensure a
123    /// feed *manifest* exists. Returns the **manifest reference** — a
124    /// stable handle that always resolves to the feed's latest content.
125    /// Read it later with [`LiteClient::cat`]; re-publishing updates what
126    /// that same handle serves. Needs a write endpoint (`--stamp`).
127    pub async fn publish(&self, key_hex: &str, topic: &str, reference: &str) -> anyhow::Result<String> {
128        let w = self.writer()?;
129        let key = PrivateKey::from_hex(key_hex)?;
130        let t = Topic::from_string(topic);
131        let r: Reference = reference.parse()?;
132        w.client
133            .file()
134            .update_feed_with_reference(&w.batch, &key, &t, &r, None)
135            .await?;
136        let owner = key.public_key()?.address();
137        let manifest = w.client.file().create_feed_manifest(&w.batch, &owner, &t).await?;
138        Ok(manifest.to_hex())
139    }
140
141    // ---------- sharing (Access Control Trie) ----------
142
143    /// Upload `data` under an ACT and grant the given compressed-pubkey
144    /// `grantees`. Returns the refs to download it (file_ref + history +
145    /// publisher) and to manage access (grantee_ref). Needs `--stamp`.
146    pub async fn share(
147        &self,
148        name: &str,
149        content_type: &str,
150        data: Vec<u8>,
151        grantees: &[String],
152    ) -> anyhow::Result<ShareInfo> {
153        let w = self.writer()?;
154        let opts = FileUploadOptions {
155            base: UploadOptions {
156                act: Some(true),
157                ..Default::default()
158            },
159            content_type: Some(content_type.to_string()),
160            ..Default::default()
161        };
162        let up = w
163            .client
164            .file()
165            .upload_file(&w.batch, data, name, content_type, Some(&opts))
166            .await?;
167        let history = up
168            .history_address
169            .ok_or_else(|| anyhow::anyhow!("upload returned no ACT history address"))?;
170        let created = w.client.api().create_grantees(&w.batch, grantees).await?;
171        let publisher = w.client.debug().addresses().await?.public_key;
172        Ok(ShareInfo {
173            file_ref: up.reference.to_hex(),
174            history: history.to_hex(),
175            grantee_ref: created.reference,
176            grantee_history: created.history_reference,
177            publisher,
178        })
179    }
180
181    /// Revoke `remove` grantees from a grantee list anchored at the
182    /// upload's `history`. Returns the new (grantee_ref, grantee_history).
183    /// Needs `--stamp`.
184    pub async fn revoke(
185        &self,
186        grantee_ref: &str,
187        history: &str,
188        remove: &[String],
189    ) -> anyhow::Result<(String, String)> {
190        let w = self.writer()?;
191        let gr = Reference::from_hex(grantee_ref)?;
192        let h = Reference::from_hex(history)?;
193        let patched = w.client.api().patch_grantees(&w.batch, &gr, &h, &[], remove).await?;
194        Ok((patched.reference, patched.history_reference))
195    }
196
197    /// List the grantees of a grantee-list reference. Read-only.
198    pub async fn grantees(&self, grantee_ref: &str) -> anyhow::Result<Vec<String>> {
199        let gr = Reference::from_hex(grantee_ref)?;
200        Ok(self.client.api().get_grantees(&gr).await?)
201    }
202
203    /// Download ACT-protected content as `publisher` (their compressed
204    /// pubkey) using the upload's `history`. The Bee node decrypts — it
205    /// must be the publisher or an authorised grantee. Read-only.
206    pub async fn fetch_act(&self, file_ref: &str, publisher: &str, history: &str) -> anyhow::Result<Vec<u8>> {
207        let r = Reference::from_hex(file_ref)?;
208        let pk = PublicKey::from_hex(publisher)?;
209        let h = Reference::from_hex(history)?;
210        let now = std::time::SystemTime::now()
211            .duration_since(std::time::UNIX_EPOCH)
212            .map(|d| d.as_secs() as i64)
213            .unwrap_or(0);
214        let opts = DownloadOptions {
215            act_publisher: Some(pk),
216            act_history_address: Some(h),
217            act_timestamp: Some(now),
218            ..Default::default()
219        };
220        let (body, _headers) = self.client.file().download_file(&r, Some(&opts)).await?;
221        Ok(body.to_vec())
222    }
223}
224
225/// Generate a fresh identity. Returns `(private_key_hex, owner_address_hex,
226/// compressed_pubkey_hex)` — the owner address is the feed identity, the
227/// compressed pubkey is what ACT `share --to` expects.
228pub fn generate_key() -> anyhow::Result<(String, String, String)> {
229    let mut b = [0u8; 32];
230    loop {
231        getrandom::getrandom(&mut b).map_err(|e| anyhow::anyhow!("rng: {e}"))?;
232        if let Ok(key) = PrivateKey::new(&b) {
233            let pubk = key.public_key()?;
234            return Ok((key.to_hex(), pubk.address().to_hex(), pubk.compressed_hex()?));
235        }
236    }
237}