use std::str::FromStr;
use serde::Serialize;
use super::prelude::enums::{ExtrinsicStatus, Hash};
use crate::error::{CallbackExecutorError, Error};
#[cfg(feature = "tokio")]
pub struct Executor {
http_connection_pool: reqwest::Client,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Url(pub(crate) reqwest::Url);
impl ToString for Url {
fn to_string(&self) -> String {
self.0.to_string()
}
}
impl Url {
pub fn new(url: &str) -> Result<Self, Error> {
Self::from_str(url)
}
}
impl FromStr for Url {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let url = reqwest::Url::from_str(s)
.map_err(|e| CallbackExecutorError::InvalidUrl(e.to_string()))?;
Ok(Self(url))
}
}
impl Clone for Executor {
fn clone(&self) -> Self {
Self {
http_connection_pool: self.http_connection_pool.clone(),
}
}
}
impl Executor {
pub fn new() -> Self {
Self {
http_connection_pool: reqwest::Client::new(),
}
}
#[cfg(feature = "tokio")]
#[cfg(feature = "serde")]
pub fn execute(&self, status: ExtrinsicStatus, callback_url: Url) -> Result<(), Error> {
let client = self.http_connection_pool.clone();
let body = Self::infer_callback_body(status);
let task = client.post(callback_url.0).json(&body).send();
tokio::task::spawn(task);
Ok(())
}
fn infer_callback_body(status: ExtrinsicStatus) -> CallBackBody<Hash> {
match status {
ExtrinsicStatus::Pending => CallBackBody::new(false, String::from("pending"), None),
ExtrinsicStatus::Failed(reason) => CallBackBody::new(
false,
format!("failed with reason : {}", reason.inner()),
None,
),
ExtrinsicStatus::Success(result) => {
CallBackBody::new(true, String::from("success"), Some(result.hash()))
}
}
}
}
#[cfg(feature = "serde")]
#[derive(Serialize)]
pub struct CallBackBody<Data: Serialize> {
status: bool,
message: String,
data: Option<Data>,
}
#[cfg(feature = "serde")]
impl<Data: Serialize> CallBackBody<Data> {
pub fn new(status: bool, message: String, data: Option<Data>) -> Self {
Self {
status,
message,
data,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
tx::extrinsics::extrinsics_tracker::enums::{ExtrinsicResult, Reason},
types::MandalaConfig,
};
#[cfg(test)]
use mockito;
use serde_json::json;
use subxt::{blocks::ExtrinsicEvents, OnlineClient};
use tokio::time::{sleep, Duration};
fn mock_url(server_url: String) -> Url {
Url::from_str((server_url + "/callback").as_str()).unwrap()
}
#[tokio::test]
async fn test_execute_pending_status() {
let mut server = mockito::Server::new();
let mock = server
.mock("POST", "/callback")
.match_body(mockito::Matcher::JsonString(
r#"{"status":false,"message":"pending","data":null}"#.to_string(),
))
.create();
let executor = Executor::new();
let status = ExtrinsicStatus::Pending;
let result = executor.execute(status, mock_url(server.url()));
assert!(result.is_ok());
sleep(Duration::from_millis(1000)).await;
mock.assert();
}
#[tokio::test]
async fn test_execute_failed_status() {
let mut server = mockito::Server::new();
let mock = server
.mock("POST", "/callback")
.match_body(mockito::Matcher::JsonString(
r#"{"status":false,"message":"failed with reason : NotFound","data":null}"#
.to_string(),
))
.create();
let executor = Executor::new();
let status = ExtrinsicStatus::Failed(Reason::from("NotFound".to_string()));
let result = executor.execute(status, mock_url(server.url()));
assert!(result.is_ok());
sleep(Duration::from_millis(1000)).await;
mock.assert();
}
#[tokio::test]
async fn test_execute_success_status() {
let mut server = mockito::Server::new();
let events = get_extrinsic_events().await.unwrap();
let extrinsic_result = ExtrinsicResult::from(events);
let mock = server.mock("POST", "/callback")
.match_body(
mockito::Matcher::Json(json!({"status": true, "message": "success", "data": extrinsic_result.hash().to_string()}))
)
.create();
let executor = Executor::new();
let status = ExtrinsicStatus::Success(extrinsic_result);
let result = executor.execute(status, mock_url(server.url()));
assert!(result.is_ok());
sleep(Duration::from_millis(1000)).await;
mock.assert();
}
async fn get_extrinsic_events(
) -> Result<ExtrinsicEvents<MandalaConfig>, Box<dyn std::error::Error>> {
let api = OnlineClient::<MandalaConfig>::new().await.unwrap();
let latest_block = api.blocks().at_latest().await?;
let extrinsics = latest_block.extrinsics().await?;
for ext in extrinsics.iter() {
let ext = ext?;
let events = ext.events().await?;
return Ok(events);
}
Err("something went south".into())
}
}