Skip to main content

bee/
storage.rs

1//! High-level storage helpers built on top of [`Client`].
2//!
3//! Wraps the postage-batch lifecycle so callers can think in terms of
4//! `(size, duration)` instead of `(amount, depth)`. The math comes
5//! from [`crate::postage`] (depth/cost formulas) and [`crate::swarm`]
6//! ([`Network`] for block time, [`Size`] for byte-count parsing).
7//!
8//! Mirrors bee-js's `client.buy_storage` / `client.get_storage_cost`.
9
10use std::time::Duration;
11
12use num_bigint::BigInt;
13
14use crate::Client;
15use crate::postage::PostageBatch;
16use crate::swarm::{BatchId, Error, Network, Size};
17
18/// Options for [`buy_storage`]. `network` shapes the per-block amount
19/// math; `label` is the optional batch label; `immutable` selects an
20/// immutable batch.
21#[derive(Clone, Debug, Default)]
22pub struct StorageOptions {
23    /// Settlement chain (controls block time).
24    pub network: Network,
25    /// Optional batch label.
26    pub label: Option<String>,
27    /// Whether the batch is immutable. `None` keeps Bee's default.
28    pub immutable: Option<bool>,
29}
30
31/// Storage-cost preview returned by [`get_storage_cost`].
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct StorageCost {
34    /// Stamp depth covering `size`.
35    pub depth: u8,
36    /// Per-block amount (PLUR) needed for `duration`.
37    pub amount_per_chunk: BigInt,
38    /// Total cost: `2^depth * amount_per_chunk` (PLUR).
39    pub total_cost: BigInt,
40    /// Duration in blocks (`duration / network.block_time`).
41    pub blocks: u64,
42}
43
44/// Compute the depth + amount for a `(size, duration)` tuple, using
45/// the chain's current price (PLUR/chunk/block) as the per-block
46/// price floor. Does not perform the purchase; pure preview.
47pub async fn get_storage_cost(
48    client: &Client,
49    size: Size,
50    duration: Duration,
51    network: Network,
52) -> Result<StorageCost, Error> {
53    let chain = client.debug().chain_state().await?;
54    let blocks = network.seconds_to_blocks(duration.as_secs());
55    let depth_i32 = crate::postage::get_depth_for_size(size.to_bytes());
56    let depth: u8 = depth_i32
57        .try_into()
58        .map_err(|_| Error::argument(format!("computed depth {depth_i32} out of u8 range")))?;
59    let amount_per_chunk = &chain.current_price * BigInt::from(blocks);
60    let total_cost = crate::postage::get_stamp_cost(depth_i32, &amount_per_chunk);
61    Ok(StorageCost {
62        depth,
63        amount_per_chunk,
64        total_cost,
65        blocks,
66    })
67}
68
69/// Preview + buy in one call: compute the cost via
70/// [`get_storage_cost`] and forward to
71/// [`crate::postage::PostageApi::create_postage_batch`]. Returns the
72/// freshly-minted [`BatchId`].
73pub async fn buy_storage(
74    client: &Client,
75    size: Size,
76    duration: Duration,
77    opts: &StorageOptions,
78) -> Result<BatchId, Error> {
79    let cost = get_storage_cost(client, size, duration, opts.network).await?;
80    client
81        .postage()
82        .create_postage_batch(&cost.amount_per_chunk, cost.depth, opts.label.as_deref())
83        .await
84}
85
86/// Top up an existing batch by the per-chunk amount needed to extend
87/// it for the given wall-clock `duration` on the chosen `network`.
88/// The on-chain top-up amount is `current_price * blocks(duration)`.
89pub async fn extend_storage_duration(
90    client: &Client,
91    batch_id: &BatchId,
92    duration: Duration,
93    network: Network,
94) -> Result<(), Error> {
95    let chain = client.debug().chain_state().await?;
96    let blocks = network.seconds_to_blocks(duration.as_secs());
97    let amount = &chain.current_price * BigInt::from(blocks);
98    client.postage().top_up_batch(batch_id, &amount).await
99}
100
101/// Dilute (deepen) an existing batch so its effective capacity covers
102/// `new_size`. No-op (returns `Ok(())`) if the batch is already deep
103/// enough.
104pub async fn extend_storage_size(
105    client: &Client,
106    batch_id: &BatchId,
107    new_size: Size,
108) -> Result<(), Error> {
109    let batch: PostageBatch = client.postage().get_postage_batch(batch_id).await?;
110    let target_depth_i32 = crate::postage::get_depth_for_size(new_size.to_bytes());
111    let target: u8 = target_depth_i32.try_into().map_err(|_| {
112        Error::argument(format!("computed depth {target_depth_i32} out of u8 range"))
113    })?;
114    if target <= batch.depth {
115        return Ok(());
116    }
117    client.postage().dilute_batch(batch_id, target).await
118}
119
120/// Total cost (PLUR) of extending a batch by `duration` on
121/// `network`, given the current per-chunk price from `/chainstate`.
122/// `current_price * blocks(duration) * 2^current_depth`.
123pub async fn get_duration_extension_cost(
124    client: &Client,
125    batch_id: &BatchId,
126    duration: Duration,
127    network: Network,
128) -> Result<BigInt, Error> {
129    let batch = client.postage().get_postage_batch(batch_id).await?;
130    let chain = client.debug().chain_state().await?;
131    let blocks = network.seconds_to_blocks(duration.as_secs());
132    let amount_per_chunk = &chain.current_price * BigInt::from(blocks);
133    Ok(crate::postage::get_stamp_cost(
134        batch.depth as i32,
135        &amount_per_chunk,
136    ))
137}
138
139/// Cost (PLUR) of growing a batch's effective capacity to cover
140/// `new_size`. The dilution cost equals
141/// `(2^new_depth - 2^old_depth) * batch.amount`. Returns `0` if the
142/// batch is already deep enough.
143pub async fn get_size_extension_cost(
144    client: &Client,
145    batch_id: &BatchId,
146    new_size: Size,
147) -> Result<BigInt, Error> {
148    let batch = client.postage().get_postage_batch(batch_id).await?;
149    let target_depth_i32 = crate::postage::get_depth_for_size(new_size.to_bytes());
150    let target: u8 = target_depth_i32.try_into().map_err(|_| {
151        Error::argument(format!("computed depth {target_depth_i32} out of u8 range"))
152    })?;
153    if target <= batch.depth {
154        return Ok(BigInt::from(0));
155    }
156    let amount = batch
157        .amount
158        .as_ref()
159        .ok_or_else(|| Error::argument("batch missing amount"))?;
160    let scale = BigInt::from(2u32).pow(target as u32) - BigInt::from(2u32).pow(batch.depth as u32);
161    Ok(scale * amount)
162}
163
164/// Top-up amount (PLUR) needed for the batch to reach `target_bzz`
165/// total spend, using the chain's current per-chunk price as the
166/// floor. Mirrors bee-js `calculateTopUpForBzz`.
167pub async fn calculate_top_up_for_bzz(
168    client: &Client,
169    batch_id: &BatchId,
170    target_bzz: &BigInt,
171) -> Result<BigInt, Error> {
172    let batch = client.postage().get_postage_batch(batch_id).await?;
173    let current = batch
174        .amount
175        .as_ref()
176        .ok_or_else(|| Error::argument("batch missing amount"))?;
177    if target_bzz <= current {
178        return Ok(BigInt::from(0));
179    }
180    Ok(target_bzz - current)
181}