From f3c99bfcd913755a8c71602d3afab718f8a0d1a5 Mon Sep 17 00:00:00 2001 From: Sild Date: Fri, 31 Jul 2026 18:03:50 +0200 Subject: [PATCH 1/2] up gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 318b748..b561f11 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ target/ .idea/ .vscode .worktrees +.DS_Store From 2a421fe2b71b1b5279ff2b5a9f4f083a4591b252 Mon Sep 17 00:00:00 2001 From: Sild Date: Fri, 31 Jul 2026 18:13:15 +0200 Subject: [PATCH 2/2] Implement #NI: Add DeDust asset and v4 clients --- AGENTS.md | 2 +- Cargo.lock | 1 + README.md | 2 +- crates/dedust/AGENTS.md | 67 ++++++++++----- crates/dedust/Cargo.toml | 1 + crates/dedust/README.md | 94 +++++++++++++++------ crates/dedust/src/api_client.rs | 16 +++- crates/dedust/src/api_client/builder.rs | 26 +++++- crates/dedust/src/assets.rs | 35 ++++++++ crates/dedust/src/assets/request.rs | 16 ++++ crates/dedust/src/assets/response.rs | 31 +++++++ crates/dedust/src/assets/types.rs | 93 ++++++++++++++++++++ crates/dedust/src/lib.rs | 4 + crates/dedust/src/v4.rs | 44 ++++++++++ crates/dedust/src/v4/request.rs | 25 ++++++ crates/dedust/src/v4/response.rs | 37 ++++++++ crates/dedust/src/v4/types.rs | 54 ++++++++++++ crates/dedust/tests/test_api_v4.rs | 49 +++++++++++ crates/dedust/tests/test_assets.rs | 22 +++++ crates/dedust/tests/test_assets_wire.rs | 107 ++++++++++++++++++++++++ 20 files changed, 669 insertions(+), 57 deletions(-) create mode 100644 crates/dedust/src/assets.rs create mode 100644 crates/dedust/src/assets/request.rs create mode 100644 crates/dedust/src/assets/response.rs create mode 100644 crates/dedust/src/assets/types.rs create mode 100644 crates/dedust/src/v4.rs create mode 100644 crates/dedust/src/v4/request.rs create mode 100644 crates/dedust/src/v4/response.rs create mode 100644 crates/dedust/src/v4/types.rs create mode 100644 crates/dedust/tests/test_api_v4.rs create mode 100644 crates/dedust/tests/test_assets.rs create mode 100644 crates/dedust/tests/test_assets_wire.rs diff --git a/AGENTS.md b/AGENTS.md index c5586c8..2ed22e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ Root guidance applies to the whole workspace. Also read the crate-local 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. +- `dedust_api_client` (`crates/dedust/`): REST wrapper for the DeDust asset registry, API v4 pool registries, and legacy API v2 operations. - `swap_coffee_api_client` (`crates/swap_coffee/`): REST wrapper for Swap Coffee API v1. - `tonco_api_client` (`crates/tonco_api_client/`): GraphQL wrapper for Tonco diff --git a/Cargo.lock b/Cargo.lock index ccc17c5..5921897 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -383,6 +383,7 @@ dependencies = [ "log", "serde", "serde_derive", + "serde_json", "tokio", ] diff --git a/README.md b/README.md index 571d2cd..a23acb7 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ MRs are welcome. |-----------------------|----------------------------------------------------------|-------------|--------------| | 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://dedust.io | [dedust_api_client](crates/dedust) | Supported | DeDust asset metadata, v4 Classic/Stable/CPMM v2/Uranus pool registries, and legacy v2 operations. | | 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. | | Bidask | [bidask_api_client](crates/bidask) | Unsupported | Legacy source only; not recommended for application integration and not published. | diff --git a/crates/dedust/AGENTS.md b/crates/dedust/AGENTS.md index fa1db3c..0748019 100644 --- a/crates/dedust/AGENTS.md +++ b/crates/dedust/AGENTS.md @@ -2,8 +2,8 @@ ## Scope -This crate is `dedust_api_client`, a Rust library crate that wraps DeDust REST -API v2. +This crate is `dedust_api_client`, a public Rust library crate that wraps the +DeDust asset registry, API v4 pool registries, and legacy REST API v2. Use the repository root `AGENTS.md` first, then this file. Use the `rust-library-review` skill for public API, docs, package, or agent-guidance @@ -14,29 +14,40 @@ changes. The crate exposes a thin typed client: - `DedustApiClient::builder().build()?` +- `client.assets.exec(&AssetsRequest::...)` - `client.v2.exec(&V2Request::...)` -- request params in `v2/request.rs` -- response enums and models in `v2/response.rs` and `v2/types.rs` +- `client.v4.exec(&V4Request::...)` +- raw request, response, and model modules under `assets/`, `v2/`, and `v4/` Keep DeDust-specific address formatting and endpoint mapping in this crate. +Do not join asset metadata into pool registries or derive dynamic pool state. ## Public API Boundary Treat these as public contracts: - `DedustApiClient` +- `DEFAULT_ASSETS_URL` - `DEFAULT_API_V2_URL` +- `DEFAULT_API_V4_URL` +- `AssetsApiClient`, `AssetsRequest`, `AssetsResponse`, and asset wire models - `V2ApiClient` - `V2Request` - `RoutingPlanParams` - `V2Response` and public response/type structs +- `V4ApiClient`, `V4Request`, `V4Response`, and pool-registry wire models - `unwrap_response!` +- `unwrap_assets_response!` +- `unwrap_v4_response!` Request parameter and response/model POD structs are `#[non_exhaustive]`; use `Default::default().with_(...)` or request parameter constructors instead -of struct literals in downstream examples and integration tests. Pass request -parameters directly to `client.v2.exec` where `Into` is implemented. -Public enums are `#[non_exhaustive]`; downstream matches need wildcard arms. +of struct literals in downstream examples and integration tests. Public enums +are `#[non_exhaustive]`; downstream matches need wildcard arms. + +The existing `with_api_url` and `with_executor` builder setters configure v2. +Use `with_assets_url`/`with_assets_executor` and +`with_v4_url`/`with_v4_executor` for the other origins. `RoutingPlanParams::new` maps the zero TON address to `native` and all other addresses to `jetton:
`. Do not change that mapping without validating @@ -44,35 +55,45 @@ DeDust API expectations and updating examples. ## Live API Notes -Tests in `tests/test_api_v2.rs` hit the live DeDust API. Prefer assertions that -prove endpoint support and response parsing without relying on volatile pool -counts, routing amounts, or ordering. +Tests in `tests/test_assets.rs`, `tests/test_api_v2.rs`, and +`tests/test_api_v4.rs` hit the live DeDust services. Prefer assertions that +prove endpoint support and response parsing without relying on volatile asset +or pool counts, routing amounts, or ordering. + +The asset registry uses friendly TON addresses and absolute image URLs. The v4 +pool registries use raw `workchain:hex_hash` addresses and asset identifiers +such as `native` and `jetton:0:`. They provide discovery/configuration +records, not v2 dynamic fields such as reserves, supply, price, volume, or fees. +Keep legacy v2 asset and pool operations available for backward compatibility, +but do not recommend them for new registry integrations. ## Downstream Integration Example ```rust use dedust_api_client::api_client::DedustApiClient; -use dedust_api_client::v2::{RoutingPlanParams, V2Request, V2Response}; +use dedust_api_client::assets::{AssetsRequest, AssetsResponse}; +use dedust_api_client::v4::{V4Request, V4Response}; # async fn example() -> anyhow::Result<()> { let client = DedustApiClient::builder().build()?; -let params = RoutingPlanParams::new( - "0:0000000000000000000000000000000000000000000000000000000000000000", - "0:0000000000000000000000000000000000000000000000000000000000000000", - "1000000000", -); -let response = client.v2.exec(params).await?; - -match response { - V2Response::RoutingPlan(routes) => println!("routes: {}", routes.len()), - other => anyhow::bail!("unexpected DeDust response: {other:?}"), +let assets_response = client.assets.exec(AssetsRequest::List).await?; +let pools_response = client.v4.exec(V4Request::AllCpmmPools).await?; + +match assets_response { + AssetsResponse::List(assets) => println!("assets: {}", assets.len()), + other => anyhow::bail!("unexpected DeDust asset response: {other:?}"), +} +match pools_response { + V4Response::AllCpmmPools(pools) => println!("CPMM v2 pools: {}", pools.len()), + other => anyhow::bail!("unexpected DeDust v4 response: {other:?}"), } # Ok(()) # } ``` -Final applications should keep address, amount, and slippage interpretation in -their own domain layer. +Final applications should keep address conversion, pool hydration, amount and +slippage interpretation, persistence, and fallback behavior in their own +domain layer. ## Validation diff --git a/crates/dedust/Cargo.toml b/crates/dedust/Cargo.toml index eb096f3..7a80108 100644 --- a/crates/dedust/Cargo.toml +++ b/crates/dedust/Cargo.toml @@ -22,3 +22,4 @@ derive_more.workspace = true tokio.workspace = true env_logger.workspace = true anyhow.workspace = true +serde_json.workspace = true diff --git a/crates/dedust/README.md b/crates/dedust/README.md index 373e205..9bee301 100644 --- a/crates/dedust/README.md +++ b/crates/dedust/README.md @@ -1,10 +1,13 @@ # dedust_api_client -Thin typed wrapper for the [DeDust API v2](https://api.dedust.io/). +Thin typed wrapper for the [DeDust](https://dedust.io/) asset registry, API v4 +pool registries, and legacy API v2. -Use this crate when an application needs typed access to DeDust assets, pools, -pool trades, or routing plans. The crate does not choose routes, calculate -slippage, execute swaps, or normalize DeDust data into a shared DEX domain model. +Use this crate when an application needs raw typed access to DeDust asset +metadata, pool discovery/configuration, pool trades, or routing plans. The crate +does not join asset metadata into pools, load on-chain pool state, choose routes, +calculate slippage, execute swaps, or normalize DeDust data into a shared DEX +domain model. ## Usage @@ -16,35 +19,65 @@ 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 request parameter structs -directly where `Into` is implemented, and match response enums with a -wildcard arm. +Run requests inside an async Tokio runtime. Match response enums with a wildcard +arm because they are non-exhaustive. ```rust,no_run use dedust_api_client::api_client::DedustApiClient; -use dedust_api_client::v2::{RoutingPlanParams, V2Response}; +use dedust_api_client::assets::{AssetsRequest, AssetsResponse}; +use dedust_api_client::v4::{V4Request, V4Response}; # async fn example() -> Result<(), Box> { let client = DedustApiClient::builder().build()?; -let params = RoutingPlanParams::new( - "0:0000000000000000000000000000000000000000000000000000000000000000", - "0:0000000000000000000000000000000000000000000000000000000000000000", - "1000000000", -); -let response = client.v2.exec(params).await?; - -match response { - V2Response::RoutingPlan(routes) => println!("route groups: {}", routes.len()), - _ => println!("unexpected DeDust response variant"), + +let assets_response = client.assets.exec(AssetsRequest::List).await?; +match assets_response { + AssetsResponse::List(assets) => println!("assets: {}", assets.len()), + _ => println!("unexpected DeDust asset response variant"), +} + +let pools_response = client.v4.exec(V4Request::AllCpmmPools).await?; +match pools_response { + V4Response::AllCpmmPools(pools) => println!("CPMM v2 pools: {}", pools.len()), + _ => println!("unexpected DeDust v4 response variant"), } # Ok(()) # } ``` -`RoutingPlanParams::new` maps the zero TON address to `native` and all other -addresses to `jetton:
`. +The asset registry returns friendly TON addresses and absolute image URLs. The +v4 pool registries return raw `workchain:hex_hash` addresses and asset strings +such as `native` and `jetton:0:`. Applications own any address conversion, +metadata joins, and dynamic pool-state hydration. + +## Asset Registry + +| Method | Endpoint | Request | Response | +|--------|------------|-----------------------|-----------------------| +| GET | /list.json | `AssetsRequest::List` | `AssetsResponse::List` | + +The default asset-registry origin is `https://assets.dedust.io`. -## Supported Endpoints +## API v4 Pool Registries + +| Method | Endpoint | Request | Response | +|--------|---------------------------|------------------------------------|------------------------------------| +| GET | /get_pools_allclassic | `V4Request::AllClassicPools` | `V4Response::AllClassicPools` | +| GET | /get_pools_allstable | `V4Request::AllStablePools` | `V4Response::AllStablePools` | +| GET | /get_pools_allcpmm | `V4Request::AllCpmmPools` | `V4Response::AllCpmmPools` | +| GET | /get_pools_alluranus | `V4Request::AllUranusPools` | `V4Response::AllUranusPools` | + +The default v4 origin is `https://mainnet.api.dedust.io/v4/api`. These endpoints +return discovery and configuration records. They do not include v2 fields such +as reserves, liquidity-token supply, logical time, last price, volume, or +accumulated fees. Uranus records identify launchpad tokens and fundraising +configuration rather than ordinary two-asset pools. + +## Legacy API v2 + +The following operations remain available through `client.v2` for backward +compatibility. The v2 asset and pool-list endpoints are considered legacy and +should not be selected for new registry integrations. | Method | Supported | |------------------------------------------|-----------| @@ -69,11 +102,18 @@ addresses to `jetton:
`. | /v2/prices | | | /v2/routing/plan | ✅ | -Public request and response types are marked `#[non_exhaustive]` for semver +`RoutingPlanParams::new` maps the zero TON address to `native` and all other +addresses to `jetton:
`. + +Public request and response types are marked `#[non_exhaustive]` for SemVer headroom. Build public POD structs with `Default::default().with_(...)` -or request parameter constructors, pass request parameters directly to -`client.v2.exec` where `Into` is implemented, and include a wildcard -arm when matching response enums. +or request parameter constructors, pass request parameters directly where +`Into` is implemented, and include a wildcard arm when matching public +enums. + +The existing `with_api_url` and `with_executor` builder setters configure v2. +Use `with_assets_url`/`with_assets_executor` and +`with_v4_url`/`with_v4_executor` to override the other origins independently. -Live API tests hit DeDust directly. Pool counts, routing amounts, and ordering -can drift with upstream state. +Live API tests hit DeDust directly. Asset and pool counts, metadata, routing +amounts, and ordering can drift with upstream state. diff --git a/crates/dedust/src/api_client.rs b/crates/dedust/src/api_client.rs index 81ef0fb..a378910 100644 --- a/crates/dedust/src/api_client.rs +++ b/crates/dedust/src/api_client.rs @@ -1,20 +1,32 @@ mod builder; use crate::api_client::builder::Builder; +use crate::assets::AssetsApiClient; use crate::v2::V2ApiClient; +use crate::v4::V4ApiClient; + +/// Default base URL for the DeDust asset registry. +pub const DEFAULT_ASSETS_URL: &str = "https://assets.dedust.io"; /// Default base URL for DeDust API v2. pub const DEFAULT_API_V2_URL: &str = "https://api.dedust.io/v2"; -/// DeDust service client with a child client for API v2. +/// Default base URL for the DeDust API v4 pool registry. +pub const DEFAULT_API_V4_URL: &str = "https://mainnet.api.dedust.io/v4/api"; + +/// DeDust service client with child clients for each upstream API surface. #[derive(Clone)] #[non_exhaustive] pub struct DedustApiClient { + /// DeDust asset-registry execution client. + pub assets: AssetsApiClient, /// DeDust API v2 execution client. pub v2: V2ApiClient, + /// DeDust API v4 pool-registry execution client. + pub v4: V4ApiClient, } impl DedustApiClient { - /// Start configuring a DeDust client with the default v2 endpoint. + /// Start configuring a DeDust client with the default asset, v2, and v4 endpoints. pub fn builder() -> Builder { Builder::new() } } diff --git a/crates/dedust/src/api_client/builder.rs b/crates/dedust/src/api_client/builder.rs index 81ae29d..76afc60 100644 --- a/crates/dedust/src/api_client/builder.rs +++ b/crates/dedust/src/api_client/builder.rs @@ -1,5 +1,7 @@ -use crate::api_client::{DedustApiClient, DEFAULT_API_V2_URL}; +use crate::api_client::{DedustApiClient, DEFAULT_API_V2_URL, DEFAULT_API_V4_URL, DEFAULT_ASSETS_URL}; +use crate::assets::AssetsApiClient; use crate::v2::V2ApiClient; +use crate::v4::V4ApiClient; use api_clients_core::{ApiClientsResult, Executor}; use derive_setters::Setters; use std::sync::Arc; @@ -10,14 +12,22 @@ use std::sync::Arc; #[non_exhaustive] pub struct Builder { api_url: String, + assets_url: String, + v4_url: String, executor: Option>, + assets_executor: Option>, + v4_executor: Option>, } impl Builder { pub(super) fn new() -> Self { Self { api_url: DEFAULT_API_V2_URL.to_string(), + assets_url: DEFAULT_ASSETS_URL.to_string(), + v4_url: DEFAULT_API_V4_URL.to_string(), executor: None, + assets_executor: None, + v4_executor: None, } } @@ -25,14 +35,24 @@ impl Builder { /// /// # Errors /// - /// Returns an error if the shared executor cannot be constructed. + /// Returns an error if any 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 assets_executor = match self.assets_executor { + Some(executor) => executor, + None => Executor::builder(self.assets_url).build()?.into(), + }; + let v4_executor = match self.v4_executor { + Some(executor) => executor, + None => Executor::builder(self.v4_url).build()?.into(), + }; + let assets = AssetsApiClient::new(assets_executor); let v2 = V2ApiClient::new(executor); - Ok(DedustApiClient { v2 }) + let v4 = V4ApiClient::new(v4_executor); + Ok(DedustApiClient { assets, v2, v4 }) } } diff --git a/crates/dedust/src/assets.rs b/crates/dedust/src/assets.rs new file mode 100644 index 0000000..3461baf --- /dev/null +++ b/crates/dedust/src/assets.rs @@ -0,0 +1,35 @@ +mod request; +mod response; +mod types; + +use api_clients_core::{ApiClientsResult, Executor}; +use std::sync::Arc; + +pub use request::*; +pub use response::*; +pub use types::*; + +/// Executes typed requests against the DeDust asset registry. +#[derive(Clone)] +pub struct AssetsApiClient { + executor: Arc, +} + +impl AssetsApiClient { + pub(crate) fn new(executor: Arc) -> Self { Self { executor } } + + /// Execute a DeDust asset-registry request and return its matching response variant. + /// + /// # Errors + /// + /// Returns an error when transport, status handling, or response deserialization fails. + pub async fn exec(&self, request: REQUEST) -> ApiClientsResult + where + REQUEST: Into, + { + let response = match request.into() { + AssetsRequest::List => AssetsResponse::List(self.executor.exec_get("list.json").await?), + }; + Ok(response) + } +} diff --git a/crates/dedust/src/assets/request.rs b/crates/dedust/src/assets/request.rs new file mode 100644 index 0000000..d0f62f6 --- /dev/null +++ b/crates/dedust/src/assets/request.rs @@ -0,0 +1,16 @@ +//! Raw DeDust asset-registry request variants. + +use derive_more::From; + +/// A request supported by the DeDust asset-registry client. +#[derive(Clone, From)] +#[non_exhaustive] +pub enum AssetsRequest { + /// Load the complete DeDust asset registry. + #[from(skip)] + List, +} + +impl From<&AssetsRequest> for AssetsRequest { + fn from(request: &AssetsRequest) -> Self { request.clone() } +} diff --git a/crates/dedust/src/assets/response.rs b/crates/dedust/src/assets/response.rs new file mode 100644 index 0000000..be05421 --- /dev/null +++ b/crates/dedust/src/assets/response.rs @@ -0,0 +1,31 @@ +//! Raw DeDust asset-registry response variants. + +use crate::assets::types::Asset; +use serde_derive::Deserialize; + +/// Extract the expected payload from a DeDust [`AssetsResponse`](crate::assets::AssetsResponse). +/// +/// 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_assets_response { + ($variant:ident, $result:expr) => { + match $result { + $crate::assets::AssetsResponse::$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 DeDust asset-registry client. +#[derive(Deserialize, Debug, Clone)] +#[non_exhaustive] +pub enum AssetsResponse { + /// Complete DeDust asset metadata registry. + List(Vec), +} diff --git a/crates/dedust/src/assets/types.rs b/crates/dedust/src/assets/types.rs new file mode 100644 index 0000000..f43b5fe --- /dev/null +++ b/crates/dedust/src/assets/types.rs @@ -0,0 +1,93 @@ +//! Raw DeDust asset-registry wire types. + +use derive_setters::Setters; +use serde_derive::{Deserialize, Serialize}; + +/// Raw metadata for an asset listed by DeDust. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, Setters)] +#[setters(prefix = "with_", strip_option)] +#[non_exhaustive] +pub struct Asset { + /// Upstream asset kind, such as `native` or `jetton`. + #[serde(rename = "type")] + pub asset_type: String, + /// Friendly TON jetton-master address; absent for the native asset. + pub address: Option, + /// Display name supplied by DeDust. + pub name: String, + /// Display symbol supplied by DeDust. + pub symbol: String, + /// Absolute asset-image URL. + pub image: String, + /// Number of decimal places used by the asset. + pub decimals: i64, + /// Raw alias flag supplied by DeDust. + pub aliased: Option, + /// Raw buy-tax value supplied by DeDust. + pub buy_tax: Option, + /// Raw sell-tax value supplied by DeDust. + pub sell_tax: Option, + /// Optional upstream description value, currently used for media URLs. + pub description: Option, + /// Optional friendly TON address used for DeDust discovery metadata. + pub discovery: Option, + /// Whether the asset originated from the legacy TON bridge. + #[serde(rename = "legacyBridge")] + pub legacy_bridge: Option, + /// Optional source-chain and bridge metadata. + pub source: Option, + /// Optional content shown on the DeDust token page. + pub token_page: Option, +} + +/// Raw source-chain metadata for a bridged or externally sourced asset. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, Setters)] +#[setters(prefix = "with_", strip_option)] +#[non_exhaustive] +pub struct AssetSource { + /// CAIP-style source-chain identifier. + pub chain: String, + /// Source-chain token address, when supplied. + pub address: String, + /// Bridge identifier, when supplied. + pub bridge: String, + /// Source-chain token symbol. + pub symbol: String, + /// Source-chain token name. + pub name: String, +} + +/// Raw DeDust token-page content. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, Setters)] +#[setters(prefix = "with_", strip_option)] +#[non_exhaustive] +pub struct TokenPage { + /// Responsive token-page banner images. + pub banners: TokenPageBanners, + /// Token-page descriptive content. + pub description: TokenPageDescription, +} + +/// Raw token-page banner URLs. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, Setters)] +#[setters(prefix = "with_", strip_option)] +#[non_exhaustive] +pub struct TokenPageBanners { + /// Small-layout banner URL. + pub small: String, + /// Large-layout banner URL. + pub large: String, +} + +/// Raw token-page description fields. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, Setters)] +#[setters(prefix = "with_", strip_option)] +#[non_exhaustive] +pub struct TokenPageDescription { + /// Description heading. + pub title: String, + /// Description body. + pub text: String, + /// Optional description banner URL. + pub banner: Option, +} diff --git a/crates/dedust/src/lib.rs b/crates/dedust/src/lib.rs index 7557433..683cd9f 100644 --- a/crates/dedust/src/lib.rs +++ b/crates/dedust/src/lib.rs @@ -5,5 +5,9 @@ pub use api_clients_core; // re-export /// Top-level DeDust client and builder. pub mod api_client; +/// DeDust asset-registry requests, responses, and wire models. +pub mod assets; /// DeDust API v2 requests, responses, and wire models. pub mod v2; +/// DeDust API v4 pool-registry requests, responses, and wire models. +pub mod v4; diff --git a/crates/dedust/src/v4.rs b/crates/dedust/src/v4.rs new file mode 100644 index 0000000..af50779 --- /dev/null +++ b/crates/dedust/src/v4.rs @@ -0,0 +1,44 @@ +mod request; +mod response; +mod types; + +use api_clients_core::{ApiClientsResult, Executor}; +use std::sync::Arc; + +pub use request::*; +pub use response::*; +pub use types::*; + +/// Executes typed requests against the DeDust API v4 pool registry. +#[derive(Clone)] +pub struct V4ApiClient { + executor: Arc, +} + +impl V4ApiClient { + pub(crate) fn new(executor: Arc) -> Self { Self { executor } } + + /// Execute a DeDust v4 pool-registry request and return its matching response variant. + /// + /// # Errors + /// + /// Returns an error when transport, status handling, or response deserialization fails. + pub async fn exec(&self, request: REQUEST) -> ApiClientsResult + where + REQUEST: Into, + { + let response = match request.into() { + V4Request::AllClassicPools => { + V4Response::AllClassicPools(self.executor.exec_get("get_pools_allclassic").await?) + } + V4Request::AllStablePools => { + V4Response::AllStablePools(self.executor.exec_get("get_pools_allstable").await?) + } + V4Request::AllCpmmPools => V4Response::AllCpmmPools(self.executor.exec_get("get_pools_allcpmm").await?), + V4Request::AllUranusPools => { + V4Response::AllUranusPools(self.executor.exec_get("get_pools_alluranus").await?) + } + }; + Ok(response) + } +} diff --git a/crates/dedust/src/v4/request.rs b/crates/dedust/src/v4/request.rs new file mode 100644 index 0000000..e4e6a1b --- /dev/null +++ b/crates/dedust/src/v4/request.rs @@ -0,0 +1,25 @@ +//! Raw DeDust API v4 pool-registry request variants. + +use derive_more::From; + +/// A request supported by the DeDust API v4 pool-registry client. +#[derive(Clone, From)] +#[non_exhaustive] +pub enum V4Request { + /// Load every Classic (CPMM v1) pool descriptor. + #[from(skip)] + AllClassicPools, + /// Load every Stable-swap pool descriptor. + #[from(skip)] + AllStablePools, + /// Load every CPMM v2 pool descriptor and its fee configuration. + #[from(skip)] + AllCpmmPools, + /// Load every Uranus launchpad pool descriptor. + #[from(skip)] + AllUranusPools, +} + +impl From<&V4Request> for V4Request { + fn from(request: &V4Request) -> Self { request.clone() } +} diff --git a/crates/dedust/src/v4/response.rs b/crates/dedust/src/v4/response.rs new file mode 100644 index 0000000..1774f0c --- /dev/null +++ b/crates/dedust/src/v4/response.rs @@ -0,0 +1,37 @@ +//! Raw DeDust API v4 pool-registry response variants. + +use crate::v4::types::{ClassicPool, CpmmPool, StablePool, UranusPool}; +use serde_derive::Deserialize; + +/// Extract the expected payload from a DeDust [`V4Response`](crate::v4::V4Response). +/// +/// 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_v4_response { + ($variant:ident, $result:expr) => { + match $result { + $crate::v4::V4Response::$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 DeDust API v4 pool-registry client. +#[derive(Deserialize, Debug, Clone)] +#[non_exhaustive] +pub enum V4Response { + /// Complete Classic (CPMM v1) pool registry. + AllClassicPools(Vec), + /// Complete Stable-swap pool registry. + AllStablePools(Vec), + /// Complete CPMM v2 pool registry. + AllCpmmPools(Vec), + /// Complete Uranus launchpad pool registry. + AllUranusPools(Vec), +} diff --git a/crates/dedust/src/v4/types.rs b/crates/dedust/src/v4/types.rs new file mode 100644 index 0000000..eed8135 --- /dev/null +++ b/crates/dedust/src/v4/types.rs @@ -0,0 +1,54 @@ +//! Raw DeDust API v4 pool-registry wire types. + +use derive_setters::Setters; +use serde_derive::{Deserialize, Serialize}; + +/// Raw Classic (CPMM v1) pool descriptor. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, Setters)] +#[setters(prefix = "with_", strip_option)] +#[non_exhaustive] +pub struct ClassicPool { + /// Ordered raw asset identifiers, such as `native` or `jetton:0:`. + pub assets: Vec, + /// Raw `workchain:hex_hash` pool address. + pub pool_address: String, +} + +/// Raw Stable-swap pool descriptor. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, Setters)] +#[setters(prefix = "with_", strip_option)] +#[non_exhaustive] +pub struct StablePool { + /// Ordered raw asset identifiers, such as `native` or `jetton:0:`. + pub assets: Vec, + /// Raw `workchain:hex_hash` pool address. + pub pool_address: String, +} + +/// Raw CPMM v2 pool descriptor and fee configuration. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, Setters)] +#[setters(prefix = "with_", strip_option)] +#[non_exhaustive] +pub struct CpmmPool { + /// Ordered raw asset identifiers, such as `native` or `jetton:0:`. + pub assets: Vec, + /// Base trading fee in basis points. + pub base_fee_bps: u16, + /// Raw upstream selector identifying which side collects fees. + pub fee_in: u8, + /// Raw `workchain:hex_hash` pool address. + pub pool_address: String, +} + +/// Raw Uranus launchpad pool descriptor. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default, Setters)] +#[setters(prefix = "with_", strip_option)] +#[non_exhaustive] +pub struct UranusPool { + /// Base trading fee in basis points. + pub base_fee_bps: u16, + /// Raw integer fundraising target represented as a decimal string. + pub raising_funds: String, + /// Raw `workchain:hex_hash` token address. + pub token_address: String, +} diff --git a/crates/dedust/tests/test_api_v4.rs b/crates/dedust/tests/test_api_v4.rs new file mode 100644 index 0000000..56e6855 --- /dev/null +++ b/crates/dedust/tests/test_api_v4.rs @@ -0,0 +1,49 @@ +use anyhow::Result; +use api_clients_core::Executor; +use dedust_api_client::api_client::{DedustApiClient, DEFAULT_API_V4_URL}; +use dedust_api_client::unwrap_v4_response; +use dedust_api_client::v4::V4Request; +use std::sync::Arc; +use std::time::Duration; + +fn init_env() -> Result { + let _ = env_logger::builder().filter_level(log::LevelFilter::Debug).try_init(); + let executor = Executor::builder(DEFAULT_API_V4_URL).with_timeout(Duration::from_secs(60)).build()?; + Ok(DedustApiClient::builder().with_v4_executor(Arc::new(executor)).build()?) +} + +#[tokio::test] +async fn test_all_classic_pools() -> Result<()> { + let client = init_env()?; + let response = unwrap_v4_response!(AllClassicPools, client.v4.exec(V4Request::AllClassicPools).await?)?; + + assert!(!response.is_empty()); + Ok(()) +} + +#[tokio::test] +async fn test_all_stable_pools() -> Result<()> { + let client = init_env()?; + let response = unwrap_v4_response!(AllStablePools, client.v4.exec(V4Request::AllStablePools).await?)?; + + assert!(!response.is_empty()); + Ok(()) +} + +#[tokio::test] +async fn test_all_cpmm_pools() -> Result<()> { + let client = init_env()?; + let response = unwrap_v4_response!(AllCpmmPools, client.v4.exec(V4Request::AllCpmmPools).await?)?; + + assert!(!response.is_empty()); + Ok(()) +} + +#[tokio::test] +async fn test_all_uranus_pools() -> Result<()> { + let client = init_env()?; + let response = unwrap_v4_response!(AllUranusPools, client.v4.exec(V4Request::AllUranusPools).await?)?; + + assert!(!response.is_empty()); + Ok(()) +} diff --git a/crates/dedust/tests/test_assets.rs b/crates/dedust/tests/test_assets.rs new file mode 100644 index 0000000..13994c3 --- /dev/null +++ b/crates/dedust/tests/test_assets.rs @@ -0,0 +1,22 @@ +use anyhow::Result; +use api_clients_core::Executor; +use dedust_api_client::api_client::{DedustApiClient, DEFAULT_ASSETS_URL}; +use dedust_api_client::assets::AssetsRequest; +use dedust_api_client::unwrap_assets_response; +use std::sync::Arc; +use std::time::Duration; + +fn init_env() -> Result { + let _ = env_logger::builder().filter_level(log::LevelFilter::Debug).try_init(); + let executor = Executor::builder(DEFAULT_ASSETS_URL).with_timeout(Duration::from_secs(60)).build()?; + Ok(DedustApiClient::builder().with_assets_executor(Arc::new(executor)).build()?) +} + +#[tokio::test] +async fn test_asset_list() -> Result<()> { + let client = init_env()?; + let response = unwrap_assets_response!(List, client.assets.exec(AssetsRequest::List).await?)?; + + assert!(!response.is_empty()); + Ok(()) +} diff --git a/crates/dedust/tests/test_assets_wire.rs b/crates/dedust/tests/test_assets_wire.rs new file mode 100644 index 0000000..427a9c8 --- /dev/null +++ b/crates/dedust/tests/test_assets_wire.rs @@ -0,0 +1,107 @@ +use anyhow::{bail, Result}; +use dedust_api_client::assets::Asset; + +#[test] +fn test_asset_deserializes_complete_observed_wire_shape() -> Result<()> { + let asset: Asset = serde_json::from_str( + r#"{ + "type": "jetton", + "address": "EQAsset", + "name": "Example", + "symbol": "EX", + "image": "https://assets.dedust.io/images/example.webp", + "decimals": 9, + "aliased": true, + "buy_tax": 300, + "sell_tax": 500, + "description": "https://example.com/description.mp3", + "discovery": "EQDiscovery", + "legacyBridge": true, + "source": { + "chain": "eip155:1", + "address": "0x1234", + "bridge": "ton-bridge", + "symbol": "EX", + "name": "Example Source" + }, + "token_page": { + "banners": { + "small": "https://assets.dedust.io/images/example-small.webp", + "large": "https://assets.dedust.io/images/example-large.webp" + }, + "description": { + "title": "Example title", + "text": "Example text", + "banner": "https://assets.dedust.io/images/example-description.webp" + } + } + }"#, + )?; + + assert_eq!(asset.asset_type, "jetton"); + assert_eq!(asset.address.as_deref(), Some("EQAsset")); + assert_eq!(asset.name, "Example"); + assert_eq!(asset.symbol, "EX"); + assert_eq!(asset.image, "https://assets.dedust.io/images/example.webp"); + assert_eq!(asset.decimals, 9); + assert_eq!(asset.aliased, Some(true)); + assert_eq!(asset.buy_tax, Some(300)); + assert_eq!(asset.sell_tax, Some(500)); + assert_eq!(asset.description.as_deref(), Some("https://example.com/description.mp3")); + assert_eq!(asset.discovery.as_deref(), Some("EQDiscovery")); + assert_eq!(asset.legacy_bridge, Some(true)); + + let Some(source) = asset.source else { + bail!("expected source metadata"); + }; + assert_eq!(source.chain, "eip155:1"); + assert_eq!(source.address, "0x1234"); + assert_eq!(source.bridge, "ton-bridge"); + assert_eq!(source.symbol, "EX"); + assert_eq!(source.name, "Example Source"); + + let Some(token_page) = asset.token_page else { + bail!("expected token-page metadata"); + }; + assert_eq!(token_page.banners.small, "https://assets.dedust.io/images/example-small.webp"); + assert_eq!(token_page.banners.large, "https://assets.dedust.io/images/example-large.webp"); + assert_eq!(token_page.description.title, "Example title"); + assert_eq!(token_page.description.text, "Example text"); + assert_eq!( + token_page.description.banner.as_deref(), + Some("https://assets.dedust.io/images/example-description.webp") + ); + + let sparse_asset: Asset = serde_json::from_str( + r#"{ + "type": "jetton", + "address": "EQSparse", + "name": "Sparse Example", + "symbol": "SPARSE", + "image": "https://assets.dedust.io/images/sparse.webp", + "decimals": 9, + "token_page": { + "banners": { + "small": "https://assets.dedust.io/images/sparse-small.webp", + "large": "https://assets.dedust.io/images/sparse-large.webp" + }, + "description": { + "title": "Sparse title", + "text": "Sparse text" + } + } + }"#, + )?; + assert_eq!(sparse_asset.aliased, None); + assert_eq!(sparse_asset.buy_tax, None); + assert_eq!(sparse_asset.sell_tax, None); + assert_eq!(sparse_asset.description, None); + assert_eq!(sparse_asset.discovery, None); + assert_eq!(sparse_asset.legacy_bridge, None); + assert_eq!(sparse_asset.source, None); + let Some(sparse_token_page) = sparse_asset.token_page else { + bail!("expected sparse token-page metadata"); + }; + assert_eq!(sparse_token_page.description.banner, None); + Ok(()) +}