Skip to main content

fyuz/
lib.rs

1//! Async Rust client for the [Fyuz](https://fyuz.fun) public REST API.
2//!
3//! Fyuz is a bonding-curve token launchpad on BNB Smart Chain. A new token
4//! trades on an internal bonding curve until it reaches a **$30,000 market cap**,
5//! at which point it *graduates* into a PancakeSwap V2 pair.
6//!
7//! **Before graduation there is no DEX pool anywhere.** `pair_address` is
8//! `None`, no on-chain pair exists to quote, and this API is the *only* source
9//! of price, liquidity and volume data for that token. Anything you build that
10//! reads pre-graduation prices from a DEX aggregator will simply find nothing.
11//!
12//! Every endpoint in this crate is unauthenticated and safe to poll.
13//!
14//! # Quickstart
15//!
16//! ```no_run
17//! use fyuz::{DiscoverParams, DiscoverTab, FyuzClient};
18//!
19//! #[tokio::main]
20//! async fn main() -> Result<(), fyuz::Error> {
21//!     let client = FyuzClient::new()?;
22//!
23//!     let trending = client
24//!         .discover(&DiscoverParams::new().tab(DiscoverTab::Trending).limit(10))
25//!         .await?;
26//!
27//!     for token in &trending {
28//!         println!(
29//!             "{:<10} ${:>12.0} mcap   {:>6.2}% to graduation",
30//!             token.symbol, token.marketcap, token.graduation_pct
31//!         );
32//!     }
33//!
34//!     Ok(())
35//! }
36//! ```
37//!
38//! # Decimal strings, and why they are not floats
39//!
40//! Token balances, prices, market caps and every `*_wei` field come back as
41//! **decimal strings** and stay [`String`] in this crate's models. An 18-decimal
42//! token amount, or a `uint256` wei total, does not survive a round trip through
43//! `f64`:
44//!
45//! ```
46//! let wire = "123456789012345678901234567890";
47//! // What a "just parse it as a number" client would do:
48//! let lossy = wire.parse::<f64>().unwrap();
49//! assert_ne!(format!("{lossy:.0}"), wire); // silently wrong
50//! ```
51//!
52//! If you need arithmetic, parse into a big-integer or decimal type of your
53//! choosing (`rust_decimal`, `bigdecimal`, `primitive_types::U256`, …) at the
54//! edge of your own code. The SDK deliberately does not pick one for you, and
55//! deliberately never parses these into `f64` on your behalf.
56//!
57//! Aggregates that the server already computed in floating point — `volume_usd`,
58//! `points`, `reward_eth`, `pot_bnb`, PnL figures — arrive as JSON numbers and
59//! are modelled as `f64`.
60//!
61//! Timestamps get the same treatment for the same reason, one step further: the
62//! RFC-3339 `created_at`/`updated_at` strings and the unix-second `date` and
63//! `round_end` fields stay as they arrived. There is no date type in `core`, and
64//! picking `chrono` or `time` for you would put a dependency — and a major
65//! version of it — into every downstream build to save one `parse` call. This is
66//! the one helper the TypeScript, Go and Python SDKs have that this one
67//! deliberately does not.
68//!
69//! # Where Fyuz is deployed
70//!
71//! [`addresses`] carries the deployed proxy addresses as compile-time constants,
72//! looked up by chain id or slug:
73//!
74//! ```
75//! let bsc = fyuz::deployment_for_chain_id(56).expect("mainnet is in the book");
76//! assert_eq!(bsc.fyuz, "0x33a98bef6496684a8dac83734d9ceb0cefc7019c");
77//! ```
78//!
79//! `GET /config` still wins wherever it answers — the launchpad is behind an
80//! upgradeable proxy, and a published crate that trusted its own copy would keep
81//! calling a dead address after a redeploy. The book is what covers the cases the
82//! API cannot: BSC testnet, which the public API does not serve at all, and a
83//! `/config` response that arrives without a `contractAddress`.
84//!
85//! # `None` is not `0`
86//!
87//! [`Pot::pot_bnb`](models::Pot::pot_bnb),
88//! [`Claimable::claimable_wei`](models::Claimable::claimable_wei),
89//! [`TokenDetail::curve_holding`](models::TokenDetail::curve_holding) and the
90//! nullable `*_wei` fields on [`RoundReceipt`] are `None`
91//! when the value is **unknown** — the RPC was unreachable, or the settlement is
92//! not indexed yet. Rendering `0` in that case tells a user something false.
93//!
94//! # Rate limits and retries
95//!
96//! The API allows 120 requests per minute per IP and answers `429` with
97//! `{"error": "Too many requests"}` past that. The client retries `429`s, `5xx`s
98//! and transport failures automatically with exponential backoff and full
99//! jitter, honouring `Retry-After` when the server sends one. Other `4xx`
100//! statuses are never retried. Three retries is the default:
101//!
102//! ```no_run
103//! # fn demo() -> Result<(), fyuz::Error> {
104//! use std::time::Duration;
105//!
106//! let client = fyuz::FyuzClient::builder()
107//!     .timeout(Duration::from_secs(10))
108//!     .max_retries(5)
109//!     .user_agent("acme-indexer/2.1 (ops@acme.example)")
110//!     .build()?;
111//! # let _ = client;
112//! # Ok(())
113//! # }
114//! ```
115//!
116//! Once the retries are spent the call fails with [`Error::RateLimited`], which
117//! is a distinct variant precisely so a backpressure handler can match on it.
118//!
119//! # What is not here
120//!
121//! Only the public, unauthenticated surface is implemented. The API-key-gated
122//! routes (recording a distribution round, posting chats, watchlists, paper
123//! trading, airdrops, …) are deliberately absent — including `POST
124//! /distributor/rounds`, which shares a path with
125//! [`Distributor::list_rounds`] but is not a public endpoint.
126
127#![deny(missing_docs)]
128#![deny(rustdoc::broken_intra_doc_links)]
129#![warn(clippy::all)]
130
131mod abi;
132mod api;
133mod client;
134mod error;
135mod pagination;
136mod params;
137mod revert;
138mod rpc;
139mod trade;
140
141pub mod addresses;
142pub mod models;
143
144pub use abi::{
145    address_word, bool_word, decode_address_at, decode_bool_at, decode_uint256_at, encode_call,
146    format_units, normalize_address, parse_units, uint256_word, SELECTOR_ALLOWANCE,
147    SELECTOR_APPROVE, SELECTOR_BALANCE_OF, SELECTOR_GET_FIRST_BUY_FEE,
148    SELECTOR_GET_MAX_SELLABLE_ETH, SELECTOR_GET_SWAP_OUTPUT, SELECTOR_PAUSED,
149    SELECTOR_PLATFORM_BUY_FEE_BPS, SELECTOR_SWAP_ETH_FOR_EXACT_TOKENS,
150    SELECTOR_SWAP_EXACT_ETH_FOR_TOKENS, SELECTOR_SWAP_EXACT_TOKENS_FOR_ETH,
151    SELECTOR_TOKEN_OWNER_FEE_BPS, SELECTOR_TOKEN_POOLS, U256,
152};
153pub use addresses::{
154    deployment_for_chain_id, deployment_for_network, Deployment, BSC, BSC_TESTNET, DEPLOYMENTS,
155};
156pub use api::distributor::Distributor;
157pub use client::{FyuzClient, FyuzClientBuilder, DEFAULT_BASE_URL, MAX_RESPONSE_BYTES, VERSION};
158pub use error::{Error, Result};
159pub use pagination::TokenPager;
160pub use params::{
161    ChartDataParams, DiscoverParams, DiscoverTab, ListTokensParams, RecentTradesParams,
162    RoundsParams, SharesParams, SortDirection, TokenDetailParams, TokenStatus, TokenTradesParams,
163    TopHoldersParams,
164};
165pub use revert::{revert_error, revert_selector, RevertKind, SELECTOR_ALREADY_LAUNCHED};
166pub use rpc::{CallOutcome, RpcClient, DEFAULT_BLOCK_TAG, DEFAULT_RPC_TIMEOUT};
167pub use trade::{
168    BuiltBuy, BuiltSell, BuyExactTokensParams, BuyParams, BuyQuote, SellParams, SellQuote,
169    TradeApi, UnsignedTransaction, DEFAULT_DEADLINE_SECONDS, DEFAULT_NETWORK, MAX_DEADLINE_SECONDS,
170};
171
172// Every response model is re-exported at the crate root for convenience; they
173// also live in `fyuz::models` if you prefer the namespace.
174pub use models::*;