bee-rs 1.2.0

Rust client for the Swarm Bee API. Functional parity with bee-js / bee-go.
Documentation
//! `/stamps` and `/batches` endpoint methods.

use num_bigint::BigInt;
use reqwest::Method;
use serde::Deserialize;

use crate::api::PostageBatchOptions;
use crate::client::request;
use crate::swarm::{BatchId, Error};

use super::PostageApi;
use super::types::{GlobalPostageBatch, PostageBatch, PostageBatchBuckets};

#[derive(Deserialize)]
struct StampsResp {
    stamps: Vec<PostageBatch>,
}

#[derive(Deserialize)]
struct BatchesResp {
    batches: Vec<GlobalPostageBatch>,
}

#[derive(Deserialize)]
struct BatchIdResp {
    #[serde(rename = "batchID")]
    batch_id: String,
}

impl PostageApi {
    /// Every postage batch owned by this node — `GET /stamps`.
    pub async fn get_postage_batches(&self) -> Result<Vec<PostageBatch>, Error> {
        let builder = request(&self.inner, Method::GET, "stamps")?;
        let res: StampsResp = self.inner.send_json(builder).await?;
        Ok(res.stamps)
    }

    /// Single owned batch by id — `GET /stamps/{id}`.
    pub async fn get_postage_batch(&self, batch_id: &BatchId) -> Result<PostageBatch, Error> {
        let path = format!("stamps/{}", batch_id.to_hex());
        let builder = request(&self.inner, Method::GET, &path)?;
        self.inner.send_json(builder).await
    }

    /// Per-bucket collision stats — `GET /stamps/{id}/buckets`.
    pub async fn get_postage_batch_buckets(
        &self,
        batch_id: &BatchId,
    ) -> Result<PostageBatchBuckets, Error> {
        let path = format!("stamps/{}/buckets", batch_id.to_hex());
        let builder = request(&self.inner, Method::GET, &path)?;
        self.inner.send_json(builder).await
    }

    /// Every chain-visible postage batch — `GET /batches`. Returns the
    /// chain-wide view (no owner-only fields).
    pub async fn get_global_postage_batches(&self) -> Result<Vec<GlobalPostageBatch>, Error> {
        let builder = request(&self.inner, Method::GET, "batches")?;
        let res: BatchesResp = self.inner.send_json(builder).await?;
        Ok(res.batches)
    }

    /// Buy a new postage batch — `POST /stamps/{amount}/{depth}`.
    /// Returns the freshly-minted [`BatchId`].
    ///
    /// `amount` is the per-chunk amount (PLUR) and `depth` is the
    /// stamp depth — together they determine effective capacity and
    /// TTL. For a `(size, duration)`-shaped API see
    /// [`crate::storage::buy_storage`].
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use bee::Client;
    /// use num_bigint::BigInt;
    ///
    /// # async fn run() -> Result<(), bee::Error> {
    /// let client = Client::new("http://localhost:1633")?;
    /// let amount = BigInt::from(414_720_000u64);
    /// let batch_id = client
    ///     .postage()
    ///     .create_postage_batch(&amount, 22, Some("my-batch"))
    ///     .await?;
    /// println!("bought batch {}", batch_id.to_hex());
    /// # Ok(()) }
    /// ```
    pub async fn create_postage_batch(
        &self,
        amount: &BigInt,
        depth: u8,
        label: Option<&str>,
    ) -> Result<BatchId, Error> {
        let opts = label.map(|l| PostageBatchOptions {
            label: Some(l.to_string()),
            ..Default::default()
        });
        self.create_postage_batch_with_options(amount, depth, opts.as_ref())
            .await
    }

    /// Buy a new postage batch with full options support. Mirrors
    /// bee-js `Bee.createPostageBatch` and bee-go's options-style
    /// constructor — `label` and `immutable` map to query parameters,
    /// `gas_price` / `gas_limit` map to `Gas-Price` / `Gas-Limit` headers.
    pub async fn create_postage_batch_with_options(
        &self,
        amount: &BigInt,
        depth: u8,
        opts: Option<&PostageBatchOptions>,
    ) -> Result<BatchId, Error> {
        let path = format!("stamps/{amount}/{depth}");
        let mut builder = request(&self.inner, Method::POST, &path)?;
        if let Some(o) = opts {
            let mut query: Vec<(&str, String)> = Vec::new();
            if let Some(l) = &o.label {
                query.push(("label", l.clone()));
            }
            if let Some(im) = o.immutable {
                query.push(("immutable", im.to_string()));
            }
            if !query.is_empty() {
                let q: Vec<(&str, &str)> = query.iter().map(|(k, v)| (*k, v.as_str())).collect();
                builder = builder.query(&q);
            }
            if let Some(gp) = &o.gas_price {
                builder = builder.header("Gas-Price", gp);
            }
            if let Some(gl) = &o.gas_limit {
                builder = builder.header("Gas-Limit", gl);
            }
        }
        let res: BatchIdResp = self.inner.send_json(builder).await?;
        BatchId::from_hex(&res.batch_id)
    }

    /// Top up an existing batch — `PATCH /stamps/topup/{id}/{amount}`.
    pub async fn top_up_batch(&self, batch_id: &BatchId, amount: &BigInt) -> Result<(), Error> {
        let path = format!("stamps/topup/{}/{amount}", batch_id.to_hex());
        let builder = request(&self.inner, Method::PATCH, &path)?;
        self.inner.send(builder).await?;
        Ok(())
    }

    /// Increase the depth of an existing batch — `PATCH /stamps/dilute/{id}/{depth}`.
    pub async fn dilute_batch(&self, batch_id: &BatchId, depth: u8) -> Result<(), Error> {
        let path = format!("stamps/dilute/{}/{depth}", batch_id.to_hex());
        let builder = request(&self.inner, Method::PATCH, &path)?;
        self.inner.send(builder).await?;
        Ok(())
    }
}