Skip to main content

fyuz/api/
distributor.rs

1//! The `/distributor/*` endpoints: pot, shares and the public payout ledger.
2
3use crate::client::{seg, FyuzClient, QueryPairs};
4use crate::error::Result;
5use crate::models::{
6    AddressPayouts, Claimable, PayoutStats, Pot, RoundDetail, RoundReceipt, Shares,
7};
8use crate::params::{RoundsParams, SharesParams, ToQuery};
9
10/// The Distributor endpoints, reached with
11/// [`FyuzClient::distributor`](crate::FyuzClient::distributor).
12///
13/// The Distributor pays back a share of platform fees every round: most of the
14/// pot pro-rata by points, the remainder to one VRF-picked winner. Everything
15/// here is a public read of that ledger.
16///
17/// **Every `*_wei` field is an exact decimal string.** Lifetime totals run past
18/// `2^53`, so parsing them into `f64` corrupts them silently. See the
19/// [`models`](crate::models) docs.
20///
21/// ```no_run
22/// # async fn demo() -> Result<(), fyuz::Error> {
23/// let client = fyuz::FyuzClient::new()?;
24/// let stats = client.distributor().get_stats().await?;
25/// println!("{} wei paid out over {} rounds", stats.total_paid_wei, stats.rounds_settled);
26/// # Ok(())
27/// # }
28/// ```
29#[derive(Debug, Clone, Copy)]
30pub struct Distributor<'a> {
31    client: &'a FyuzClient,
32}
33
34impl<'a> Distributor<'a> {
35    pub(crate) fn new(client: &'a FyuzClient) -> Self {
36        Self { client }
37    }
38
39    /// `GET /distributor/stats` — lifetime payback totals.
40    ///
41    /// ```no_run
42    /// # async fn demo() -> Result<(), fyuz::Error> {
43    /// let client = fyuz::FyuzClient::new()?;
44    /// let stats = client.distributor().get_stats().await?;
45    /// // Decimal string, not a number: print it, do not parse it.
46    /// println!("largest single payout: {} wei", stats.largest_payout_wei);
47    /// # Ok(())
48    /// # }
49    /// ```
50    pub async fn get_stats(&self) -> Result<PayoutStats> {
51        self.client
52            .get_json("/distributor/stats", QueryPairs::new())
53            .await
54    }
55
56    /// `GET /distributor/pot` — the live undistributed pot and the parameters
57    /// needed to price a point.
58    ///
59    /// [`Pot::pot_bnb`] and [`Pot::total_points`] are `None` when the value could
60    /// not be read. That is **unknown**, not zero — hide the figure rather than
61    /// showing `0`.
62    ///
63    /// ```no_run
64    /// # async fn demo() -> Result<(), fyuz::Error> {
65    /// let client = fyuz::FyuzClient::new()?;
66    /// let pot = client.distributor().get_pot().await?;
67    /// if let (Some(bnb), Some(points)) = (pot.pot_bnb, pot.total_points) {
68    ///     println!("{:.6} BNB per point", bnb * pot.distribute_bps / 10_000.0 / points);
69    /// }
70    /// # Ok(())
71    /// # }
72    /// ```
73    pub async fn get_pot(&self) -> Result<Pot> {
74        self.client
75            .get_json("/distributor/pot", QueryPairs::new())
76            .await
77    }
78
79    /// `GET /distributor/shares` — the share allocation for the current window,
80    /// plus the exact calldata the round-runner posts on-chain.
81    ///
82    /// ```no_run
83    /// # async fn demo() -> Result<(), fyuz::Error> {
84    /// use fyuz::SharesParams;
85    ///
86    /// let client = fyuz::FyuzClient::new()?;
87    /// let shares = client.distributor().get_shares(&SharesParams::new().limit(50)).await?;
88    /// println!("{} holders, calldata {} bytes", shares.holders.len(), shares.packed.len() / 2 - 1);
89    /// # Ok(())
90    /// # }
91    /// ```
92    pub async fn get_shares(&self, params: &SharesParams) -> Result<Shares> {
93        self.client
94            .get_json("/distributor/shares", params.to_query())
95            .await
96    }
97
98    /// `GET /distributor/rounds` — the public payout ledger, newest first.
99    ///
100    /// ```no_run
101    /// # async fn demo() -> Result<(), fyuz::Error> {
102    /// use fyuz::RoundsParams;
103    ///
104    /// let client = fyuz::FyuzClient::new()?;
105    /// for round in client.distributor().list_rounds(&RoundsParams::new().limit(5)).await? {
106    ///     // `None` means "not indexed yet", never zero.
107    ///     match round.distributed_wei.as_deref() {
108    ///         Some(wei) => println!("round {}: {wei} wei", round.round_id),
109    ///         None => println!("round {}: pending settlement", round.round_id),
110    ///     }
111    /// }
112    /// # Ok(())
113    /// # }
114    /// ```
115    pub async fn list_rounds(&self, params: &RoundsParams) -> Result<Vec<RoundReceipt>> {
116        self.client
117            .get_json("/distributor/rounds", params.to_query())
118            .await
119    }
120
121    /// `GET /distributor/rounds/{id}` — one round's receipt and its recipient
122    /// list.
123    ///
124    /// The receipt fields are flattened into the top level of the JSON object;
125    /// in Rust they live under [`RoundDetail::round`].
126    ///
127    /// Answers `404` for an unknown round.
128    ///
129    /// ```no_run
130    /// # async fn demo() -> Result<(), fyuz::Error> {
131    /// let client = fyuz::FyuzClient::new()?;
132    /// let detail = client.distributor().get_round(7).await?;
133    /// println!("round {} paid {} wallets", detail.round.round_id, detail.payouts.len());
134    /// # Ok(())
135    /// # }
136    /// ```
137    pub async fn get_round(&self, id: i64) -> Result<RoundDetail> {
138        self.client
139            .get_json(&format!("/distributor/rounds/{id}"), QueryPairs::new())
140            .await
141    }
142
143    /// `GET /distributor/payouts/{address}` — every payout one wallet has
144    /// received.
145    ///
146    /// ```no_run
147    /// # async fn demo() -> Result<(), fyuz::Error> {
148    /// let client = fyuz::FyuzClient::new()?;
149    /// let paid = client
150    ///     .distributor()
151    ///     .get_payouts("0x1111111111111111111111111111111111111111")
152    ///     .await?;
153    /// println!("{} wei across {} rounds", paid.total_wei, paid.rounds_paid);
154    /// # Ok(())
155    /// # }
156    /// ```
157    pub async fn get_payouts(&self, address: &str) -> Result<AddressPayouts> {
158        self.client
159            .get_json(
160                &format!("/distributor/payouts/{}", seg(address)),
161                QueryPairs::new(),
162            )
163            .await
164    }
165
166    /// `GET /distributor/claimable/{address}` — wei owed from payout pushes the
167    /// contract could not complete.
168    ///
169    /// [`Claimable::claimable_wei`] is `None` when the RPC is unreachable or no
170    /// Distributor is configured. Hide the banner in that case rather than
171    /// telling someone they are owed nothing.
172    ///
173    /// ```no_run
174    /// # async fn demo() -> Result<(), fyuz::Error> {
175    /// let client = fyuz::FyuzClient::new()?;
176    /// let c = client
177    ///     .distributor()
178    ///     .get_claimable("0x1111111111111111111111111111111111111111")
179    ///     .await?;
180    ///
181    /// match c.claimable_wei.as_deref() {
182    ///     Some("0") => println!("nothing pending"),
183    ///     Some(wei) => println!("{wei} wei claimable from {:?}", c.distributor_address),
184    ///     None => println!("unknown — do not render a zero here"),
185    /// }
186    /// # Ok(())
187    /// # }
188    /// ```
189    pub async fn get_claimable(&self, address: &str) -> Result<Claimable> {
190        self.client
191            .get_json(
192                &format!("/distributor/claimable/{}", seg(address)),
193                QueryPairs::new(),
194            )
195            .await
196    }
197}