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

[dex.credentials]
# 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,
},
}
78 changes: 78 additions & 0 deletions src/infra/config/dex/bitget/file.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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.
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
163 changes: 163 additions & 0 deletions src/infra/dex/bitget/dto.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
//! DTOs for the Bitget swap API.
//! Full documentation: https://web3.bitget.com/en/docs/swap/

use {
crate::domain::{dex, eth},
bigdecimal::{BigDecimal, ToPrimitive},
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 swap request with enriched response (`requestMod = "rich"`).
///
/// 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 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 - hardcoded to "bgwevmaggregator" for EVM chains.
pub market: String,

/// Slippage tolerance as a percentage (e.g. 1.0 for 1%).
pub slippage: f64,

/// Request mode - "rich" returns quote data alongside swap calldata.
pub request_mod: String,

/// 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,
slippage: &dex::Slippage,
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: "bgwevmaggregator".to_string(),
slippage: slippage.as_factor().to_f64().unwrap_or_default() * 100.0,
request_mod: "rich".to_string(),
fee_rate: Some(0.0),
}
}
}

/// A Bitget API enriched swap response (returned when `requestMod = "rich"`).
#[serde_as]
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
#[allow(dead_code)]
pub struct SwapResponse {
/// Output amount in decimal units.
#[serde_as(as = "serde_with::DisplayFromStr")]
pub out_amount: BigDecimal,

/// Minimum output amount after slippage (computed by API).
#[serde_as(as = "serde_with::DisplayFromStr")]
pub min_amount: BigDecimal,

/// Gas fee information.
pub gas_fee: GasFee,

/// Transaction data for the swap.
pub swap_transaction: SwapTransaction,
}

#[serde_as]
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GasFee {
#[serde_as(as = "serde_with::DisplayFromStr")]
pub gas_limit: u64,
}

#[derive(Deserialize, Clone, Debug)]
pub struct SwapTransaction {
/// Contract address (router/spender).
pub to: eth::Address,
/// Hex-encoded calldata with "0x" prefix.
pub data: String,
}

impl SwapTransaction {
/// Decode the hex-encoded calldata (with "0x" prefix) to bytes.
pub fn decode_calldata(&self) -> Result<Vec<u8>, hex::FromHexError> {
let hex_str = self.data.strip_prefix("0x").unwrap_or(&self.data);
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