use std::fmt;
use super::arborter_pb::arborter_service_client::ArborterServiceClient;
use super::arborter_pb::{CancelOrderRequest, CancelOrderResponse, OrderToCancel};
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 {{ order_canceled: {} }}",
self.order_canceled
)
}
}
pub(crate) async fn call_cancel_order_with_wallet(
url: String,
market_id: String,
side: i32,
order_id: [u8; 32],
wallet: &Wallet,
) -> Result<CancelOrderResponse> {
let channel = create_channel(&url).await?;
let mut client = ArborterServiceClient::new(channel);
let order_to_cancel = OrderToCancel {
market_id,
side,
order_id: order_id.to_vec(),
};
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,
}),
Err(status) => Err(status),
}
}
pub(crate) fn resolve_side(side: &str) -> Result<i32> {
Ok(super::send_order::parse_side(side)? as i32)
}
pub async fn call_cancel_order_from_config_with_wallet(
url: String,
market_id: String,
side: String,
order_id: [u8; 32],
wallet: &Wallet,
config: GetConfigResponse,
) -> Result<CancelOrderResponse> {
let market = super::send_order::lookup_market(&config, &market_id)?;
let side_value = resolve_side(&side)?;
tracing::info!(
"Canceling order: market={}, side={}, order_id=0x{}",
market.name,
side,
hex::encode(order_id)
);
call_cancel_order_with_wallet(url, market.market_id.clone(), side_value, order_id, wallet).await
}
#[cfg(test)]
mod resolve_side_tests {
use super::super::arborter_pb::Side;
use super::*;
#[test]
fn a_bid_resolves_to_the_bid_side() {
for side in ["buy", "bid", "BUY", "Bid"] {
let s = resolve_side(side).expect(side);
assert_eq!(s, Side::Bid as i32, "side for {side}");
}
}
#[test]
fn an_ask_resolves_to_the_ask_side() {
for side in ["sell", "ask", "SELL", "Ask"] {
let s = resolve_side(side).expect(side);
assert_eq!(s, Side::Ask as i32, "side for {side}");
}
}
#[test]
fn an_unknown_side_is_rejected() {
let err = resolve_side("sideways").expect_err("an unknown side must not resolve");
assert!(err.to_string().contains("sideways"), "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"
);
}
#[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,
};
let result = classify_cancel_result(Ok(tonic::Response::new(response)));
assert!(result.expect("ok passes through").order_canceled);
}
}