Skip to content

Commit

Permalink
Add more APIs and use the model from the services repository
Browse files Browse the repository at this point in the history
  • Loading branch information
cakevm committed Jan 24, 2025
1 parent d859b6e commit d637d7e
Show file tree
Hide file tree
Showing 15 changed files with 1,853 additions and 242 deletions.
1,903 changes: 1,697 additions & 206 deletions Cargo.lock

Large diffs are not rendered by default.

23 changes: 15 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
[package]
name = "cowprotocol-api-rs"
name = "cowprotocol-client"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
license = "MIT OR Apache-2.0"

[dependencies]
alloy-primitives = { version = "0.8.9", features = ["serde"] }
reqwest = { version = "0.12.8", default-features = false, features = ["json", "rustls-tls"] }
serde = { version = "1.0.213", features = ["derive"] }
serde_json = { version = "1.0.132" }
thiserror = "1.0.65"
tokio = { version = "1.41.0", features = ["macros", "rt", "rt-multi-thread"] }
alloy-primitives = { version = "0.8.19", features = ["serde"] }
ethereum-types = "0.14.1"
reqwest = { version = "0.12.12", default-features = false, features = ["json", "rustls-tls"] }
serde = { version = "1.0.217", features = ["derive"] }
serde_json = { version = "1.0.137" }
thiserror = "2.0.11"

cowprotocol-solvers-dto-alloy = {git = "https://github.com/cakevm/cowprotocol-solvers-dto-alloy", rev = "298feba"}
services-model = { git = "https://github.com/cowprotocol/services", rev = "01f35f8", package = "model"}

[dev-dependencies]
tokio = { version = "1.43.0", features = ["macros", "rt", "rt-multi-thread"] }
eyre = "0.6.12"
File renamed without changes.
21 changes: 21 additions & 0 deletions LICENSE-MIT
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2025 cakevm

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
# CoW Protocol REST API client

This crate provides an async client for interacting with the [CoW Protocol REST API](https://api.cow.fi/docs/#/).
This crate provides an async client for interacting with the [CoW Protocol REST API](https://api.cow.fi/docs/#/). The benefit is to use typed models as they have been intended by the [Cow Protocol service](https://github.com/cowprotocol/services).

# Usage
## Status
The crate is currently just a showcase and not for productive use. Generated clients can be difficult to use when dealing with byte types like U256, Address, etc. This client directly uses some models from the [services](https://github.com/cowprotocol/services) repository, ensuring compatibility with the API. The long-term goal is to migrate everything to use [Alloy](https://github.com/alloy-rs/alloy) types or have some converter. This crate currently supports a limited set of API endpoints. If you need additional endpoints, please open an issue or a PR.

Current concept for types:
- The functions from this client accepting [alloy_primitives](https://docs.rs/alloy-primitives/latest/alloy_primitives/)
- The return types can be models from [services](https://github.com/cowprotocol/services) with [ethereum-types](https://crates.io/crates/ethereum-types) or like Auction from this crate.

Future concept for types:
- Alloy for all types

## Usage
See the [examples](./examples) directory for usage examples.

# Features
Currently only supports the `GET /auction` and `GET /solver_competition/latest` endpoint.
## Acknowledgements
Many thanks to the [CoW Protocol](https://github.com/cowprotocol) team for making their service open source. This project uses the models from the [services](https://github.com/cowprotocol/services) repository.

# License
This project is licensed under the [Apache 2.0](./LICENSE).
## License
This project is dual licensed under [Apache 2.0](./LICENSE-APACHE) or [MIT](./LICENSE-MIT).
16 changes: 15 additions & 1 deletion deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ allow = [
# permissive licenses
"0BSD",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0",
"ISC",
"MIT",
"Unicode-DFS-2016",
"Unicode-3.0",
"Unlicense",
"Zlib",
# permissive license with unclear patent grant
Expand All @@ -24,6 +25,19 @@ expression = "ISC AND MIT AND OpenSSL"
license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }]
name = "ring"

[advisories]
ignore = [
# idna url parsing is not used, is a transient dependency from the model - https://rustsec.org/advisories/RUSTSEC-2024-0421
"RUSTSEC-2024-0421",

]
yanked = "warn"

[bans]
multiple-versions = "allow"

[sources]
allow-git = [
"https://github.com/cowprotocol/services",
"https://github.com/cakevm/cowprotocol-solvers-dto-alloy"
]
2 changes: 1 addition & 1 deletion examples/fetch_auction.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use cowprotocol_api_rs::client::{OrderBookApiClient, OrderBookApiConfig};
use cowprotocol_client::client::{OrderBookApiClient, OrderBookApiConfig};

#[tokio::main]
async fn main() {
Expand Down
23 changes: 23 additions & 0 deletions examples/fetch_order.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use alloy_primitives::FixedBytes;
use cowprotocol_client::client::{OrderBookApiClient, OrderBookApiConfig};
use cowprotocol_solvers_dto_alloy::order_uid::OrderUid;
use std::str::FromStr;

#[tokio::main]
async fn main() {
let client = OrderBookApiClient::new(OrderBookApiConfig::default());
let order_uid = OrderUid(
FixedBytes::<56>::from_str(
"0x1e1e4b3c27038cfe9b63c7c662ece8e722667f0b4316d502ee29dad6c49392c0891a539d008f69e62f22902877cce54a58644cae67875b9f",
)
.unwrap(),
);
match client.get_order(&order_uid).await {
Ok(order) => {
println!("{:?}", order);
}
Err(e) => {
println!("{:?}", e);
}
}
}
14 changes: 14 additions & 0 deletions examples/fetch_solver_competition_by_tx_hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use alloy_primitives::TxHash;
use cowprotocol_client::client::{OrderBookApiClient, OrderBookApiConfig};
use eyre::Result;
use std::str::FromStr;

#[tokio::main]
async fn main() -> Result<()> {
let client = OrderBookApiClient::new(OrderBookApiConfig::default());
let tx_hash = TxHash::from_str("0xd0405ae34a75f394ec20493988674ac8912787edbfdcd673b59239bd46ac1684")?;
let solver_competition_latest = client.solver_competition_by_tx_hash(&tx_hash).await?;
println!("{:#?}", solver_competition_latest);

Ok(())
}
4 changes: 2 additions & 2 deletions examples/fetch_solver_competition_latest.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use cowprotocol_api_rs::client::{OrderBookApiClient, OrderBookApiConfig};
use cowprotocol_client::client::{OrderBookApiClient, OrderBookApiConfig};

#[tokio::main]
async fn main() {
Expand All @@ -7,7 +7,7 @@ async fn main() {
match solver_competition_latest {
Ok(solver_competition) => {
println!("Auction ID: {}", solver_competition.auction_id);
match solver_competition.solutions.first() {
match solver_competition.common.solutions.first() {
Some(solution) => {
println!("First Solution: {:#?}", solution);
}
Expand Down
4 changes: 3 additions & 1 deletion resources/response_auction.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
"quote": {
"sellAmount": "401985279546821022",
"buyAmount": "126719694369508828561662681",
"fee": "2706038793168044"
"fee": "2706038793168044",
"solver": "0xcd01bd317df6419af020eeb43e58d7d0c7c48f29"
}
}
}
],
"created": 1718392000,
"validTo": 1718392100,
"kind": "sell",
"receiver": "0xcd01bd317df6419af020eeb43e58d7d0c7c48f29",
Expand Down
1 change: 1 addition & 0 deletions resources/response_solver_competition_latest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"transactionHash": "0xc3afa3f8b9f65c765edc80ad6dfe0048dabbdcb1c6606fd56cec17ec78a6010e",
"auctionStartBlock": 20092228,
"competitionSimulationBlock": 20092230,
"transactionHashes": ["0x044499c2a830890cb0a8ecf9aec6c5621e8310092a58d369cdef726254d3d108"],
"auction": {
"orders": [
"0x9382c4a6ef4d2031e57a28f384b6333e7ba301bd9c1930f0029a118b6ec4d1bd301076c36e034948a747bb61bab9cd03f62672e3666c9ee8",
Expand Down
49 changes: 35 additions & 14 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::{
constants::PROTOCOL_VERSION,
types::{ApiUrl, Chain, OrderBookApiError},
};

use crate::types::{auction::AuctionWithId, solver_competition::SolverCompetition};
use crate::types::{ApiUrl, Chain, OrderBookApiError};
use crate::types::{auction::AuctionWithId};
use alloy_primitives::TxHash;
use cowprotocol_solvers_dto_alloy::order_uid::OrderUid;
use reqwest::{Client, ClientBuilder};
use services_model::order::Order;
use services_model::solver_competition::SolverCompetitionAPI;

#[derive(Debug, Clone)]
pub struct OrderBookApiClient {
Expand All @@ -22,27 +22,45 @@ impl OrderBookApiClient {
pub fn new(cfg: OrderBookApiConfig) -> Self {
let builder = ClientBuilder::new();
let client = builder.build().unwrap();
let base_url = format!("{}/api/{}", cfg.chain.url(), PROTOCOL_VERSION);
let base_url = format!("{}/api/v1", cfg.chain.url());

Self { client, url: ApiUrl { base: base_url } }
}

pub async fn auction(&self) -> Result<AuctionWithId, OrderBookApiError> {
let res = self.client.get(self.url.auction()).send().await?.json::<AuctionWithId>().await?;
let res = self.client.get(self.url.auction()).send().await?.error_for_status()?.json::<AuctionWithId>().await?;
Ok(res)
}

pub async fn solver_competition_latest(&self) -> Result<SolverCompetitionAPI, OrderBookApiError> {
let res =
self.client.get(self.url.solver_competition_latest()).send().await?.error_for_status()?.json::<SolverCompetitionAPI>().await?;
Ok(res)
}

pub async fn solver_competition_by_tx_hash(&self, tx_hash: &TxHash) -> Result<SolverCompetitionAPI, OrderBookApiError> {
let res = self
.client
.get(self.url.solver_competition_by_tx_hash(tx_hash))
.send()
.await?
.error_for_status()?
.json::<SolverCompetitionAPI>()
.await?;
Ok(res)
}

pub async fn solver_competition_latest(&self) -> Result<SolverCompetition, OrderBookApiError> {
let res = self.client.get(self.url.solver_competition_latest()).send().await?.json::<SolverCompetition>().await?;
pub async fn get_order(&self, order_uid: &OrderUid) -> Result<Order, OrderBookApiError> {
let res = self.client.get(self.url.get_order(order_uid)).send().await?.error_for_status()?.json::<Order>().await?;
Ok(res)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::types::solver_competition::SolverCompetition;
use alloy_primitives::{address, U256};
use alloy_primitives::{address, hex, U256};
use ethereum_types::{H160, U256 as EthereumU256};
use std::path::PathBuf;

#[test]
Expand All @@ -62,8 +80,11 @@ mod tests {
d.push("resources/response_solver_competition_latest.json");

let res = std::fs::read_to_string(d).unwrap();
let res: SolverCompetition = serde_json::from_str(&res).unwrap();
let res: SolverCompetitionAPI = serde_json::from_str(&res).unwrap();
assert_eq!(res.auction_id, 9008649);
assert_eq!(*res.auction.prices.get(&address!("0001a500a6b18995b03f44bb040a5ffc28e45cb0")).unwrap(), U256::from(464799964606399u64))
assert_eq!(
res.common.auction.prices.get(&H160(hex!("0001a500a6b18995b03f44bb040a5ffc28e45cb0"))),
Some(&EthereumU256::from(464799964606399u64))
)
}
}
2 changes: 0 additions & 2 deletions src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
pub const PROTOCOL_VERSION: &str = "v1";

pub const API_BASE_MAINNET_PROD: &str = "https://api.cow.fi/mainnet";
pub const API_BASE_MAINNET_STAGING: &str = "https://barn.api.cow.fi/mainnet";

Expand Down
11 changes: 10 additions & 1 deletion src/types.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
pub mod auction;
pub mod solver_competition;

use crate::constants::{
API_BASE_ARBITRUM_ONE_PROD, API_BASE_ARBITRUM_ONE_STAGING, API_BASE_GNOSIS_CHAIN_PROD, API_BASE_GNOSIS_CHAIN_STAGING,
API_BASE_MAINNET_PROD, API_BASE_MAINNET_STAGING, API_BASE_SEPOLIA_PROD, API_BASE_SEPOLIA_STAGING,
};
use alloy_primitives::{hex, TxHash};
use cowprotocol_solvers_dto_alloy::order_uid::OrderUid;
use thiserror::Error;

#[derive(Debug, Error)]
Expand Down Expand Up @@ -57,4 +58,12 @@ impl ApiUrl {
pub fn solver_competition_latest(&self) -> String {
format!("{}/solver_competition/latest", self.base)
}

pub fn solver_competition_by_tx_hash(&self, tx_hash: &TxHash) -> String {
format!("{}/solver_competition/by_tx_hash/{}", self.base, hex::encode_prefixed(tx_hash))
}

pub(crate) fn get_order(&self, order_uid: &OrderUid) -> String {
format!("{}/orders/{}", self.base, order_uid.0)
}
}

0 comments on commit d637d7e

Please sign in to comment.