use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum TransactionState {
Received,
PolicyRequired,
PartiallyAuthorized,
ReadyToSettle,
Settled,
Rejected,
Cancelled,
Reverted,
}
impl TransactionState {
pub fn is_terminal(&self) -> bool {
matches!(
self,
TransactionState::Rejected | TransactionState::Cancelled | TransactionState::Reverted
)
}
pub fn requires_decision(&self) -> bool {
matches!(
self,
TransactionState::Received
| TransactionState::PolicyRequired
| TransactionState::ReadyToSettle
)
}
}
impl fmt::Display for TransactionState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TransactionState::Received => write!(f, "received"),
TransactionState::PolicyRequired => write!(f, "policy_required"),
TransactionState::PartiallyAuthorized => write!(f, "partially_authorized"),
TransactionState::ReadyToSettle => write!(f, "ready_to_settle"),
TransactionState::Settled => write!(f, "settled"),
TransactionState::Rejected => write!(f, "rejected"),
TransactionState::Cancelled => write!(f, "cancelled"),
TransactionState::Reverted => write!(f, "reverted"),
}
}
}
impl std::str::FromStr for TransactionState {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"received" => Ok(TransactionState::Received),
"policy_required" => Ok(TransactionState::PolicyRequired),
"partially_authorized" => Ok(TransactionState::PartiallyAuthorized),
"ready_to_settle" => Ok(TransactionState::ReadyToSettle),
"settled" => Ok(TransactionState::Settled),
"rejected" => Ok(TransactionState::Rejected),
"cancelled" => Ok(TransactionState::Cancelled),
"reverted" => Ok(TransactionState::Reverted),
_ => Err(format!("Invalid transaction state: {}", s)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AgentState {
Pending,
Authorized,
Rejected,
Removed,
}
impl fmt::Display for AgentState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AgentState::Pending => write!(f, "pending"),
AgentState::Authorized => write!(f, "authorized"),
AgentState::Rejected => write!(f, "rejected"),
AgentState::Removed => write!(f, "removed"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FsmEvent {
TransactionReceived {
agent_dids: Vec<String>,
},
AuthorizeReceived {
agent_did: String,
settlement_address: Option<String>,
expiry: Option<String>,
},
RejectReceived {
agent_did: String,
reason: Option<String>,
},
CancelReceived {
by_did: String,
reason: Option<String>,
},
PoliciesReceived {
from_did: String,
},
PresentationReceived {
from_did: String,
},
SettleReceived {
settlement_id: Option<String>,
amount: Option<String>,
},
RevertReceived {
by_did: String,
reason: String,
},
AgentsAdded {
agent_dids: Vec<String>,
},
AgentRemoved {
agent_did: String,
},
}
impl fmt::Display for FsmEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FsmEvent::TransactionReceived { .. } => write!(f, "TransactionReceived"),
FsmEvent::AuthorizeReceived { agent_did, .. } => {
write!(f, "AuthorizeReceived({})", agent_did)
}
FsmEvent::RejectReceived { agent_did, .. } => {
write!(f, "RejectReceived({})", agent_did)
}
FsmEvent::CancelReceived { by_did, .. } => write!(f, "CancelReceived({})", by_did),
FsmEvent::PoliciesReceived { from_did } => {
write!(f, "PoliciesReceived({})", from_did)
}
FsmEvent::PresentationReceived { from_did } => {
write!(f, "PresentationReceived({})", from_did)
}
FsmEvent::SettleReceived { .. } => write!(f, "SettleReceived"),
FsmEvent::RevertReceived { by_did, .. } => write!(f, "RevertReceived({})", by_did),
FsmEvent::AgentsAdded { agent_dids } => {
write!(f, "AgentsAdded({})", agent_dids.join(", "))
}
FsmEvent::AgentRemoved { agent_did } => write!(f, "AgentRemoved({})", agent_did),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Decision {
AuthorizationRequired {
transaction_id: String,
pending_agents: Vec<String>,
},
PolicySatisfactionRequired {
transaction_id: String,
requested_by: String,
},
SettlementRequired {
transaction_id: String,
},
}
impl fmt::Display for Decision {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Decision::AuthorizationRequired {
transaction_id,
pending_agents,
} => write!(
f,
"AuthorizationRequired(tx={}, agents={})",
transaction_id,
pending_agents.join(", ")
),
Decision::PolicySatisfactionRequired {
transaction_id,
requested_by,
} => write!(
f,
"PolicySatisfactionRequired(tx={}, by={})",
transaction_id, requested_by
),
Decision::SettlementRequired { transaction_id } => {
write!(f, "SettlementRequired(tx={})", transaction_id)
}
}
}
}
#[derive(Debug, Clone)]
pub struct Transition {
pub from_state: TransactionState,
pub to_state: TransactionState,
pub event: FsmEvent,
pub decision: Option<Decision>,
}
#[derive(Debug, Clone)]
pub struct InvalidTransition {
pub current_state: TransactionState,
pub event: FsmEvent,
pub reason: String,
}
impl fmt::Display for InvalidTransition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Invalid transition: cannot apply {} in state {} ({})",
self.event, self.current_state, self.reason
)
}
}
impl std::error::Error for InvalidTransition {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionContext {
pub transaction_id: String,
pub state: TransactionState,
pub agents: HashMap<String, AgentState>,
pub has_pending_policies: bool,
}
impl TransactionContext {
pub fn new(transaction_id: String, agent_dids: Vec<String>) -> Self {
let agents = agent_dids
.into_iter()
.map(|did| (did, AgentState::Pending))
.collect();
Self {
transaction_id,
state: TransactionState::Received,
agents,
has_pending_policies: false,
}
}
pub fn all_agents_authorized(&self) -> bool {
if self.agents.is_empty() {
return true;
}
self.agents
.values()
.filter(|s| **s != AgentState::Removed)
.all(|s| *s == AgentState::Authorized)
}
pub fn pending_agents(&self) -> Vec<String> {
self.agents
.iter()
.filter(|(_, s)| **s == AgentState::Pending)
.map(|(did, _)| did.clone())
.collect()
}
}
pub struct TransactionFsm;
impl TransactionFsm {
pub fn apply(
ctx: &mut TransactionContext,
event: FsmEvent,
) -> Result<Transition, InvalidTransition> {
let from_state = ctx.state.clone();
if ctx.state.is_terminal() {
return Err(InvalidTransition {
current_state: from_state,
event,
reason: "transaction is in a terminal state".to_string(),
});
}
match (&ctx.state, &event) {
(TransactionState::Received, FsmEvent::TransactionReceived { .. }) => {
let decision = Some(Decision::AuthorizationRequired {
transaction_id: ctx.transaction_id.clone(),
pending_agents: ctx.pending_agents(),
});
Ok(Transition {
from_state,
to_state: ctx.state.clone(),
event,
decision,
})
}
(
TransactionState::Received | TransactionState::PolicyRequired,
FsmEvent::PoliciesReceived { from_did },
) => {
ctx.has_pending_policies = true;
ctx.state = TransactionState::PolicyRequired;
let decision = Some(Decision::PolicySatisfactionRequired {
transaction_id: ctx.transaction_id.clone(),
requested_by: from_did.clone(),
});
Ok(Transition {
from_state,
to_state: ctx.state.clone(),
event,
decision,
})
}
(TransactionState::PolicyRequired, FsmEvent::PresentationReceived { .. }) => {
ctx.has_pending_policies = false;
ctx.state = TransactionState::Received;
let decision = Some(Decision::AuthorizationRequired {
transaction_id: ctx.transaction_id.clone(),
pending_agents: ctx.pending_agents(),
});
Ok(Transition {
from_state,
to_state: ctx.state.clone(),
event,
decision,
})
}
(
TransactionState::Received
| TransactionState::PartiallyAuthorized
| TransactionState::PolicyRequired,
FsmEvent::AuthorizeReceived { agent_did, .. },
) => {
if let Some(agent_state) = ctx.agents.get_mut(agent_did) {
*agent_state = AgentState::Authorized;
}
if ctx.all_agents_authorized() {
ctx.state = TransactionState::ReadyToSettle;
let decision = Some(Decision::SettlementRequired {
transaction_id: ctx.transaction_id.clone(),
});
Ok(Transition {
from_state,
to_state: ctx.state.clone(),
event,
decision,
})
} else {
ctx.state = TransactionState::PartiallyAuthorized;
Ok(Transition {
from_state,
to_state: ctx.state.clone(),
event,
decision: None,
})
}
}
(TransactionState::ReadyToSettle, FsmEvent::AuthorizeReceived { agent_did, .. }) => {
if let Some(agent_state) = ctx.agents.get_mut(agent_did) {
*agent_state = AgentState::Authorized;
}
if ctx.all_agents_authorized() {
let decision = Some(Decision::SettlementRequired {
transaction_id: ctx.transaction_id.clone(),
});
Ok(Transition {
from_state,
to_state: ctx.state.clone(),
event,
decision,
})
} else {
ctx.state = TransactionState::PartiallyAuthorized;
Ok(Transition {
from_state,
to_state: ctx.state.clone(),
event,
decision: None,
})
}
}
(_, FsmEvent::RejectReceived { agent_did, .. }) => {
if let Some(agent_state) = ctx.agents.get_mut(agent_did) {
*agent_state = AgentState::Rejected;
}
ctx.state = TransactionState::Rejected;
Ok(Transition {
from_state,
to_state: ctx.state.clone(),
event,
decision: None,
})
}
(_, FsmEvent::CancelReceived { .. }) => {
ctx.state = TransactionState::Cancelled;
Ok(Transition {
from_state,
to_state: ctx.state.clone(),
event,
decision: None,
})
}
(TransactionState::ReadyToSettle, FsmEvent::SettleReceived { .. }) => {
ctx.state = TransactionState::Settled;
Ok(Transition {
from_state,
to_state: ctx.state.clone(),
event,
decision: None,
})
}
(TransactionState::Settled, FsmEvent::RevertReceived { .. }) => {
ctx.state = TransactionState::Reverted;
Ok(Transition {
from_state,
to_state: ctx.state.clone(),
event,
decision: None,
})
}
(_, FsmEvent::AgentsAdded { agent_dids }) => {
for did in agent_dids {
ctx.agents.entry(did.clone()).or_insert(AgentState::Pending);
}
if from_state == TransactionState::ReadyToSettle && !ctx.all_agents_authorized() {
ctx.state = TransactionState::PartiallyAuthorized;
}
Ok(Transition {
from_state,
to_state: ctx.state.clone(),
event,
decision: None,
})
}
(_, FsmEvent::AgentRemoved { agent_did }) => {
if let Some(agent_state) = ctx.agents.get_mut(agent_did) {
*agent_state = AgentState::Removed;
}
if matches!(
ctx.state,
TransactionState::PartiallyAuthorized | TransactionState::Received
) && ctx.all_agents_authorized()
{
ctx.state = TransactionState::ReadyToSettle;
let decision = Some(Decision::SettlementRequired {
transaction_id: ctx.transaction_id.clone(),
});
return Ok(Transition {
from_state,
to_state: ctx.state.clone(),
event,
decision,
});
}
Ok(Transition {
from_state,
to_state: ctx.state.clone(),
event,
decision: None,
})
}
_ => Err(InvalidTransition {
current_state: from_state,
event: event.clone(),
reason: format!("event {} is not valid in state {}", event, ctx.state),
}),
}
}
pub fn valid_events(state: &TransactionState) -> Vec<&'static str> {
match state {
TransactionState::Received => vec![
"TransactionReceived",
"AuthorizeReceived",
"RejectReceived",
"CancelReceived",
"PoliciesReceived",
"AgentsAdded",
"AgentRemoved",
],
TransactionState::PolicyRequired => vec![
"PresentationReceived",
"PoliciesReceived",
"AuthorizeReceived",
"RejectReceived",
"CancelReceived",
"AgentsAdded",
"AgentRemoved",
],
TransactionState::PartiallyAuthorized => vec![
"AuthorizeReceived",
"RejectReceived",
"CancelReceived",
"AgentsAdded",
"AgentRemoved",
],
TransactionState::ReadyToSettle => vec![
"SettleReceived",
"AuthorizeReceived",
"RejectReceived",
"CancelReceived",
"AgentsAdded",
"AgentRemoved",
],
TransactionState::Settled => vec!["RevertReceived"],
TransactionState::Rejected
| TransactionState::Cancelled
| TransactionState::Reverted => {
vec![]
}
}
}
}
#[derive(Debug, Clone, Default)]
pub enum DecisionMode {
#[default]
AutoApprove,
EventBus,
Custom(Arc<dyn DecisionHandler>),
}
#[async_trait]
pub trait DecisionHandler: Send + Sync + fmt::Debug {
async fn handle_decision(&self, ctx: &TransactionContext, decision: &Decision);
}
#[derive(Debug)]
pub struct AutoApproveHandler;
#[async_trait]
impl DecisionHandler for AutoApproveHandler {
async fn handle_decision(&self, _ctx: &TransactionContext, decision: &Decision) {
log::debug!("AutoApproveHandler: auto-resolving {}", decision);
}
}
#[derive(Debug)]
pub struct LogOnlyHandler;
#[async_trait]
impl DecisionHandler for LogOnlyHandler {
async fn handle_decision(&self, ctx: &TransactionContext, decision: &Decision) {
log::info!(
"Decision required for transaction {} (state={}): {}",
ctx.transaction_id,
ctx.state,
decision
);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_ctx(agents: &[&str]) -> TransactionContext {
TransactionContext::new(
"tx-001".to_string(),
agents.iter().map(|s| s.to_string()).collect(),
)
}
#[test]
fn test_happy_path_single_agent() {
let mut ctx = make_ctx(&["did:example:compliance"]);
assert_eq!(ctx.state, TransactionState::Received);
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::TransactionReceived {
agent_dids: vec!["did:example:compliance".to_string()],
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::Received);
assert!(t.decision.is_some());
assert!(matches!(
t.decision.unwrap(),
Decision::AuthorizationRequired { .. }
));
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::AuthorizeReceived {
agent_did: "did:example:compliance".to_string(),
settlement_address: None,
expiry: None,
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::ReadyToSettle);
assert!(matches!(
t.decision.unwrap(),
Decision::SettlementRequired { .. }
));
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::SettleReceived {
settlement_id: Some("eip155:1:tx/0xabc".to_string()),
amount: None,
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::Settled);
assert!(t.decision.is_none());
}
#[test]
fn test_happy_path_multi_agent() {
let mut ctx = make_ctx(&["did:example:a", "did:example:b"]);
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::AuthorizeReceived {
agent_did: "did:example:a".to_string(),
settlement_address: None,
expiry: None,
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::PartiallyAuthorized);
assert!(t.decision.is_none());
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::AuthorizeReceived {
agent_did: "did:example:b".to_string(),
settlement_address: None,
expiry: None,
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::ReadyToSettle);
assert!(matches!(
t.decision.unwrap(),
Decision::SettlementRequired { .. }
));
}
#[test]
fn test_rejection() {
let mut ctx = make_ctx(&["did:example:a"]);
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::RejectReceived {
agent_did: "did:example:a".to_string(),
reason: Some("sanctions screening failed".to_string()),
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::Rejected);
assert!(ctx.state.is_terminal());
}
#[test]
fn test_cancellation() {
let mut ctx = make_ctx(&["did:example:a"]);
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::CancelReceived {
by_did: "did:example:originator".to_string(),
reason: None,
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::Cancelled);
assert!(ctx.state.is_terminal());
}
#[test]
fn test_policy_flow() {
let mut ctx = make_ctx(&["did:example:a"]);
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::PoliciesReceived {
from_did: "did:example:beneficiary-vasp".to_string(),
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::PolicyRequired);
assert!(matches!(
t.decision.unwrap(),
Decision::PolicySatisfactionRequired { .. }
));
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::PresentationReceived {
from_did: "did:example:originator-vasp".to_string(),
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::Received);
assert!(matches!(
t.decision.unwrap(),
Decision::AuthorizationRequired { .. }
));
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::AuthorizeReceived {
agent_did: "did:example:a".to_string(),
settlement_address: None,
expiry: None,
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::ReadyToSettle);
}
#[test]
fn test_revert() {
let mut ctx = make_ctx(&["did:example:a"]);
TransactionFsm::apply(
&mut ctx,
FsmEvent::AuthorizeReceived {
agent_did: "did:example:a".to_string(),
settlement_address: None,
expiry: None,
},
)
.unwrap();
TransactionFsm::apply(
&mut ctx,
FsmEvent::SettleReceived {
settlement_id: None,
amount: None,
},
)
.unwrap();
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::RevertReceived {
by_did: "did:example:beneficiary".to_string(),
reason: "incorrect amount".to_string(),
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::Reverted);
assert!(ctx.state.is_terminal());
}
#[test]
fn test_terminal_state_rejects_events() {
let mut ctx = make_ctx(&["did:example:a"]);
ctx.state = TransactionState::Rejected;
let result = TransactionFsm::apply(
&mut ctx,
FsmEvent::AuthorizeReceived {
agent_did: "did:example:a".to_string(),
settlement_address: None,
expiry: None,
},
);
assert!(result.is_err());
}
#[test]
fn test_settle_only_from_ready_to_settle() {
let mut ctx = make_ctx(&["did:example:a"]);
let result = TransactionFsm::apply(
&mut ctx,
FsmEvent::SettleReceived {
settlement_id: None,
amount: None,
},
);
assert!(result.is_err());
}
#[test]
fn test_add_agents_blocks_settlement() {
let mut ctx = make_ctx(&["did:example:a"]);
TransactionFsm::apply(
&mut ctx,
FsmEvent::AuthorizeReceived {
agent_did: "did:example:a".to_string(),
settlement_address: None,
expiry: None,
},
)
.unwrap();
assert_eq!(ctx.state, TransactionState::ReadyToSettle);
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::AgentsAdded {
agent_dids: vec!["did:example:b".to_string()],
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::PartiallyAuthorized);
}
#[test]
fn test_remove_agent_enables_settlement() {
let mut ctx = make_ctx(&["did:example:a", "did:example:b"]);
TransactionFsm::apply(
&mut ctx,
FsmEvent::AuthorizeReceived {
agent_did: "did:example:a".to_string(),
settlement_address: None,
expiry: None,
},
)
.unwrap();
assert_eq!(ctx.state, TransactionState::PartiallyAuthorized);
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::AgentRemoved {
agent_did: "did:example:b".to_string(),
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::ReadyToSettle);
assert!(matches!(
t.decision.unwrap(),
Decision::SettlementRequired { .. }
));
}
#[test]
fn test_no_agents_goes_straight_to_ready() {
let mut ctx = make_ctx(&[]);
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::TransactionReceived { agent_dids: vec![] },
)
.unwrap();
assert_eq!(t.to_state, TransactionState::Received);
}
#[test]
fn test_valid_events() {
let events = TransactionFsm::valid_events(&TransactionState::Received);
assert!(events.contains(&"AuthorizeReceived"));
assert!(events.contains(&"PoliciesReceived"));
let events = TransactionFsm::valid_events(&TransactionState::Settled);
assert_eq!(events, vec!["RevertReceived"]);
let events = TransactionFsm::valid_events(&TransactionState::Rejected);
assert!(events.is_empty());
}
#[test]
fn test_display_implementations() {
assert_eq!(TransactionState::Received.to_string(), "received");
assert_eq!(
TransactionState::PolicyRequired.to_string(),
"policy_required"
);
assert_eq!(
TransactionState::PartiallyAuthorized.to_string(),
"partially_authorized"
);
assert_eq!(
TransactionState::ReadyToSettle.to_string(),
"ready_to_settle"
);
assert_eq!(TransactionState::Settled.to_string(), "settled");
assert_eq!(TransactionState::Rejected.to_string(), "rejected");
assert_eq!(TransactionState::Cancelled.to_string(), "cancelled");
assert_eq!(TransactionState::Reverted.to_string(), "reverted");
}
#[test]
fn test_agent_state_display() {
assert_eq!(AgentState::Pending.to_string(), "pending");
assert_eq!(AgentState::Authorized.to_string(), "authorized");
assert_eq!(AgentState::Rejected.to_string(), "rejected");
assert_eq!(AgentState::Removed.to_string(), "removed");
}
#[test]
fn test_reject_during_partial_authorization() {
let mut ctx = make_ctx(&["did:example:a", "did:example:b"]);
TransactionFsm::apply(
&mut ctx,
FsmEvent::AuthorizeReceived {
agent_did: "did:example:a".to_string(),
settlement_address: None,
expiry: None,
},
)
.unwrap();
assert_eq!(ctx.state, TransactionState::PartiallyAuthorized);
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::RejectReceived {
agent_did: "did:example:b".to_string(),
reason: Some("compliance failure".to_string()),
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::Rejected);
}
#[test]
fn test_cancel_during_ready_to_settle() {
let mut ctx = make_ctx(&["did:example:a"]);
TransactionFsm::apply(
&mut ctx,
FsmEvent::AuthorizeReceived {
agent_did: "did:example:a".to_string(),
settlement_address: None,
expiry: None,
},
)
.unwrap();
assert_eq!(ctx.state, TransactionState::ReadyToSettle);
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::CancelReceived {
by_did: "did:example:originator".to_string(),
reason: Some("changed mind".to_string()),
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::Cancelled);
}
#[test]
fn test_authorize_from_policy_required() {
let mut ctx = make_ctx(&["did:example:a"]);
TransactionFsm::apply(
&mut ctx,
FsmEvent::PoliciesReceived {
from_did: "did:example:b".to_string(),
},
)
.unwrap();
assert_eq!(ctx.state, TransactionState::PolicyRequired);
let t = TransactionFsm::apply(
&mut ctx,
FsmEvent::AuthorizeReceived {
agent_did: "did:example:a".to_string(),
settlement_address: None,
expiry: None,
},
)
.unwrap();
assert_eq!(t.to_state, TransactionState::ReadyToSettle);
}
}