use std::fmt;
use super::arborter_pb::arborter_service_client::ArborterServiceClient;
use super::arborter_pb::{CancelOrderRequest, CancelOrderResponse, OrderToCancel, Side};
use eyre::Result;
use prost::Message;
use crate::commands::config::config_pb::GetConfigResponse;
use crate::grpc::create_channel;
use crate::wallet::Wallet;
impl fmt::Display for CancelOrderResponse {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"CancelOrderResponse {{\n order_canceled: {},\n transaction_hashes: [{}]\n}}",
self.order_canceled,
self.transaction_hashes
.iter()
.map(|th| format!("{}: {}", th.hash_type, th.hash_value))
.collect::<Vec<_>>()
.join(", ")
)
}
}
impl CancelOrderResponse {
pub fn get_formatted_transaction_hashes(&self) -> Vec<String> {
self.transaction_hashes
.iter()
.map(|th| format!("[{}] {}", th.hash_type.to_uppercase(), th.hash_value))
.collect()
}
}
pub async fn call_cancel_order_with_wallet(
url: String,
market_id: String,
side: i32,
token_address: String,
order_id: u64,
wallet: &Wallet,
) -> Result<CancelOrderResponse> {
let channel = create_channel(&url).await?;
let mut client = ArborterServiceClient::new(channel);
let order_to_cancel = OrderToCancel {
market_id,
side,
token_address,
order_id,
};
let mut buffer = Vec::new();
order_to_cancel.encode(&mut buffer)?;
let signature_bytes = wallet.sign_message(&buffer).await?;
let request = CancelOrderRequest {
order: Some(order_to_cancel),
signature_hash: signature_bytes,
};
let request = tonic::Request::new(request);
let response_data = classify_cancel_result(client.cancel_order(request).await)?;
tracing::info!("Cancel response received: {}", response_data);
Ok(response_data)
}
fn classify_cancel_result(
result: Result<tonic::Response<CancelOrderResponse>, tonic::Status>,
) -> Result<CancelOrderResponse, tonic::Status> {
match result {
Ok(response) => Ok(response.into_inner()),
Err(status) if status.code() == tonic::Code::NotFound => Ok(CancelOrderResponse {
order_canceled: false,
transaction_hashes: Vec::new(),
current_orderbook: Vec::new(),
}),
Err(status) => Err(status),
}
}
pub(crate) fn resolve_side_and_token(
config: &GetConfigResponse,
market: &crate::commands::config::config_pb::Market,
side: &str,
) -> Result<(i32, String)> {
let (side_value, network, symbol) = match side.to_lowercase().as_str() {
"buy" | "bid" => (
Side::Bid as i32,
&market.quote_chain_network,
&market.quote_chain_token_symbol,
),
"sell" | "ask" => (
Side::Ask as i32,
&market.base_chain_network,
&market.base_chain_token_symbol,
),
_ => {
return Err(eyre::eyre!(
"Invalid side '{}'. Must be 'buy' or 'sell'",
side
));
}
};
let chain = config
.get_chain(network)
.ok_or_else(|| eyre::eyre!("Chain '{}' not found in configuration", network))?;
let token = chain
.tokens
.get(symbol)
.ok_or_else(|| eyre::eyre!("Token '{}' not found on chain '{}'", symbol, network))?;
Ok((side_value, token.address.clone()))
}
pub async fn call_cancel_order_from_config_with_wallet(
url: String,
market_id: String,
side: String,
order_id: u64,
wallet: &Wallet,
config: GetConfigResponse,
) -> Result<CancelOrderResponse> {
let market = super::send_order::lookup_market(&config, &market_id)?;
let (side_value, token_address) = resolve_side_and_token(&config, market, &side)?;
tracing::info!(
"Canceling order: market={}, side={}, order_id={}, token_address={}",
market.name,
side,
order_id,
token_address
);
call_cancel_order_with_wallet(
url,
market.market_id.clone(),
side_value,
token_address,
order_id,
wallet,
)
.await
}
#[cfg(test)]
mod resolve_side_and_token_tests {
use super::*;
use crate::commands::config::config_pb::{Chain, Configuration, Market, Token};
fn chain(network: &str, symbol: &str, address: &str) -> Chain {
let mut c = Chain {
network: network.to_string(),
..Default::default()
};
c.tokens.insert(
symbol.to_string(),
Token {
symbol: symbol.to_string(),
address: address.to_string(),
..Default::default()
},
);
c
}
fn fixture() -> (GetConfigResponse, Market) {
let market = Market {
base_chain_network: "base-net".into(),
quote_chain_network: "quote-net".into(),
base_chain_token_symbol: "BASE".into(),
quote_chain_token_symbol: "QUOTE".into(),
market_id: "m".into(),
..Default::default()
};
let config = GetConfigResponse {
config: Some(Configuration {
chains: vec![
chain("base-net", "BASE", "0xbase"),
chain("quote-net", "QUOTE", "0xquote"),
],
markets: vec![market.clone()],
}),
};
(config, market)
}
#[test]
fn a_bid_resolves_the_quote_chain_token() {
let (config, market) = fixture();
for side in ["buy", "bid", "BUY", "Bid"] {
let (s, token) = resolve_side_and_token(&config, &market, side).expect(side);
assert_eq!(s, Side::Bid as i32, "side for {side}");
assert_eq!(token, "0xquote", "token for {side}");
}
}
#[test]
fn an_ask_resolves_the_base_chain_token() {
let (config, market) = fixture();
for side in ["sell", "ask", "SELL", "Ask"] {
let (s, token) = resolve_side_and_token(&config, &market, side).expect(side);
assert_eq!(s, Side::Ask as i32, "side for {side}");
assert_eq!(token, "0xbase", "token for {side}");
}
}
#[test]
fn an_unknown_side_is_rejected() {
let (config, market) = fixture();
let err = resolve_side_and_token(&config, &market, "sideways")
.expect_err("an unknown side must not resolve");
assert!(err.to_string().contains("sideways"), "got: {err}");
}
#[test]
fn a_missing_chain_is_an_error() {
let (_, market) = fixture();
let config = GetConfigResponse {
config: Some(Configuration {
chains: vec![chain("base-net", "BASE", "0xbase")],
markets: vec![market.clone()],
}),
};
let err = resolve_side_and_token(&config, &market, "buy")
.expect_err("a missing quote chain must not resolve");
assert!(err.to_string().contains("quote-net"), "got: {err}");
}
#[test]
fn a_missing_token_is_an_error() {
let (_, market) = fixture();
let config = GetConfigResponse {
config: Some(Configuration {
chains: vec![
chain("base-net", "BASE", "0xbase"),
chain("quote-net", "OTHER", "0xother"),
],
markets: vec![market.clone()],
}),
};
let err = resolve_side_and_token(&config, &market, "buy")
.expect_err("a missing token must not resolve");
assert!(err.to_string().contains("QUOTE"), "got: {err}");
}
}
#[cfg(test)]
mod classify_cancel_result_tests {
use super::*;
#[test]
fn a_not_found_cancel_reports_already_gone_not_failure() {
let result = classify_cancel_result(Err(tonic::Status::not_found("order not found")));
let response =
result.expect("NOT_FOUND must classify as an outcome, not a propagated error");
assert!(
!response.order_canceled,
"already-gone must not read as a successful cancel"
);
assert!(response.transaction_hashes.is_empty());
}
#[test]
fn a_different_status_code_is_still_an_error() {
let result = classify_cancel_result(Err(tonic::Status::internal("boom")));
let status = result.expect_err("an internal error must not classify as success");
assert_eq!(status.code(), tonic::Code::Internal);
}
#[test]
fn a_successful_cancel_passes_through_unchanged() {
let response = CancelOrderResponse {
order_canceled: true,
transaction_hashes: Vec::new(),
current_orderbook: Vec::new(),
};
let result = classify_cancel_result(Ok(tonic::Response::new(response)));
assert!(result.expect("ok passes through").order_canceled);
}
}