From 6f39c7e21874a0f5a1c6f3a10d9fb2775567637c Mon Sep 17 00:00:00 2001 From: Sild Date: Tue, 21 Jul 2026 13:14:28 +0200 Subject: [PATCH 1/4] stonks: add API client --- .github/workflows/build.yml | 1 + AGENTS.md | 2 + Cargo.lock | 14 +++ Cargo.toml | 1 + README.md | 1 + crates/stonks/AGENTS.md | 95 +++++++++++++++++ crates/stonks/CHANGELOG.md | 8 ++ crates/stonks/Cargo.toml | 23 ++++ crates/stonks/README.md | 56 ++++++++++ crates/stonks/src/api.rs | 128 +++++++++++++++++++++++ crates/stonks/src/api/request.rs | 34 ++++++ crates/stonks/src/api/response.rs | 29 +++++ crates/stonks/src/api/types.rs | 18 ++++ crates/stonks/src/api_client.rs | 20 ++++ crates/stonks/src/api_client/builder.rs | 38 +++++++ crates/stonks/src/lib.rs | 9 ++ crates/stonks/tests/test_api.rs | 38 +++++++ crates/stonks/tests/test_construction.rs | 53 ++++++++++ 18 files changed, 568 insertions(+) create mode 100644 crates/stonks/AGENTS.md create mode 100644 crates/stonks/CHANGELOG.md create mode 100644 crates/stonks/Cargo.toml create mode 100644 crates/stonks/README.md create mode 100644 crates/stonks/src/api.rs create mode 100644 crates/stonks/src/api/request.rs create mode 100644 crates/stonks/src/api/response.rs create mode 100644 crates/stonks/src/api/types.rs create mode 100644 crates/stonks/src/api_client.rs create mode 100644 crates/stonks/src/api_client/builder.rs create mode 100644 crates/stonks/src/lib.rs create mode 100644 crates/stonks/tests/test_api.rs create mode 100644 crates/stonks/tests/test_construction.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 55d421f..ea8aab6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,6 +38,7 @@ jobs: for package in \ api_clients_core \ stonfi_api_client \ + stonks_api_client \ dedust_api_client \ swap_coffee_api_client \ tonco_api_client diff --git a/AGENTS.md b/AGENTS.md index fb3906b..c5586c8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,8 @@ Root guidance applies to the whole workspace. Also read the crate-local common error/result types. - `stonfi_api_client` (`crates/stonfi/`): REST wrapper for STON.fi API v1 and public export endpoints. +- `stonks_api_client` (`crates/stonks/`): REST wrapper for Stonks public-token + metadata and Virtual Pool address discovery. - `dedust_api_client` (`crates/dedust/`): REST wrapper for DeDust API v2. - `swap_coffee_api_client` (`crates/swap_coffee/`): REST wrapper for Swap Coffee API v1. diff --git a/Cargo.lock b/Cargo.lock index 06695ae..ccc17c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1895,6 +1895,20 @@ dependencies = [ "tokio-test", ] +[[package]] +name = "stonks_api_client" +version = "0.1.0" +dependencies = [ + "anyhow", + "api_clients_core", + "derive_more", + "derive_setters", + "serde", + "serde_json", + "serde_qs", + "tokio", +] + [[package]] name = "strsim" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index 7267468..bd43bc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/core", "crates/dedust", "crates/stonfi", + "crates/stonks", "crates/swap_coffee", "crates/tonco_api_client", "crates/bidask", diff --git a/README.md b/README.md index 3873e94..571d2cd 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ MRs are welcome. | Service | Client | Status | Capabilities | |-----------------------|----------------------------------------------------------|-------------|--------------| | https://ston.fi | [stonfi_api_client](crates/stonfi) | Supported | STON.fi API v1 assets, pools, farms, routers, swap/liquidity simulation, wallet views, stats, transactions, and public export feeds. | +| https://app.stonks.cash | [stonks_api_client](crates/stonks) | Supported | Stonks public-token tax metadata and paginated Virtual Pool address discovery. | | https://dedust.io | [dedust_api_client](crates/dedust) | Supported | DeDust API v2 assets, pools, pool trades, and routing plans. | | https://app.tonco.io/ | [tonco_api_client](crates/tonco_api_client) | Supported | Low-level Tonco Indexer GraphQL execution with caller-owned query/schema files and generated types. | | https://swap.coffee | [swap_coffee_api_client](crates/swap_coffee) | Supported | Swap Coffee API v1 tokens and pools. | diff --git a/crates/stonks/AGENTS.md b/crates/stonks/AGENTS.md new file mode 100644 index 0000000..dad939f --- /dev/null +++ b/crates/stonks/AGENTS.md @@ -0,0 +1,95 @@ +# AGENTS.md + +## Scope + +This crate is `stonks_api_client`, a public Rust library that wraps Stonks +public-token metadata and Virtual Pool address-discovery endpoints. + +Use the repository root `AGENTS.md` first, then this file. Use the +`rust-library-review` skill for public API, docs, package, or agent-guidance +changes. + +## Crate Purpose + +The crate exposes a thin typed client: + +- `StonksApiClient::builder().build()?` +- `client.api.exec(Request::Assets)` +- `client.api.exec(PoolsParams::new(page, size))` +- `client.api.exec(Request::AllPools)` +- request parameters in `api/request.rs` +- response enums and wire models in `api/response.rs` and `api/types.rs` + +Keep pool hydration, tax conversion, filtering, routing, and application retry +policy outside this crate. + +## Public API Boundary + +Treat these as public contracts: + +- `StonksApiClient` +- `DEFAULT_API_URL` +- `api::ApiClient` +- `Request` and `PoolsParams` +- `Response` and `Asset` +- `unwrap_response!` + +Request parameter and response/model POD structs are `#[non_exhaustive]`; use +constructors or `Default::default().with_(...)` rather than struct +literals in downstream code. Public enums are also `#[non_exhaustive]`, so +downstream matches need wildcard arms. + +The API returns `buyTax` and `sellTax` as raw percentage integers. Do not +convert them to basis points or apply an application-specific fee. Pool +discovery returns raw TON address strings rather than hydrated pool objects. + +`Request::AllPools` owns zero-based pagination with a fixed page size of 100. +It preserves response ordering and duplicates, stops after a short page, +returns no partial result if a request fails, and must remain sequential unless +the public behavior is deliberately redesigned. + +## Downstream Integration Example + +```rust +use stonks_api_client::api::{Request, Response}; +use stonks_api_client::api_client::StonksApiClient; + +# async fn example() -> anyhow::Result<()> { +let client = StonksApiClient::builder().build()?; +let response = client.api.exec(Request::Assets).await?; + +match response { + Response::Assets(assets) => println!("assets: {}", assets.len()), + other => anyhow::bail!("unexpected Stonks response: {other:?}"), +} +# Ok(()) +# } +``` + +## Live API Notes + +Tests in `tests/test_api.rs` call Stonks directly. Avoid assertions on volatile +address ordering, total counts, symbols, or tax values. Validate endpoint +routing, response shape, and required field parsing. + +## Changing The API + +Keep endpoint paths in `api.rs` and wire names in the request/response modules. +When the public API or supported endpoints change, update this file, the crate +README and rustdoc, integration tests, the root service table, and package +surface checks together. + +## Validation + +```bash +cargo test -p stonks_api_client --tests +cargo test -p stonks_api_client --doc +cargo +nightly fmt +cargo clippy -p stonks_api_client --all-targets --all-features -- -D warnings +RUSTDOCFLAGS="-D warnings" cargo doc -p stonks_api_client --no-deps +cargo +1.88.0 check -p stonks_api_client --all-targets --all-features +cargo package --list -p stonks_api_client +cargo publish --dry-run -p stonks_api_client +``` + +Version bumps and generated release changelog entries are owned by release-plz. diff --git a/crates/stonks/CHANGELOG.md b/crates/stonks/CHANGELOG.md new file mode 100644 index 0000000..11bddf3 --- /dev/null +++ b/crates/stonks/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] diff --git a/crates/stonks/Cargo.toml b/crates/stonks/Cargo.toml new file mode 100644 index 0000000..b5e8730 --- /dev/null +++ b/crates/stonks/Cargo.toml @@ -0,0 +1,23 @@ +[package] +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +name = "stonks_api_client" +version = "0.1.0" +description = "API client for Stonks public tokens and Virtual Pool discovery" + +[dependencies] +# Internal +api_clients_core.workspace = true + +# External +serde.workspace = true +derive_setters.workspace = true +derive_more.workspace = true + +[dev-dependencies] +tokio.workspace = true +anyhow.workspace = true +serde_json.workspace = true +serde_qs.workspace = true diff --git a/crates/stonks/README.md b/crates/stonks/README.md new file mode 100644 index 0000000..43b62c9 --- /dev/null +++ b/crates/stonks/README.md @@ -0,0 +1,56 @@ +# stonks_api_client + +Thin typed wrapper for the public [Stonks](https://app.stonks.cash) endpoints. + +Use this crate to load public-token tax metadata or discover Stonks Virtual +Pool addresses. The crate preserves the raw tax percentages returned by +Stonks; fee conversion, pool hydration, filtering, and routing belong in the +application layer. + +## Usage + +```toml +[dependencies] +stonks_api_client = "0.1" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +``` + +The minimum supported Rust version (MSRV) is 1.88. + +Run requests inside an async Tokio runtime. Pass [`PoolsParams`] directly to +the API client for one page, or use [`Request::AllPools`] to load every page. + +```rust,no_run +use stonks_api_client::api::{Request, Response}; +use stonks_api_client::api_client::StonksApiClient; + +# async fn example() -> Result<(), Box> { +let client = StonksApiClient::builder().build()?; +let response = client.api.exec(Request::AllPools).await?; + +match response { + Response::AllPools(addresses) => println!("Virtual Pools: {}", addresses.len()), + _ => println!("unexpected Stonks response variant"), +} +# Ok(()) +# } +``` + +[`PoolsParams`]: api::PoolsParams +[`Request::AllPools`]: api::Request::AllPools + +## Supported Endpoints + +| Method | Supported | +|-----------------------------------------------------------|-----------| +| `/api/deployments/public-tokens` | ✅ | +| `/api/virtual-deployments/non-bonded-tokens?page=&size=` | ✅ | + +Public request and response types are marked `#[non_exhaustive]` for semver +headroom. Build public POD structs with `Default::default().with_(...)` +or constructors, pass request parameters directly where `Into` is +implemented, and include wildcard arms when matching response enums. + +`Request::AllPools` starts at page zero, requests 100 addresses per page, and +returns only after every page succeeds. Live API results can change as tokens +bond and new deployments are created. diff --git a/crates/stonks/src/api.rs b/crates/stonks/src/api.rs new file mode 100644 index 0000000..3c500e1 --- /dev/null +++ b/crates/stonks/src/api.rs @@ -0,0 +1,128 @@ +mod request; +mod response; +mod types; + +use api_clients_core::{ApiClientsError, ApiClientsResult, Executor}; +use std::sync::Arc; + +pub use request::*; +pub use response::*; +pub use types::*; + +const ALL_POOLS_PAGE_SIZE: usize = 100; +const ASSETS_PATH: &str = "api/deployments/public-tokens"; +const POOLS_PATH: &str = "api/virtual-deployments/non-bonded-tokens"; + +/// Executes typed requests against the Stonks public API. +#[derive(Clone)] +pub struct ApiClient { + executor: Arc, +} + +impl ApiClient { + pub(crate) fn new(executor: Arc) -> Self { Self { executor } } + + /// Execute a Stonks request and return its matching response variant. + /// + /// `Request::AllPools` loads pages sequentially and returns no partial + /// result if any page fails. + /// + /// # Errors + /// + /// Returns an error when request serialization, transport, status handling, + /// response deserialization, or automatic pagination fails. + pub async fn exec(&self, request: REQUEST) -> ApiClientsResult + where + REQUEST: Into, + { + let request = request.into(); + let response = match &request { + Request::Assets => Response::Assets(self.executor.exec_get(ASSETS_PATH).await?), + Request::Pools(params) => Response::Pools(self.load_pool_page(params).await?), + Request::AllPools => Response::AllPools(self.load_all_pools().await?), + }; + Ok(response) + } + + async fn load_pool_page(&self, params: &PoolsParams) -> ApiClientsResult> { + self.executor.exec_get_extra(POOLS_PATH, params, &[]).await + } + + async fn load_all_pools(&self) -> ApiClientsResult> { + let mut pools = Vec::new(); + let mut page = 0_usize; + + loop { + let params = PoolsParams::new(page, ALL_POOLS_PAGE_SIZE); + let page_pools = self.load_pool_page(¶ms).await?; + + if append_pool_page(&mut pools, &mut page, page_pools)? { + return Ok(pools); + } + } + } +} + +fn append_pool_page(pools: &mut Vec, page: &mut usize, mut page_pools: Vec) -> ApiClientsResult { + let is_last_page = page_pools.len() < ALL_POOLS_PAGE_SIZE; + let next_page = if is_last_page { + None + } else { + Some( + page.checked_add(1) + .ok_or_else(|| ApiClientsError::Internal("Stonks pool pagination page overflow".to_string()))?, + ) + }; + + pools.append(&mut page_pools); + if let Some(next_page) = next_page { + *page = next_page; + } + Ok(is_last_page) +} + +#[cfg(test)] +mod tests { + use super::{append_pool_page, ALL_POOLS_PAGE_SIZE}; + + #[test] + fn test_full_pool_page_advances_pagination() -> anyhow::Result<()> { + let mut pools = Vec::new(); + let mut page = 0; + let page_pools = (0..ALL_POOLS_PAGE_SIZE).map(|index| index.to_string()).collect(); + + let is_last_page = append_pool_page(&mut pools, &mut page, page_pools)?; + + assert!(!is_last_page); + assert_eq!(page, 1); + assert_eq!(pools.len(), ALL_POOLS_PAGE_SIZE); + Ok(()) + } + + #[test] + fn test_short_pool_page_finishes_without_reordering_or_deduplication() -> anyhow::Result<()> { + let mut pools = vec!["first".to_string()]; + let mut page = 6; + + let is_last_page = + append_pool_page(&mut pools, &mut page, vec!["duplicate".to_string(), "duplicate".to_string()])?; + + assert!(is_last_page); + assert_eq!(page, 6); + assert_eq!(pools, vec!["first", "duplicate", "duplicate"]); + Ok(()) + } + + #[test] + fn test_pool_page_overflow_returns_error_without_appending() { + let mut pools = vec!["existing".to_string()]; + let mut page = usize::MAX; + let page_pools = vec!["new".to_string(); ALL_POOLS_PAGE_SIZE]; + + let result = append_pool_page(&mut pools, &mut page, page_pools); + + assert!(result.is_err()); + assert_eq!(page, usize::MAX); + assert_eq!(pools, vec!["existing"]); + } +} diff --git a/crates/stonks/src/api/request.rs b/crates/stonks/src/api/request.rs new file mode 100644 index 0000000..026a150 --- /dev/null +++ b/crates/stonks/src/api/request.rs @@ -0,0 +1,34 @@ +//! Raw Stonks request variants and query parameters. +//! +//! Names and fields intentionally mirror the upstream wire contract. +#![allow(missing_docs, reason = "raw request models mirror the upstream API contract")] + +use derive_more::From; +use derive_setters::Setters; +use serde::Serialize; + +#[derive(Clone, From)] +#[non_exhaustive] +pub enum Request { + #[from(skip)] + Assets, + Pools(PoolsParams), + #[from(skip)] + AllPools, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Setters)] +#[setters(prefix = "with_")] +#[non_exhaustive] +pub struct PoolsParams { + pub page: usize, + pub size: usize, +} + +impl PoolsParams { + pub fn new(page: usize, size: usize) -> Self { Self { page, size } } +} + +impl From<&Request> for Request { + fn from(request: &Request) -> Self { request.clone() } +} diff --git a/crates/stonks/src/api/response.rs b/crates/stonks/src/api/response.rs new file mode 100644 index 0000000..04a892c --- /dev/null +++ b/crates/stonks/src/api/response.rs @@ -0,0 +1,29 @@ +//! Raw Stonks response variants. +//! +//! Names and fields intentionally mirror the upstream wire contract. +#![allow(missing_docs, reason = "raw response models mirror the upstream API contract")] + +use crate::api::types::Asset; +use serde::Deserialize; + +#[macro_export] +macro_rules! unwrap_response { + ($variant:ident, $result:expr) => { + match $result { + $crate::api::Response::$variant(inner) => Ok(inner), + other => Err($crate::api_clients_core::ApiClientsError::UnexpectedResponse(format!( + "ApiClientError: expected {}, but got {:?}", + stringify!($variant), + other + ))), + } + }; +} + +#[derive(Deserialize, Debug, Clone)] +#[non_exhaustive] +pub enum Response { + Assets(Vec), + Pools(Vec), + AllPools(Vec), +} diff --git a/crates/stonks/src/api/types.rs b/crates/stonks/src/api/types.rs new file mode 100644 index 0000000..955bcb9 --- /dev/null +++ b/crates/stonks/src/api/types.rs @@ -0,0 +1,18 @@ +//! Raw Stonks response types. +//! +//! Names and fields intentionally mirror the upstream response schema. +#![allow(missing_docs, reason = "raw wire types mirror the upstream API contract")] + +use derive_setters::Setters; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Setters)] +#[setters(prefix = "with_")] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct Asset { + pub symbol: String, + pub address: String, + pub buy_tax: u16, + pub sell_tax: u16, +} diff --git a/crates/stonks/src/api_client.rs b/crates/stonks/src/api_client.rs new file mode 100644 index 0000000..cd6beea --- /dev/null +++ b/crates/stonks/src/api_client.rs @@ -0,0 +1,20 @@ +mod builder; + +use crate::api::ApiClient; +use crate::api_client::builder::Builder; + +/// Default base URL for Stonks public API requests. +pub const DEFAULT_API_URL: &str = "https://app.stonks.cash"; + +/// Stonks service client with an unversioned API child client. +#[derive(Clone)] +#[non_exhaustive] +pub struct StonksApiClient { + /// Stonks API execution client. + pub api: ApiClient, +} + +impl StonksApiClient { + /// Start configuring a Stonks client with the default endpoint. + pub fn builder() -> Builder { Builder::new() } +} diff --git a/crates/stonks/src/api_client/builder.rs b/crates/stonks/src/api_client/builder.rs new file mode 100644 index 0000000..2c99daf --- /dev/null +++ b/crates/stonks/src/api_client/builder.rs @@ -0,0 +1,38 @@ +use crate::api::ApiClient; +use crate::api_client::{StonksApiClient, DEFAULT_API_URL}; +use api_clients_core::{ApiClientsResult, Executor}; +use derive_setters::Setters; +use std::sync::Arc; + +/// Builder for [`StonksApiClient`]. +#[derive(Setters)] +#[setters(prefix = "with_", strip_option)] +#[non_exhaustive] +pub struct Builder { + api_url: String, + executor: Option>, +} + +impl Builder { + pub(super) fn new() -> Self { + Self { + api_url: DEFAULT_API_URL.to_string(), + executor: None, + } + } + + /// Build the configured Stonks client. + /// + /// # Errors + /// + /// Returns an error if the shared executor cannot be constructed. + pub fn build(self) -> ApiClientsResult { + let executor = match self.executor { + Some(executor) => executor, + None => Executor::builder(self.api_url).build()?.into(), + }; + + let api = ApiClient::new(executor); + Ok(StonksApiClient { api }) + } +} diff --git a/crates/stonks/src/lib.rs b/crates/stonks/src/lib.rs new file mode 100644 index 0000000..e94c68b --- /dev/null +++ b/crates/stonks/src/lib.rs @@ -0,0 +1,9 @@ +#![doc = include_str!("../README.md")] +#![warn(missing_docs)] + +/// Re-export of the shared executor and error crate used by this client. +pub use api_clients_core; +/// Stonks requests, responses, and wire models. +pub mod api; +/// Top-level Stonks client and builder. +pub mod api_client; diff --git a/crates/stonks/tests/test_api.rs b/crates/stonks/tests/test_api.rs new file mode 100644 index 0000000..1ccaadd --- /dev/null +++ b/crates/stonks/tests/test_api.rs @@ -0,0 +1,38 @@ +use anyhow::{Context, Result}; +use stonks_api_client::api::{PoolsParams, Request}; +use stonks_api_client::api_client::StonksApiClient; +use stonks_api_client::unwrap_response; + +fn init_client() -> Result { Ok(StonksApiClient::builder().build()?) } + +#[tokio::test] +async fn test_assets() -> Result<()> { + let client = init_client()?; + let assets = unwrap_response!(Assets, client.api.exec(Request::Assets).await?)?; + let asset = assets.first().context("Stonks returned no public tokens")?; + + assert!(!asset.symbol.is_empty()); + assert!(!asset.address.is_empty()); + Ok(()) +} + +#[tokio::test] +async fn test_pool_page() -> Result<()> { + let client = init_client()?; + let pools = unwrap_response!(Pools, client.api.exec(PoolsParams::new(0, 1)).await?)?; + let address = pools.first().context("Stonks returned no Virtual Pool addresses")?; + + assert!(pools.len() <= 1); + assert!(!address.is_empty()); + Ok(()) +} + +#[tokio::test] +async fn test_all_pools() -> Result<()> { + let client = init_client()?; + let pools = unwrap_response!(AllPools, client.api.exec(Request::AllPools).await?)?; + + assert!(!pools.is_empty()); + assert!(pools.iter().all(|address| !address.is_empty())); + Ok(()) +} diff --git a/crates/stonks/tests/test_construction.rs b/crates/stonks/tests/test_construction.rs new file mode 100644 index 0000000..defd901 --- /dev/null +++ b/crates/stonks/tests/test_construction.rs @@ -0,0 +1,53 @@ +use stonks_api_client::api::{Asset, PoolsParams, Request}; +use stonks_api_client::api_client::StonksApiClient; + +#[test] +fn test_client_exposes_api_executor() -> anyhow::Result<()> { + let client = StonksApiClient::builder().build()?; + let assets = client.api.exec(Request::Assets); + let pools = client.api.exec(PoolsParams::new(0, 10)); + let all_pools = client.api.exec(Request::AllPools); + + drop((assets, pools, all_pools)); + Ok(()) +} + +#[test] +fn test_pools_params_serialize_to_upstream_query_names() -> anyhow::Result<()> { + let params = PoolsParams::new(2, 50); + + assert_eq!(serde_qs::to_string(¶ms)?, "page=2&size=50"); + Ok(()) +} + +#[test] +fn test_asset_deserializes_all_upstream_fields() -> anyhow::Result<()> { + let asset: Asset = serde_json::from_value(serde_json::json!({ + "symbol": "TOKEN", + "address": "EQAddress", + "buyTax": 3, + "sellTax": 7 + }))?; + + assert_eq!(asset.symbol, "TOKEN"); + assert_eq!(asset.address, "EQAddress"); + assert_eq!(asset.buy_tax, 3); + assert_eq!(asset.sell_tax, 7); + Ok(()) +} + +#[test] +fn test_response_models_support_default_setter_construction() { + let asset = Asset::default() + .with_symbol("TOKEN".to_string()) + .with_address("EQAddress".to_string()) + .with_buy_tax(3) + .with_sell_tax(7); + let params = PoolsParams::default().with_page(2).with_size(50); + + assert_eq!(asset.symbol, "TOKEN"); + assert_eq!(asset.address, "EQAddress"); + assert_eq!(asset.buy_tax, 3); + assert_eq!(asset.sell_tax, 7); + assert_eq!(params, PoolsParams::new(2, 50)); +} From 87bad5e439e93035845936e7a0e4929a713ccc1d Mon Sep 17 00:00:00 2001 From: Sild Date: Tue, 21 Jul 2026 13:21:20 +0200 Subject: [PATCH 2/4] stonks: clarify pool address responses --- crates/stonks/src/api/response.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/stonks/src/api/response.rs b/crates/stonks/src/api/response.rs index 04a892c..d11709d 100644 --- a/crates/stonks/src/api/response.rs +++ b/crates/stonks/src/api/response.rs @@ -24,6 +24,8 @@ macro_rules! unwrap_response { #[non_exhaustive] pub enum Response { Assets(Vec), + /// One page of raw TON addresses for discovered Virtual Pools. Pools(Vec), + /// All raw TON addresses discovered across every Virtual Pool page. AllPools(Vec), } From 9ce9efe3d3f0e33cb0ba3e0c2538203e477090c0 Mon Sep 17 00:00:00 2001 From: Sild Date: Tue, 21 Jul 2026 13:33:02 +0200 Subject: [PATCH 3/4] stonks: clarify public API naming --- crates/stonks/AGENTS.md | 22 +++--- crates/stonks/README.md | 19 +++--- crates/stonks/src/api.rs | 86 ++++++++++++++---------- crates/stonks/src/api/request.rs | 10 +-- crates/stonks/src/api/response.rs | 8 +-- crates/stonks/src/api/types.rs | 2 +- crates/stonks/tests/test_api.rs | 30 +++++---- crates/stonks/tests/test_construction.rs | 40 +++++------ 8 files changed, 117 insertions(+), 100 deletions(-) diff --git a/crates/stonks/AGENTS.md b/crates/stonks/AGENTS.md index dad939f..bdf12ed 100644 --- a/crates/stonks/AGENTS.md +++ b/crates/stonks/AGENTS.md @@ -14,9 +14,9 @@ changes. The crate exposes a thin typed client: - `StonksApiClient::builder().build()?` -- `client.api.exec(Request::Assets)` -- `client.api.exec(PoolsParams::new(page, size))` -- `client.api.exec(Request::AllPools)` +- `client.api.exec(Request::PublicTokens)` +- `client.api.exec(VirtualPoolAddressesParams::new(page, size))` +- `client.api.exec(Request::AllVirtualPoolAddresses)` - request parameters in `api/request.rs` - response enums and wire models in `api/response.rs` and `api/types.rs` @@ -30,8 +30,8 @@ Treat these as public contracts: - `StonksApiClient` - `DEFAULT_API_URL` - `api::ApiClient` -- `Request` and `PoolsParams` -- `Response` and `Asset` +- `Request` and `VirtualPoolAddressesParams` +- `Response` and `PublicToken` - `unwrap_response!` Request parameter and response/model POD structs are `#[non_exhaustive]`; use @@ -43,10 +43,10 @@ The API returns `buyTax` and `sellTax` as raw percentage integers. Do not convert them to basis points or apply an application-specific fee. Pool discovery returns raw TON address strings rather than hydrated pool objects. -`Request::AllPools` owns zero-based pagination with a fixed page size of 100. -It preserves response ordering and duplicates, stops after a short page, -returns no partial result if a request fails, and must remain sequential unless -the public behavior is deliberately redesigned. +`Request::AllVirtualPoolAddresses` owns zero-based pagination with a fixed page +size of 100. It preserves response ordering and duplicates, stops after a short +page, returns no partial result if a request fails, and must remain sequential +unless the public behavior is deliberately redesigned. ## Downstream Integration Example @@ -56,10 +56,10 @@ use stonks_api_client::api_client::StonksApiClient; # async fn example() -> anyhow::Result<()> { let client = StonksApiClient::builder().build()?; -let response = client.api.exec(Request::Assets).await?; +let response = client.api.exec(Request::PublicTokens).await?; match response { - Response::Assets(assets) => println!("assets: {}", assets.len()), + Response::PublicTokens(tokens) => println!("public tokens: {}", tokens.len()), other => anyhow::bail!("unexpected Stonks response: {other:?}"), } # Ok(()) diff --git a/crates/stonks/README.md b/crates/stonks/README.md index 43b62c9..1645b37 100644 --- a/crates/stonks/README.md +++ b/crates/stonks/README.md @@ -17,8 +17,9 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread"] } The minimum supported Rust version (MSRV) is 1.88. -Run requests inside an async Tokio runtime. Pass [`PoolsParams`] directly to -the API client for one page, or use [`Request::AllPools`] to load every page. +Run requests inside an async Tokio runtime. Pass +[`VirtualPoolAddressesParams`] directly to the API client for one page, or use +[`Request::AllVirtualPoolAddresses`] to load every page. ```rust,no_run use stonks_api_client::api::{Request, Response}; @@ -26,18 +27,18 @@ use stonks_api_client::api_client::StonksApiClient; # async fn example() -> Result<(), Box> { let client = StonksApiClient::builder().build()?; -let response = client.api.exec(Request::AllPools).await?; +let response = client.api.exec(Request::AllVirtualPoolAddresses).await?; match response { - Response::AllPools(addresses) => println!("Virtual Pools: {}", addresses.len()), + Response::AllVirtualPoolAddresses(addresses) => println!("Virtual Pools: {}", addresses.len()), _ => println!("unexpected Stonks response variant"), } # Ok(()) # } ``` -[`PoolsParams`]: api::PoolsParams -[`Request::AllPools`]: api::Request::AllPools +[`VirtualPoolAddressesParams`]: api::VirtualPoolAddressesParams +[`Request::AllVirtualPoolAddresses`]: api::Request::AllVirtualPoolAddresses ## Supported Endpoints @@ -51,6 +52,6 @@ headroom. Build public POD structs with `Default::default().with_(...)` or constructors, pass request parameters directly where `Into` is implemented, and include wildcard arms when matching response enums. -`Request::AllPools` starts at page zero, requests 100 addresses per page, and -returns only after every page succeeds. Live API results can change as tokens -bond and new deployments are created. +`Request::AllVirtualPoolAddresses` starts at page zero, requests 100 addresses +per page, and returns only after every page succeeds. Live API results can +change as tokens bond and new deployments are created. diff --git a/crates/stonks/src/api.rs b/crates/stonks/src/api.rs index 3c500e1..722d4f7 100644 --- a/crates/stonks/src/api.rs +++ b/crates/stonks/src/api.rs @@ -9,9 +9,9 @@ pub use request::*; pub use response::*; pub use types::*; -const ALL_POOLS_PAGE_SIZE: usize = 100; -const ASSETS_PATH: &str = "api/deployments/public-tokens"; -const POOLS_PATH: &str = "api/virtual-deployments/non-bonded-tokens"; +const ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE: usize = 100; +const PUBLIC_TOKENS_PATH: &str = "api/deployments/public-tokens"; +const VIRTUAL_POOL_ADDRESSES_PATH: &str = "api/virtual-deployments/non-bonded-tokens"; /// Executes typed requests against the Stonks public API. #[derive(Clone)] @@ -24,8 +24,8 @@ impl ApiClient { /// Execute a Stonks request and return its matching response variant. /// - /// `Request::AllPools` loads pages sequentially and returns no partial - /// result if any page fails. + /// `Request::AllVirtualPoolAddresses` loads pages sequentially and returns + /// no partial result if any page fails. /// /// # Errors /// @@ -37,44 +37,55 @@ impl ApiClient { { let request = request.into(); let response = match &request { - Request::Assets => Response::Assets(self.executor.exec_get(ASSETS_PATH).await?), - Request::Pools(params) => Response::Pools(self.load_pool_page(params).await?), - Request::AllPools => Response::AllPools(self.load_all_pools().await?), + Request::PublicTokens => Response::PublicTokens(self.executor.exec_get(PUBLIC_TOKENS_PATH).await?), + Request::VirtualPoolAddresses(params) => { + Response::VirtualPoolAddresses(self.load_virtual_pool_addresses_page(params).await?) + } + Request::AllVirtualPoolAddresses => { + Response::AllVirtualPoolAddresses(self.load_all_virtual_pool_addresses().await?) + } }; Ok(response) } - async fn load_pool_page(&self, params: &PoolsParams) -> ApiClientsResult> { - self.executor.exec_get_extra(POOLS_PATH, params, &[]).await + async fn load_virtual_pool_addresses_page( + &self, + params: &VirtualPoolAddressesParams, + ) -> ApiClientsResult> { + self.executor.exec_get_extra(VIRTUAL_POOL_ADDRESSES_PATH, params, &[]).await } - async fn load_all_pools(&self) -> ApiClientsResult> { - let mut pools = Vec::new(); + async fn load_all_virtual_pool_addresses(&self) -> ApiClientsResult> { + let mut addresses = Vec::new(); let mut page = 0_usize; loop { - let params = PoolsParams::new(page, ALL_POOLS_PAGE_SIZE); - let page_pools = self.load_pool_page(¶ms).await?; + let params = VirtualPoolAddressesParams::new(page, ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE); + let page_addresses = self.load_virtual_pool_addresses_page(¶ms).await?; - if append_pool_page(&mut pools, &mut page, page_pools)? { - return Ok(pools); + if append_virtual_pool_addresses_page(&mut addresses, &mut page, page_addresses)? { + return Ok(addresses); } } } } -fn append_pool_page(pools: &mut Vec, page: &mut usize, mut page_pools: Vec) -> ApiClientsResult { - let is_last_page = page_pools.len() < ALL_POOLS_PAGE_SIZE; +fn append_virtual_pool_addresses_page( + addresses: &mut Vec, + page: &mut usize, + mut page_addresses: Vec, +) -> ApiClientsResult { + let is_last_page = page_addresses.len() < ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE; let next_page = if is_last_page { None } else { Some( page.checked_add(1) - .ok_or_else(|| ApiClientsError::Internal("Stonks pool pagination page overflow".to_string()))?, + .ok_or_else(|| ApiClientsError::Internal("Stonks Virtual Pool address page overflow".to_string()))?, ) }; - pools.append(&mut page_pools); + addresses.append(&mut page_addresses); if let Some(next_page) = next_page { *page = next_page; } @@ -83,46 +94,49 @@ fn append_pool_page(pools: &mut Vec, page: &mut usize, mut page_pools: V #[cfg(test)] mod tests { - use super::{append_pool_page, ALL_POOLS_PAGE_SIZE}; + use super::{append_virtual_pool_addresses_page, ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE}; #[test] - fn test_full_pool_page_advances_pagination() -> anyhow::Result<()> { - let mut pools = Vec::new(); + fn test_full_virtual_pool_address_page_advances_pagination() -> anyhow::Result<()> { + let mut addresses = Vec::new(); let mut page = 0; - let page_pools = (0..ALL_POOLS_PAGE_SIZE).map(|index| index.to_string()).collect(); + let page_addresses = (0..ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE).map(|index| index.to_string()).collect(); - let is_last_page = append_pool_page(&mut pools, &mut page, page_pools)?; + let is_last_page = append_virtual_pool_addresses_page(&mut addresses, &mut page, page_addresses)?; assert!(!is_last_page); assert_eq!(page, 1); - assert_eq!(pools.len(), ALL_POOLS_PAGE_SIZE); + assert_eq!(addresses.len(), ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE); Ok(()) } #[test] - fn test_short_pool_page_finishes_without_reordering_or_deduplication() -> anyhow::Result<()> { - let mut pools = vec!["first".to_string()]; + fn test_short_virtual_pool_address_page_preserves_order_and_duplicates() -> anyhow::Result<()> { + let mut addresses = vec!["first".to_string()]; let mut page = 6; - let is_last_page = - append_pool_page(&mut pools, &mut page, vec!["duplicate".to_string(), "duplicate".to_string()])?; + let is_last_page = append_virtual_pool_addresses_page( + &mut addresses, + &mut page, + vec!["duplicate".to_string(), "duplicate".to_string()], + )?; assert!(is_last_page); assert_eq!(page, 6); - assert_eq!(pools, vec!["first", "duplicate", "duplicate"]); + assert_eq!(addresses, vec!["first", "duplicate", "duplicate"]); Ok(()) } #[test] - fn test_pool_page_overflow_returns_error_without_appending() { - let mut pools = vec!["existing".to_string()]; + fn test_virtual_pool_address_page_overflow_returns_error_without_appending() { + let mut addresses = vec!["existing".to_string()]; let mut page = usize::MAX; - let page_pools = vec!["new".to_string(); ALL_POOLS_PAGE_SIZE]; + let page_addresses = vec!["new".to_string(); ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE]; - let result = append_pool_page(&mut pools, &mut page, page_pools); + let result = append_virtual_pool_addresses_page(&mut addresses, &mut page, page_addresses); assert!(result.is_err()); assert_eq!(page, usize::MAX); - assert_eq!(pools, vec!["existing"]); + assert_eq!(addresses, vec!["existing"]); } } diff --git a/crates/stonks/src/api/request.rs b/crates/stonks/src/api/request.rs index 026a150..7426508 100644 --- a/crates/stonks/src/api/request.rs +++ b/crates/stonks/src/api/request.rs @@ -11,21 +11,21 @@ use serde::Serialize; #[non_exhaustive] pub enum Request { #[from(skip)] - Assets, - Pools(PoolsParams), + PublicTokens, + VirtualPoolAddresses(VirtualPoolAddressesParams), #[from(skip)] - AllPools, + AllVirtualPoolAddresses, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Setters)] #[setters(prefix = "with_")] #[non_exhaustive] -pub struct PoolsParams { +pub struct VirtualPoolAddressesParams { pub page: usize, pub size: usize, } -impl PoolsParams { +impl VirtualPoolAddressesParams { pub fn new(page: usize, size: usize) -> Self { Self { page, size } } } diff --git a/crates/stonks/src/api/response.rs b/crates/stonks/src/api/response.rs index d11709d..5520e75 100644 --- a/crates/stonks/src/api/response.rs +++ b/crates/stonks/src/api/response.rs @@ -3,7 +3,7 @@ //! Names and fields intentionally mirror the upstream wire contract. #![allow(missing_docs, reason = "raw response models mirror the upstream API contract")] -use crate::api::types::Asset; +use crate::api::types::PublicToken; use serde::Deserialize; #[macro_export] @@ -23,9 +23,9 @@ macro_rules! unwrap_response { #[derive(Deserialize, Debug, Clone)] #[non_exhaustive] pub enum Response { - Assets(Vec), + PublicTokens(Vec), /// One page of raw TON addresses for discovered Virtual Pools. - Pools(Vec), + VirtualPoolAddresses(Vec), /// All raw TON addresses discovered across every Virtual Pool page. - AllPools(Vec), + AllVirtualPoolAddresses(Vec), } diff --git a/crates/stonks/src/api/types.rs b/crates/stonks/src/api/types.rs index 955bcb9..c180e4a 100644 --- a/crates/stonks/src/api/types.rs +++ b/crates/stonks/src/api/types.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; #[setters(prefix = "with_")] #[serde(rename_all = "camelCase")] #[non_exhaustive] -pub struct Asset { +pub struct PublicToken { pub symbol: String, pub address: String, pub buy_tax: u16, diff --git a/crates/stonks/tests/test_api.rs b/crates/stonks/tests/test_api.rs index 1ccaadd..9af3932 100644 --- a/crates/stonks/tests/test_api.rs +++ b/crates/stonks/tests/test_api.rs @@ -1,38 +1,40 @@ use anyhow::{Context, Result}; -use stonks_api_client::api::{PoolsParams, Request}; +use stonks_api_client::api::{Request, VirtualPoolAddressesParams}; use stonks_api_client::api_client::StonksApiClient; use stonks_api_client::unwrap_response; fn init_client() -> Result { Ok(StonksApiClient::builder().build()?) } #[tokio::test] -async fn test_assets() -> Result<()> { +async fn test_public_tokens() -> Result<()> { let client = init_client()?; - let assets = unwrap_response!(Assets, client.api.exec(Request::Assets).await?)?; - let asset = assets.first().context("Stonks returned no public tokens")?; + let public_tokens = unwrap_response!(PublicTokens, client.api.exec(Request::PublicTokens).await?)?; + let public_token = public_tokens.first().context("Stonks returned no public tokens")?; - assert!(!asset.symbol.is_empty()); - assert!(!asset.address.is_empty()); + assert!(!public_token.symbol.is_empty()); + assert!(!public_token.address.is_empty()); Ok(()) } #[tokio::test] -async fn test_pool_page() -> Result<()> { +async fn test_virtual_pool_addresses_page() -> Result<()> { let client = init_client()?; - let pools = unwrap_response!(Pools, client.api.exec(PoolsParams::new(0, 1)).await?)?; - let address = pools.first().context("Stonks returned no Virtual Pool addresses")?; + let addresses = + unwrap_response!(VirtualPoolAddresses, client.api.exec(VirtualPoolAddressesParams::new(0, 1)).await?)?; + let address = addresses.first().context("Stonks returned no Virtual Pool addresses")?; - assert!(pools.len() <= 1); + assert!(addresses.len() <= 1); assert!(!address.is_empty()); Ok(()) } #[tokio::test] -async fn test_all_pools() -> Result<()> { +async fn test_all_virtual_pool_addresses() -> Result<()> { let client = init_client()?; - let pools = unwrap_response!(AllPools, client.api.exec(Request::AllPools).await?)?; + let addresses = + unwrap_response!(AllVirtualPoolAddresses, client.api.exec(Request::AllVirtualPoolAddresses).await?)?; - assert!(!pools.is_empty()); - assert!(pools.iter().all(|address| !address.is_empty())); + assert!(!addresses.is_empty()); + assert!(addresses.iter().all(|address| !address.is_empty())); Ok(()) } diff --git a/crates/stonks/tests/test_construction.rs b/crates/stonks/tests/test_construction.rs index defd901..8c813d5 100644 --- a/crates/stonks/tests/test_construction.rs +++ b/crates/stonks/tests/test_construction.rs @@ -1,53 +1,53 @@ -use stonks_api_client::api::{Asset, PoolsParams, Request}; +use stonks_api_client::api::{PublicToken, Request, VirtualPoolAddressesParams}; use stonks_api_client::api_client::StonksApiClient; #[test] fn test_client_exposes_api_executor() -> anyhow::Result<()> { let client = StonksApiClient::builder().build()?; - let assets = client.api.exec(Request::Assets); - let pools = client.api.exec(PoolsParams::new(0, 10)); - let all_pools = client.api.exec(Request::AllPools); + let public_tokens = client.api.exec(Request::PublicTokens); + let virtual_pool_addresses = client.api.exec(VirtualPoolAddressesParams::new(0, 10)); + let all_virtual_pool_addresses = client.api.exec(Request::AllVirtualPoolAddresses); - drop((assets, pools, all_pools)); + drop((public_tokens, virtual_pool_addresses, all_virtual_pool_addresses)); Ok(()) } #[test] -fn test_pools_params_serialize_to_upstream_query_names() -> anyhow::Result<()> { - let params = PoolsParams::new(2, 50); +fn test_virtual_pool_addresses_params_serialize_to_upstream_query_names() -> anyhow::Result<()> { + let params = VirtualPoolAddressesParams::new(2, 50); assert_eq!(serde_qs::to_string(¶ms)?, "page=2&size=50"); Ok(()) } #[test] -fn test_asset_deserializes_all_upstream_fields() -> anyhow::Result<()> { - let asset: Asset = serde_json::from_value(serde_json::json!({ +fn test_public_token_deserializes_all_upstream_fields() -> anyhow::Result<()> { + let public_token: PublicToken = serde_json::from_value(serde_json::json!({ "symbol": "TOKEN", "address": "EQAddress", "buyTax": 3, "sellTax": 7 }))?; - assert_eq!(asset.symbol, "TOKEN"); - assert_eq!(asset.address, "EQAddress"); - assert_eq!(asset.buy_tax, 3); - assert_eq!(asset.sell_tax, 7); + assert_eq!(public_token.symbol, "TOKEN"); + assert_eq!(public_token.address, "EQAddress"); + assert_eq!(public_token.buy_tax, 3); + assert_eq!(public_token.sell_tax, 7); Ok(()) } #[test] fn test_response_models_support_default_setter_construction() { - let asset = Asset::default() + let public_token = PublicToken::default() .with_symbol("TOKEN".to_string()) .with_address("EQAddress".to_string()) .with_buy_tax(3) .with_sell_tax(7); - let params = PoolsParams::default().with_page(2).with_size(50); + let params = VirtualPoolAddressesParams::default().with_page(2).with_size(50); - assert_eq!(asset.symbol, "TOKEN"); - assert_eq!(asset.address, "EQAddress"); - assert_eq!(asset.buy_tax, 3); - assert_eq!(asset.sell_tax, 7); - assert_eq!(params, PoolsParams::new(2, 50)); + assert_eq!(public_token.symbol, "TOKEN"); + assert_eq!(public_token.address, "EQAddress"); + assert_eq!(public_token.buy_tax, 3); + assert_eq!(public_token.sell_tax, 7); + assert_eq!(params, VirtualPoolAddressesParams::new(2, 50)); } From b52604e88436debb150f967010ac3b3167813eac Mon Sep 17 00:00:00 2001 From: Sild Date: Tue, 21 Jul 2026 14:08:59 +0200 Subject: [PATCH 4/4] stonks: harden pre-release API contract --- crates/stonks/AGENTS.md | 3 +++ crates/stonks/README.md | 3 ++- crates/stonks/src/api.rs | 16 ++++++++-------- crates/stonks/src/api/request.rs | 16 ++++++++++++---- crates/stonks/src/api/response.rs | 8 +++++++- crates/stonks/src/api/types.rs | 6 +++++- crates/stonks/tests/test_construction.rs | 2 +- 7 files changed, 38 insertions(+), 16 deletions(-) diff --git a/crates/stonks/AGENTS.md b/crates/stonks/AGENTS.md index bdf12ed..c9c98b3 100644 --- a/crates/stonks/AGENTS.md +++ b/crates/stonks/AGENTS.md @@ -48,6 +48,9 @@ size of 100. It preserves response ordering and duplicates, stops after a short page, returns no partial result if a request fails, and must remain sequential unless the public behavior is deliberately redesigned. +`VirtualPoolAddressesParams` uses fixed-width `u32` values for both `page` and +`size`; do not replace wire pagination with target-width integer types. + ## Downstream Integration Example ```rust diff --git a/crates/stonks/README.md b/crates/stonks/README.md index 1645b37..2254439 100644 --- a/crates/stonks/README.md +++ b/crates/stonks/README.md @@ -19,7 +19,8 @@ The minimum supported Rust version (MSRV) is 1.88. Run requests inside an async Tokio runtime. Pass [`VirtualPoolAddressesParams`] directly to the API client for one page, or use -[`Request::AllVirtualPoolAddresses`] to load every page. +[`Request::AllVirtualPoolAddresses`] to load every page. Page numbers are +zero-based, and both pagination values use `u32`. ```rust,no_run use stonks_api_client::api::{Request, Response}; diff --git a/crates/stonks/src/api.rs b/crates/stonks/src/api.rs index 722d4f7..60e3b96 100644 --- a/crates/stonks/src/api.rs +++ b/crates/stonks/src/api.rs @@ -9,7 +9,7 @@ pub use request::*; pub use response::*; pub use types::*; -const ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE: usize = 100; +const ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE: u32 = 100; const PUBLIC_TOKENS_PATH: &str = "api/deployments/public-tokens"; const VIRTUAL_POOL_ADDRESSES_PATH: &str = "api/virtual-deployments/non-bonded-tokens"; @@ -57,7 +57,7 @@ impl ApiClient { async fn load_all_virtual_pool_addresses(&self) -> ApiClientsResult> { let mut addresses = Vec::new(); - let mut page = 0_usize; + let mut page = 0_u32; loop { let params = VirtualPoolAddressesParams::new(page, ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE); @@ -72,10 +72,10 @@ impl ApiClient { fn append_virtual_pool_addresses_page( addresses: &mut Vec, - page: &mut usize, + page: &mut u32, mut page_addresses: Vec, ) -> ApiClientsResult { - let is_last_page = page_addresses.len() < ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE; + let is_last_page = page_addresses.len() < ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE as usize; let next_page = if is_last_page { None } else { @@ -106,7 +106,7 @@ mod tests { assert!(!is_last_page); assert_eq!(page, 1); - assert_eq!(addresses.len(), ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE); + assert_eq!(addresses.len(), ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE as usize); Ok(()) } @@ -130,13 +130,13 @@ mod tests { #[test] fn test_virtual_pool_address_page_overflow_returns_error_without_appending() { let mut addresses = vec!["existing".to_string()]; - let mut page = usize::MAX; - let page_addresses = vec!["new".to_string(); ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE]; + let mut page = u32::MAX; + let page_addresses = vec!["new".to_string(); ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE as usize]; let result = append_virtual_pool_addresses_page(&mut addresses, &mut page, page_addresses); assert!(result.is_err()); - assert_eq!(page, usize::MAX); + assert_eq!(page, u32::MAX); assert_eq!(addresses, vec!["existing"]); } } diff --git a/crates/stonks/src/api/request.rs b/crates/stonks/src/api/request.rs index 7426508..1d18a08 100644 --- a/crates/stonks/src/api/request.rs +++ b/crates/stonks/src/api/request.rs @@ -1,32 +1,40 @@ //! Raw Stonks request variants and query parameters. //! //! Names and fields intentionally mirror the upstream wire contract. -#![allow(missing_docs, reason = "raw request models mirror the upstream API contract")] use derive_more::From; use derive_setters::Setters; use serde::Serialize; +/// A request supported by the unversioned Stonks API client. #[derive(Clone, From)] #[non_exhaustive] pub enum Request { + /// Load public-token symbols, addresses, and raw tax percentages. #[from(skip)] PublicTokens, + /// Load one page of raw TON addresses for discovered Virtual Pools. VirtualPoolAddresses(VirtualPoolAddressesParams), + /// Load raw TON addresses from every Virtual Pool page sequentially. #[from(skip)] AllVirtualPoolAddresses, } +/// Pagination parameters for one Virtual Pool address page. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Setters)] #[setters(prefix = "with_")] #[non_exhaustive] pub struct VirtualPoolAddressesParams { - pub page: usize, - pub size: usize, + /// Zero-based page number. + pub page: u32, + /// Maximum number of addresses requested for the page. + pub size: u32, } impl VirtualPoolAddressesParams { - pub fn new(page: usize, size: usize) -> Self { Self { page, size } } + /// Create pagination parameters from a zero-based page number and page size. + #[must_use] + pub fn new(page: u32, size: u32) -> Self { Self { page, size } } } impl From<&Request> for Request { diff --git a/crates/stonks/src/api/response.rs b/crates/stonks/src/api/response.rs index 5520e75..0e1e076 100644 --- a/crates/stonks/src/api/response.rs +++ b/crates/stonks/src/api/response.rs @@ -1,11 +1,15 @@ //! Raw Stonks response variants. //! //! Names and fields intentionally mirror the upstream wire contract. -#![allow(missing_docs, reason = "raw response models mirror the upstream API contract")] use crate::api::types::PublicToken; use serde::Deserialize; +/// Extract the expected payload from a Stonks [`Response`](crate::api::Response). +/// +/// Returns +/// [`ApiClientsError::UnexpectedResponse`](crate::api_clients_core::ApiClientsError::UnexpectedResponse) +/// when the response variant does not match the requested variant name. #[macro_export] macro_rules! unwrap_response { ($variant:ident, $result:expr) => { @@ -20,9 +24,11 @@ macro_rules! unwrap_response { }; } +/// A typed response returned by the unversioned Stonks API client. #[derive(Deserialize, Debug, Clone)] #[non_exhaustive] pub enum Response { + /// Public-token metadata, including raw buy and sell tax percentages. PublicTokens(Vec), /// One page of raw TON addresses for discovered Virtual Pools. VirtualPoolAddresses(Vec), diff --git a/crates/stonks/src/api/types.rs b/crates/stonks/src/api/types.rs index c180e4a..d702078 100644 --- a/crates/stonks/src/api/types.rs +++ b/crates/stonks/src/api/types.rs @@ -1,18 +1,22 @@ //! Raw Stonks response types. //! //! Names and fields intentionally mirror the upstream response schema. -#![allow(missing_docs, reason = "raw wire types mirror the upstream API contract")] use derive_setters::Setters; use serde::{Deserialize, Serialize}; +/// Public-token metadata returned by Stonks. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Setters)] #[setters(prefix = "with_")] #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct PublicToken { + /// Token symbol supplied by Stonks. pub symbol: String, + /// Raw TON token address supplied by Stonks. pub address: String, + /// Raw buy-tax percentage; this value is not converted to basis points. pub buy_tax: u16, + /// Raw sell-tax percentage; this value is not converted to basis points. pub sell_tax: u16, } diff --git a/crates/stonks/tests/test_construction.rs b/crates/stonks/tests/test_construction.rs index 8c813d5..67bfbf7 100644 --- a/crates/stonks/tests/test_construction.rs +++ b/crates/stonks/tests/test_construction.rs @@ -14,7 +14,7 @@ fn test_client_exposes_api_executor() -> anyhow::Result<()> { #[test] fn test_virtual_pool_addresses_params_serialize_to_upstream_query_names() -> anyhow::Result<()> { - let params = VirtualPoolAddressesParams::new(2, 50); + let params = VirtualPoolAddressesParams::new(2_u32, 50_u32); assert_eq!(serde_qs::to_string(¶ms)?, "page=2&size=50"); Ok(())