use std::sync::Arc;
use std::time::Duration;
use affinidi_tdk::didcomm::Message;
use affinidi_tdk::messaging::{ATM, profiles::ATMProfile};
use serde_json::Value;
use tracing::{debug, warn};
use trust_tasks_didcomm::ENVELOPE_TYPE;
use trust_tasks_rs::TrustTask;
use crate::error::TrqlError;
use crate::pending::PendingReplies;
use crate::transport::{TransportKind, TrqlTransport};
const INBOUND_POLL_WAIT: Duration = Duration::from_secs(10);
const INBOUND_ERROR_BACKOFF: Duration = Duration::from_millis(500);
#[derive(Debug, Clone)]
pub struct DidcommTransportConfig {
pub reply_timeout: Duration,
}
impl Default for DidcommTransportConfig {
fn default() -> Self {
Self {
reply_timeout: Duration::from_secs(60),
}
}
}
pub struct DidcommTransport {
atm: ATM,
profile: Arc<ATMProfile>,
pending: PendingReplies,
reply_timeout: Duration,
demux: tokio::task::JoinHandle<()>,
}
impl DidcommTransport {
pub fn new(
atm: ATM,
profile: Arc<ATMProfile>,
config: DidcommTransportConfig,
) -> Result<Self, TrqlError> {
if profile.to_tdk_profile().mediator.is_none() {
return Err(TrqlError::Config(
"profile has no mediator configured (required for the DIDComm binding)".to_string(),
));
}
let pending = PendingReplies::new();
let demux = tokio::spawn(demux_loop(atm.clone(), profile.clone(), pending.clone()));
Ok(Self {
atm,
profile,
pending,
reply_timeout: config.reply_timeout,
demux,
})
}
}
impl Drop for DidcommTransport {
fn drop(&mut self) {
self.demux.abort();
}
}
#[async_trait::async_trait]
impl TrqlTransport for DidcommTransport {
fn kind(&self) -> TransportKind {
TransportKind::Didcomm
}
async fn exchange(&self, request: TrustTask<Value>) -> Result<TrustTask<Value>, TrqlError> {
let dest = request.recipient.clone().ok_or_else(|| {
TrqlError::Config("request document has no recipient to route to".to_string())
})?;
let request_id = request.id.clone();
let body = serde_json::to_value(&request)
.map_err(|e| TrqlError::Contract(format!("request did not serialize: {e}")))?;
let receiver = self.pending.register(&request_id);
let send_result = self.send_envelope(&dest, &request_id, body).await;
if let Err(error) = send_result {
self.pending.abandon(&request_id);
return Err(error);
}
match tokio::time::timeout(self.reply_timeout, receiver).await {
Ok(Ok(document)) => Ok(document),
Ok(Err(_closed)) => {
self.pending.abandon(&request_id);
Err(TrqlError::Transport {
kind: TransportKind::Didcomm,
detail: "reply demux task stopped".to_string(),
})
}
Err(_elapsed) => {
self.pending.abandon(&request_id);
Err(TrqlError::Timeout {
kind: TransportKind::Didcomm,
waited_secs: self.reply_timeout.as_secs(),
})
}
}
}
}
impl DidcommTransport {
async fn send_envelope(
&self,
dest: &str,
request_id: &str,
body: Value,
) -> Result<(), TrqlError> {
let my_did = self.profile.inner.did.clone();
let envelope_id = uuid::Uuid::new_v4().to_string();
let envelope = Message::build(envelope_id.clone(), ENVELOPE_TYPE.to_string(), body)
.from(my_did.clone())
.to(dest.to_string())
.thid(request_id.to_string())
.finalize();
let packed = self
.atm
.pack_encrypted(&envelope, dest, Some(&my_did), Some(&my_did))
.await
.map_err(|e| TrqlError::Transport {
kind: TransportKind::Didcomm,
detail: format!("packing failed: {e}"),
})?;
let mediator = self
.profile
.to_tdk_profile()
.mediator
.clone()
.ok_or_else(|| {
TrqlError::Config("profile lost its mediator configuration".to_string())
})?;
self.atm
.forward_and_send_message(
&self.profile,
false,
&packed.0,
Some(&envelope_id),
&mediator,
dest,
None,
None,
false,
)
.await
.map_err(|e| TrqlError::Transport {
kind: TransportKind::Didcomm,
detail: format!("send via mediator failed: {e}"),
})?;
Ok(())
}
}
async fn demux_loop(atm: ATM, profile: Arc<ATMProfile>, pending: PendingReplies) {
loop {
match atm
.message_pickup()
.live_stream_next(&profile, Some(INBOUND_POLL_WAIT), false)
.await
{
Ok(Some((message, meta))) => {
if message.typ != ENVELOPE_TYPE {
continue;
}
let document: TrustTask<Value> = match serde_json::from_value(message.body) {
Ok(document) => document,
Err(e) => {
warn!("dropping malformed Trust Task envelope: {e}");
continue;
}
};
if pending.route(document)
&& let Err(e) = atm
.delete_message_background(&profile, &meta.sha256_hash)
.await
{
debug!("ack of handled reply failed (will redeliver): {e}");
}
}
Ok(None) => {}
Err(e) => {
debug!("inbound stream error (backing off): {e}");
tokio::time::sleep(INBOUND_ERROR_BACKOFF).await;
}
}
}
}