Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"crates/core",
"crates/dedust",
"crates/stonfi",
"crates/stonks",
"crates/swap_coffee",
"crates/tonco_api_client",
"crates/bidask",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
98 changes: 98 additions & 0 deletions crates/stonks/AGENTS.md
Original file line number Diff line number Diff line change
@@ -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_<field>(...)` 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.
8 changes: 8 additions & 0 deletions crates/stonks/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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]
23 changes: 23 additions & 0 deletions crates/stonks/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
58 changes: 58 additions & 0 deletions crates/stonks/README.md
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
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_<field>(...)`
or constructors, pass request parameters directly where `Into<Request>` 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.
142 changes: 142 additions & 0 deletions crates/stonks/src/api.rs
Original file line number Diff line number Diff line change
@@ -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<Executor>,
}

impl ApiClient {
pub(crate) fn new(executor: Arc<Executor>) -> 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<REQUEST>(&self, request: REQUEST) -> ApiClientsResult<Response>
where
REQUEST: Into<Request>,
{
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<Vec<String>> {
self.executor.exec_get_extra(VIRTUAL_POOL_ADDRESSES_PATH, params, &[]).await
}

async fn load_all_virtual_pool_addresses(&self) -> ApiClientsResult<Vec<String>> {
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(&params).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<String>,
page: &mut u32,
mut page_addresses: Vec<String>,
) -> ApiClientsResult<bool> {
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"]);
}
}
Loading
Loading