Skip to content
Closed
Show file tree
Hide file tree
Changes from 17 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
18 changes: 18 additions & 0 deletions config/example.bitget.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
node-url = "http://localhost:8545"

[dex]
# Specify which chain to use, 1 for Ethereum.
# Supported chains: eth (1), bsc (56), base (8453)
chain-id = "1"

# Optionally specify a custom Bitget swap API endpoint
# endpoint = "https://bopenapi.bgwapi.io/bgw-pro/swapx/pro/"

# Bitget API key (obtain from Bitget integration team)
api-key = "$BITGET_API_KEY"

# Bitget API secret for signing requests
api-secret = "$BITGET_API_SECRET"

# Partner code sent in the Partner-Code header (defaults to "Cowswap")
# partner-code = "Cowswap"
5 changes: 5 additions & 0 deletions src/infra/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,9 @@ pub enum Command {
#[clap(long, env)]
config: PathBuf,
},
/// solve individual orders using Bitget API
Bitget {
#[clap(long, env)]
config: PathBuf,
},
}
79 changes: 79 additions & 0 deletions src/infra/config/dex/bitget/file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use {
crate::{
domain::eth,
infra::{config::dex::file, dex::bitget},
},
serde::Deserialize,
serde_with::serde_as,
std::path::Path,
};

#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct Config {
/// The base URL for the Bitget swap API.
#[serde(default = "default_endpoint")]
#[serde_as(as = "serde_with::DisplayFromStr")]
endpoint: reqwest::Url,

/// Chain ID used to automatically determine contract addresses.
chain_id: eth::ChainId,

/// Bitget API credentials.
#[serde(flatten)]
credentials: BitgetCredentialsConfig,

/// Partner code sent in the `Partner-Code` header.
#[serde(default = "default_partner_code")]
partner_code: String,
}

#[derive(Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
struct BitgetCredentialsConfig {
/// Bitget API key.
api_key: String,

/// Bitget API secret for signing requests.
api_secret: String,
}

#[allow(clippy::from_over_into)]
impl Into<bitget::BitgetCredentialsConfig> for BitgetCredentialsConfig {
fn into(self) -> bitget::BitgetCredentialsConfig {
bitget::BitgetCredentialsConfig {
api_key: self.api_key,
api_secret: self.api_secret,
}
}
}

fn default_partner_code() -> String {
"cowswap".to_string()
}

fn default_endpoint() -> reqwest::Url {
bitget::DEFAULT_ENDPOINT.parse().unwrap()
}

/// Load the Bitget solver configuration from a TOML file.
///
/// # Panics
///
/// This method panics if the config is invalid or on I/O errors.
pub async fn load(path: &Path) -> super::Config {
let (base, config) = file::load::<Config>(path).await;

super::Config {
bitget: bitget::Config {
endpoint: config.endpoint,
chain_id: config.chain_id,
credentials: config.credentials.into(),
partner_code: config.partner_code,
block_stream: base.block_stream.clone(),
settlement_contract: base.contracts.settlement,
},
base,
}
}
6 changes: 6 additions & 0 deletions src/infra/config/dex/bitget/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub mod file;

pub struct Config {
pub bitget: crate::infra::dex::bitget::Config,
pub base: super::Config,
}
1 change: 1 addition & 0 deletions src/infra/config/dex/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod balancer;
pub mod bitget;
mod file;
pub mod okx;
pub mod oneinch;
Expand Down
205 changes: 205 additions & 0 deletions src/infra/dex/bitget/dto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
//! DTOs for the Bitget swap API.
//! Full documentation: https://web3.bitget.com/en/docs/swap/

use {
crate::domain::{dex, eth},
bigdecimal::BigDecimal,
serde::{Deserialize, Serialize},
serde_with::serde_as,
};

/// Bitget chain name used in API requests.
#[derive(Clone, Copy, Serialize)]
pub enum ChainName {
#[serde(rename = "eth")]
Mainnet,
#[serde(rename = "bsc")]
Bnb,
#[serde(rename = "base")]
Base,
#[serde(rename = "polygon")]
Polygon,
#[serde(rename = "arb")]
ArbitrumOne,
}

impl ChainName {
pub fn new(chain_id: eth::ChainId) -> Self {
match chain_id {
eth::ChainId::Mainnet => Self::Mainnet,
eth::ChainId::Bnb => Self::Bnb,
eth::ChainId::Base => Self::Base,
eth::ChainId::Polygon => Self::Polygon,
eth::ChainId::ArbitrumOne => Self::ArbitrumOne,
_ => panic!("unsupported Bitget chain: {chain_id:?}"),
}
}
}

/// A Bitget API quote request.
///
/// See [API](https://web3.bitget.com/en/docs/swap/)
/// documentation for more detailed information on each parameter.
#[serde_as]
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QuoteRequest {
/// Source token contract address (empty string for native token).
pub from_contract: eth::Address,

/// Input amount in human-readable decimal units (e.g. "1" for 1 WETH).
#[serde_as(as = "serde_with::DisplayFromStr")]
pub from_amount: BigDecimal,

/// Source chain name (e.g., "eth", "bsc", "base").
pub from_chain: ChainName,

/// Debit address for gas estimation.
#[serde(skip_serializing_if = "Option::is_none")]
pub from_address: Option<eth::Address>,

/// Target token contract address (empty string for native token).
pub to_contract: eth::Address,

/// Target chain name (same as from_chain for same-chain swaps).
pub to_chain: ChainName,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting! So in the cross chain version of cow protocol this solver could already be used. 🚀


/// Whether to estimate gas.
#[serde(skip_serializing_if = "Option::is_none")]
pub estimate_gas: Option<bool>,
}

impl QuoteRequest {
pub fn from_order(
order: &dex::Order,
chain_name: ChainName,
settlement_contract: eth::Address,
sell_decimals: u8,
) -> Self {
Self {
from_contract: order.sell.0,
from_amount: super::wei_to_decimal(order.amount.get(), sell_decimals),
from_chain: chain_name,
to_contract: order.buy.0,
to_chain: chain_name,
from_address: Some(settlement_contract),
estimate_gas: Some(true),
}
}
}

/// A Bitget API swap (calldata) request.
#[serde_as]
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SwapRequest {
/// Source token contract address.
pub from_contract: eth::Address,

/// Input amount in human-readable decimal units (e.g. "1" for 1 WETH).
#[serde_as(as = "serde_with::DisplayFromStr")]
pub from_amount: BigDecimal,

/// Source chain name.
pub from_chain: ChainName,

/// Target token contract address.
pub to_contract: eth::Address,

/// Target chain name.
pub to_chain: ChainName,

/// Debit address.
pub from_address: eth::Address,

/// Recipient address.
pub to_address: eth::Address,

/// Optimal channel from quote API.
pub market: String,

/// Minimum amount to receive in decimal units. When provided, the
/// `slippage` field is ignored by the API. By setting this explicitly we
/// ensure the generated calldata will revert on-chain if the output drops
/// below this value — avoiding a race between quote and swap calls.
#[serde_as(as = "serde_with::DisplayFromStr")]
pub to_min_amount: BigDecimal,

/// Fee rate in per mille. 0 for no fee.
#[serde(skip_serializing_if = "Option::is_none")]
pub fee_rate: Option<f64>,
}

impl SwapRequest {
pub fn from_order(
order: &dex::Order,
chain_name: ChainName,
settlement_contract: eth::Address,
market: String,
to_min_amount: BigDecimal,
sell_decimals: u8,
) -> Self {
Self {
from_contract: order.sell.0,
from_amount: super::wei_to_decimal(order.amount.get(), sell_decimals),
from_chain: chain_name,
to_contract: order.buy.0,
to_chain: chain_name,
from_address: settlement_contract,
to_address: settlement_contract,
market,
to_min_amount,
fee_rate: Some(0.0),
}
}
}

/// A Bitget API quote response.
#[serde_as]
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct QuoteResponse {
/// Output amount in decimal units (e.g. "1964.365496").
#[serde_as(as = "serde_with::DisplayFromStr")]
pub to_amount: BigDecimal,

/// Channel name (e.g., "uniswap.v3").
pub market: String,

/// Estimated gas limit.
#[serde(default)]
pub gas_limit: u64,
}

/// A Bitget API swap response.
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct SwapResponse {
/// Contract address to interact with (EVM chains only).
/// This is the router/spender address.
pub contract: eth::Address,

/// Hex-encoded calldata for the transaction (with "0x" prefix).
pub calldata: String,
}

impl SwapResponse {
/// Decode the hex-encoded calldata (with "0x" prefix) to bytes.
pub fn decode_calldata(&self) -> Result<Vec<u8>, hex::FromHexError> {
let hex_str = self.calldata.strip_prefix("0x").unwrap_or(&self.calldata);
hex::decode(hex_str)
}
}

/// A Bitget API response wrapper.
///
/// On success `status` is 0 and `data` contains the result.
/// On error `status` is non-zero and `data` is null.
#[derive(Deserialize, Clone, Debug)]
pub struct Response<T> {
/// Response status code (0 = success).
pub status: i64,

/// Response data — `None` when the API returns an error.
pub data: Option<T>,
}
Loading
Loading