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..c9c98b3 --- /dev/null +++ b/crates/stonks/AGENTS.md @@ -0,0 +1,98 @@ +# 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::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` + +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 `VirtualPoolAddressesParams` +- `Response` and `PublicToken` +- `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::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. + +`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 +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::PublicTokens).await?; + +match response { + Response::PublicTokens(tokens) => println!("public tokens: {}", tokens.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..2254439 --- /dev/null +++ b/crates/stonks/README.md @@ -0,0 +1,58 @@ +# 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 +[`VirtualPoolAddressesParams`] directly to the API client for one page, or use +[`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}; +use stonks_api_client::api_client::StonksApiClient; + +# async fn example() -> Result<(), Box> { +let client = StonksApiClient::builder().build()?; +let response = client.api.exec(Request::AllVirtualPoolAddresses).await?; + +match response { + Response::AllVirtualPoolAddresses(addresses) => println!("Virtual Pools: {}", addresses.len()), + _ => println!("unexpected Stonks response variant"), +} +# Ok(()) +# } +``` + +[`VirtualPoolAddressesParams`]: api::VirtualPoolAddressesParams +[`Request::AllVirtualPoolAddresses`]: api::Request::AllVirtualPoolAddresses + +## 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::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 new file mode 100644 index 0000000..60e3b96 --- /dev/null +++ b/crates/stonks/src/api.rs @@ -0,0 +1,142 @@ +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_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"; + +/// 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::AllVirtualPoolAddresses` 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::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_virtual_pool_addresses_page( + &self, + params: &VirtualPoolAddressesParams, + ) -> ApiClientsResult> { + self.executor.exec_get_extra(VIRTUAL_POOL_ADDRESSES_PATH, params, &[]).await + } + + async fn load_all_virtual_pool_addresses(&self) -> ApiClientsResult> { + let mut addresses = Vec::new(); + let mut page = 0_u32; + + loop { + let params = VirtualPoolAddressesParams::new(page, ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE); + let page_addresses = self.load_virtual_pool_addresses_page(¶ms).await?; + + if append_virtual_pool_addresses_page(&mut addresses, &mut page, page_addresses)? { + return Ok(addresses); + } + } + } +} + +fn append_virtual_pool_addresses_page( + addresses: &mut Vec, + page: &mut u32, + mut page_addresses: Vec, +) -> ApiClientsResult { + let is_last_page = page_addresses.len() < ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE as usize; + let next_page = if is_last_page { + None + } else { + Some( + page.checked_add(1) + .ok_or_else(|| ApiClientsError::Internal("Stonks Virtual Pool address page overflow".to_string()))?, + ) + }; + + addresses.append(&mut page_addresses); + if let Some(next_page) = next_page { + *page = next_page; + } + Ok(is_last_page) +} + +#[cfg(test)] +mod tests { + use super::{append_virtual_pool_addresses_page, ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE}; + + #[test] + fn test_full_virtual_pool_address_page_advances_pagination() -> anyhow::Result<()> { + let mut addresses = Vec::new(); + let mut page = 0; + let page_addresses = (0..ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE).map(|index| index.to_string()).collect(); + + 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!(addresses.len(), ALL_VIRTUAL_POOL_ADDRESSES_PAGE_SIZE as usize); + Ok(()) + } + + #[test] + 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_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!(addresses, vec!["first", "duplicate", "duplicate"]); + Ok(()) + } + + #[test] + fn test_virtual_pool_address_page_overflow_returns_error_without_appending() { + let mut addresses = vec!["existing".to_string()]; + 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, u32::MAX); + assert_eq!(addresses, vec!["existing"]); + } +} diff --git a/crates/stonks/src/api/request.rs b/crates/stonks/src/api/request.rs new file mode 100644 index 0000000..1d18a08 --- /dev/null +++ b/crates/stonks/src/api/request.rs @@ -0,0 +1,42 @@ +//! Raw Stonks request variants and query parameters. +//! +//! Names and fields intentionally mirror the upstream wire 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 { + /// Zero-based page number. + pub page: u32, + /// Maximum number of addresses requested for the page. + pub size: u32, +} + +impl VirtualPoolAddressesParams { + /// 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 { + 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..0e1e076 --- /dev/null +++ b/crates/stonks/src/api/response.rs @@ -0,0 +1,37 @@ +//! Raw Stonks response variants. +//! +//! Names and fields intentionally mirror the upstream wire 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) => { + 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 + ))), + } + }; +} + +/// 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), + /// All raw TON addresses discovered across every Virtual Pool page. + AllVirtualPoolAddresses(Vec), +} diff --git a/crates/stonks/src/api/types.rs b/crates/stonks/src/api/types.rs new file mode 100644 index 0000000..d702078 --- /dev/null +++ b/crates/stonks/src/api/types.rs @@ -0,0 +1,22 @@ +//! Raw Stonks response types. +//! +//! Names and fields intentionally mirror the upstream response schema. + +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/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..9af3932 --- /dev/null +++ b/crates/stonks/tests/test_api.rs @@ -0,0 +1,40 @@ +use anyhow::{Context, Result}; +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_public_tokens() -> Result<()> { + let client = init_client()?; + 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!(!public_token.symbol.is_empty()); + assert!(!public_token.address.is_empty()); + Ok(()) +} + +#[tokio::test] +async fn test_virtual_pool_addresses_page() -> Result<()> { + let client = init_client()?; + 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!(addresses.len() <= 1); + assert!(!address.is_empty()); + Ok(()) +} + +#[tokio::test] +async fn test_all_virtual_pool_addresses() -> Result<()> { + let client = init_client()?; + let addresses = + unwrap_response!(AllVirtualPoolAddresses, client.api.exec(Request::AllVirtualPoolAddresses).await?)?; + + 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 new file mode 100644 index 0000000..67bfbf7 --- /dev/null +++ b/crates/stonks/tests/test_construction.rs @@ -0,0 +1,53 @@ +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 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((public_tokens, virtual_pool_addresses, all_virtual_pool_addresses)); + Ok(()) +} + +#[test] +fn test_virtual_pool_addresses_params_serialize_to_upstream_query_names() -> anyhow::Result<()> { + let params = VirtualPoolAddressesParams::new(2_u32, 50_u32); + + assert_eq!(serde_qs::to_string(¶ms)?, "page=2&size=50"); + Ok(()) +} + +#[test] +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!(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 public_token = PublicToken::default() + .with_symbol("TOKEN".to_string()) + .with_address("EQAddress".to_string()) + .with_buy_tax(3) + .with_sell_tax(7); + let params = VirtualPoolAddressesParams::default().with_page(2).with_size(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)); +}