Skip to main content

tap_node/state_machine/
fsm.rs

1//! Transaction Finite State Machine (FSM)
2//!
3//! Formal state machine modeling the lifecycle of a TAP transaction from
4//! initiation through authorization to settlement. Each state transition
5//! is driven by an incoming TAP message and may require an external
6//! decision before the node takes action.
7//!
8//! # States
9//!
10//! ```text
11//!                          ┌──────────────────────────────────────────┐
12//!                          │         Transaction Lifecycle            │
13//!                          └──────────────────────────────────────────┘
14//!
15//!   ┌─────────┐  Transfer/   ┌──────────────┐  UpdatePolicies/  ┌─────────────────┐
16//!   │         │  Payment     │              │  RequestPresent.  │                 │
17//!   │  (none) │─────────────▶│   Received    │─────────────────▶│ PolicyRequired  │
18//!   │         │              │              │                   │                 │
19//!   └─────────┘              └──────┬───────┘                   └────────┬────────┘
20//!                                   │                                    │
21//!                              ┌────┴─────┐                    Presentation
22//!                              │ DECISION │                    received
23//!                              │ Authorize│                         │
24//!                              │ Reject   │◀────────────────────────┘
25//!                              │ Cancel   │
26//!                              └────┬─────┘
27//!                    ┌──────────────┼──────────────┐
28//!                    │              │              │
29//!                Authorize       Reject         Cancel
30//!                    │              │              │
31//!                    ▼              ▼              ▼
32//!            ┌──────────────┐ ┌──────────┐ ┌───────────┐
33//!            │  Authorized  │ │ Rejected │ │ Cancelled │
34//!            │ (per agent)  │ │          │ │           │
35//!            └──────┬───────┘ └──────────┘ └───────────┘
36//!                   │
37//!            all agents authorized?
38//!                   │ yes
39//!                   ▼
40//!            ┌──────────────────┐
41//!            │ ReadyToSettle    │
42//!            │                  │
43//!            └────────┬─────────┘
44//!                     │
45//!                ┌────┴─────┐
46//!                │ DECISION │
47//!                │ Settle   │
48//!                │ Cancel   │
49//!                └────┬─────┘
50//!                     │
51//!                  Settle
52//!                     │
53//!                     ▼
54//!              ┌─────────────┐
55//!              │  Settled    │
56//!              └──────┬──────┘
57//!                     │
58//!                  Revert?
59//!                     │
60//!                     ▼
61//!              ┌─────────────┐
62//!              │  Reverted   │
63//!              └─────────────┘
64//! ```
65//!
66//! # Decision Points
67//!
68//! The FSM identifies two categories of transitions:
69//!
70//! - **Automatic**: The node processes the message and moves to the next state
71//!   with no external input (e.g., storing a transaction, recording an authorization).
72//!
73//! - **Decision Required**: The transition produces a [`Decision`] that an
74//!   external system must resolve before the node takes further action. For
75//!   example, when a Transfer arrives the node must decide whether to
76//!   Authorize, Reject, or request more information via policies.
77//!
78//! # Per-Agent vs Per-Transaction State
79//!
80//! A transaction has a single top-level [`TransactionState`], but also tracks
81//! per-agent authorization status via [`AgentState`]. The transaction advances
82//! to `ReadyToSettle` only when **all** agents reach `Authorized`.
83
84use async_trait::async_trait;
85use serde::{Deserialize, Serialize};
86use std::collections::HashMap;
87use std::fmt;
88use std::sync::Arc;
89
90// ---------------------------------------------------------------------------
91// Transaction States
92// ---------------------------------------------------------------------------
93
94/// Top-level state of a TAP transaction.
95///
96/// These states represent the full lifecycle from initiation to terminal
97/// states. The FSM enforces that only valid transitions occur.
98#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
99pub enum TransactionState {
100    /// Transaction initiated — a Transfer or Payment has been received and
101    /// stored. The node must now decide how to respond (authorize, reject,
102    /// request more info, or wait for external input).
103    Received,
104
105    /// One or more counterparty policies must be satisfied before
106    /// authorization can proceed. The node is waiting for the external
107    /// system to gather and submit the required presentations or proofs.
108    PolicyRequired,
109
110    /// At least one agent has authorized but not all required agents have
111    /// done so yet. The transaction is waiting for remaining authorizations.
112    PartiallyAuthorized,
113
114    /// All required agents have authorized. The originator may now settle
115    /// the transaction on-chain. This is a decision point — the node must
116    /// decide whether to proceed with settlement.
117    ReadyToSettle,
118
119    /// The originator has sent a Settle message (with an on-chain
120    /// transaction reference). The transaction is considered complete.
121    Settled,
122
123    /// An agent has rejected the transaction. Terminal state.
124    Rejected,
125
126    /// A party has cancelled the transaction. Terminal state.
127    Cancelled,
128
129    /// A previously settled transaction has been reverted. Terminal state.
130    Reverted,
131}
132
133impl TransactionState {
134    /// Returns true if this is a terminal state (no further transitions).
135    pub fn is_terminal(&self) -> bool {
136        matches!(
137            self,
138            TransactionState::Rejected | TransactionState::Cancelled | TransactionState::Reverted
139        )
140    }
141
142    /// Returns true if this state requires an external decision before
143    /// the transaction can advance.
144    pub fn requires_decision(&self) -> bool {
145        matches!(
146            self,
147            TransactionState::Received
148                | TransactionState::PolicyRequired
149                | TransactionState::ReadyToSettle
150        )
151    }
152}
153
154impl fmt::Display for TransactionState {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        match self {
157            TransactionState::Received => write!(f, "received"),
158            TransactionState::PolicyRequired => write!(f, "policy_required"),
159            TransactionState::PartiallyAuthorized => write!(f, "partially_authorized"),
160            TransactionState::ReadyToSettle => write!(f, "ready_to_settle"),
161            TransactionState::Settled => write!(f, "settled"),
162            TransactionState::Rejected => write!(f, "rejected"),
163            TransactionState::Cancelled => write!(f, "cancelled"),
164            TransactionState::Reverted => write!(f, "reverted"),
165        }
166    }
167}
168
169impl std::str::FromStr for TransactionState {
170    type Err = String;
171
172    fn from_str(s: &str) -> Result<Self, Self::Err> {
173        match s {
174            "received" => Ok(TransactionState::Received),
175            "policy_required" => Ok(TransactionState::PolicyRequired),
176            "partially_authorized" => Ok(TransactionState::PartiallyAuthorized),
177            "ready_to_settle" => Ok(TransactionState::ReadyToSettle),
178            "settled" => Ok(TransactionState::Settled),
179            "rejected" => Ok(TransactionState::Rejected),
180            "cancelled" => Ok(TransactionState::Cancelled),
181            "reverted" => Ok(TransactionState::Reverted),
182            _ => Err(format!("Invalid transaction state: {}", s)),
183        }
184    }
185}
186
187// ---------------------------------------------------------------------------
188// Per-Agent States
189// ---------------------------------------------------------------------------
190
191/// Authorization state of an individual agent within a transaction.
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193pub enum AgentState {
194    /// Agent has been added to the transaction but has not yet responded.
195    Pending,
196
197    /// Agent has sent an Authorize message.
198    Authorized,
199
200    /// Agent has sent a Reject message.
201    Rejected,
202
203    /// Agent has been removed from the transaction.
204    Removed,
205}
206
207impl fmt::Display for AgentState {
208    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209        match self {
210            AgentState::Pending => write!(f, "pending"),
211            AgentState::Authorized => write!(f, "authorized"),
212            AgentState::Rejected => write!(f, "rejected"),
213            AgentState::Removed => write!(f, "removed"),
214        }
215    }
216}
217
218// ---------------------------------------------------------------------------
219// Events (incoming messages that drive transitions)
220// ---------------------------------------------------------------------------
221
222/// An event that can trigger a state transition in the FSM.
223///
224/// Each variant corresponds to a TAP message type that affects transaction
225/// state. Events carry only the data needed for the state transition, not
226/// the full message payload.
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub enum FsmEvent {
229    /// A new Transfer or Payment was received, initiating the transaction.
230    TransactionReceived {
231        /// DIDs of agents involved in this transaction.
232        agent_dids: Vec<String>,
233    },
234
235    /// An agent sent an Authorize message for this transaction.
236    AuthorizeReceived {
237        /// DID of the agent that authorized.
238        agent_did: String,
239        /// Optional settlement address provided by the agent.
240        settlement_address: Option<String>,
241        /// Optional expiry for this authorization.
242        expiry: Option<String>,
243    },
244
245    /// An agent sent a Reject message for this transaction.
246    RejectReceived {
247        /// DID of the agent that rejected.
248        agent_did: String,
249        /// Optional reason for rejection.
250        reason: Option<String>,
251    },
252
253    /// A party sent a Cancel message.
254    CancelReceived {
255        /// DID of the party that cancelled.
256        by_did: String,
257        /// Optional reason for cancellation.
258        reason: Option<String>,
259    },
260
261    /// A counterparty sent UpdatePolicies, indicating requirements that
262    /// must be fulfilled before they will authorize.
263    PoliciesReceived {
264        /// DID of the party that sent the policies.
265        from_did: String,
266    },
267
268    /// A Presentation was received satisfying (some) outstanding policies.
269    PresentationReceived {
270        /// DID of the party that sent the presentation.
271        from_did: String,
272    },
273
274    /// The originator sent a Settle message with an on-chain reference.
275    SettleReceived {
276        /// On-chain settlement identifier (CAIP-220).
277        settlement_id: Option<String>,
278        /// Actual amount settled.
279        amount: Option<String>,
280    },
281
282    /// A party sent a Revert message for a settled transaction.
283    RevertReceived {
284        /// DID of the party requesting revert.
285        by_did: String,
286        /// Reason for reversal.
287        reason: String,
288    },
289
290    /// New agents were added to the transaction (TAIP-5).
291    AgentsAdded {
292        /// DIDs of newly added agents.
293        agent_dids: Vec<String>,
294    },
295
296    /// An agent was removed from the transaction (TAIP-5).
297    AgentRemoved {
298        /// DID of the removed agent.
299        agent_did: String,
300    },
301}
302
303impl fmt::Display for FsmEvent {
304    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
305        match self {
306            FsmEvent::TransactionReceived { .. } => write!(f, "TransactionReceived"),
307            FsmEvent::AuthorizeReceived { agent_did, .. } => {
308                write!(f, "AuthorizeReceived({})", agent_did)
309            }
310            FsmEvent::RejectReceived { agent_did, .. } => {
311                write!(f, "RejectReceived({})", agent_did)
312            }
313            FsmEvent::CancelReceived { by_did, .. } => write!(f, "CancelReceived({})", by_did),
314            FsmEvent::PoliciesReceived { from_did } => {
315                write!(f, "PoliciesReceived({})", from_did)
316            }
317            FsmEvent::PresentationReceived { from_did } => {
318                write!(f, "PresentationReceived({})", from_did)
319            }
320            FsmEvent::SettleReceived { .. } => write!(f, "SettleReceived"),
321            FsmEvent::RevertReceived { by_did, .. } => write!(f, "RevertReceived({})", by_did),
322            FsmEvent::AgentsAdded { agent_dids } => {
323                write!(f, "AgentsAdded({})", agent_dids.join(", "))
324            }
325            FsmEvent::AgentRemoved { agent_did } => write!(f, "AgentRemoved({})", agent_did),
326        }
327    }
328}
329
330// ---------------------------------------------------------------------------
331// Decisions (what the external system must resolve)
332// ---------------------------------------------------------------------------
333
334/// A decision that an external system must make before the FSM can advance.
335///
336/// When the FSM reaches a decision point, it returns one of these variants
337/// describing the choices available. The external system (compliance engine,
338/// human operator, business rules) must call back with the chosen action.
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub enum Decision {
341    /// A new transaction was received. The external system must decide
342    /// how to respond.
343    ///
344    /// **Available actions:**
345    /// - Send `Authorize` to approve
346    /// - Send `Reject` to deny
347    /// - Send `UpdatePolicies` to request more information
348    /// - Send `RequestPresentation` to request credentials
349    /// - Do nothing (wait for more context)
350    AuthorizationRequired {
351        /// The transaction ID requiring a decision.
352        transaction_id: String,
353        /// DIDs of agents that need to make a decision.
354        pending_agents: Vec<String>,
355    },
356
357    /// Outstanding policies must be satisfied before authorization can
358    /// proceed. The external system must gather the required data and
359    /// submit it.
360    ///
361    /// **Available actions:**
362    /// - Send `Presentation` with requested credentials
363    /// - Send `ConfirmRelationship` to prove agent-party link
364    /// - Send `Reject` if policies cannot be satisfied
365    /// - Send `Cancel` to abort
366    PolicySatisfactionRequired {
367        /// The transaction ID.
368        transaction_id: String,
369        /// DID of the party that requested policies.
370        requested_by: String,
371    },
372
373    /// All agents have authorized. The originator must decide whether to
374    /// execute settlement on-chain and send a Settle message.
375    ///
376    /// **Available actions:**
377    /// - Execute on-chain settlement and send `Settle`
378    /// - Send `Cancel` if settlement should not proceed
379    SettlementRequired {
380        /// The transaction ID.
381        transaction_id: String,
382    },
383}
384
385impl fmt::Display for Decision {
386    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
387        match self {
388            Decision::AuthorizationRequired {
389                transaction_id,
390                pending_agents,
391            } => write!(
392                f,
393                "AuthorizationRequired(tx={}, agents={})",
394                transaction_id,
395                pending_agents.join(", ")
396            ),
397            Decision::PolicySatisfactionRequired {
398                transaction_id,
399                requested_by,
400            } => write!(
401                f,
402                "PolicySatisfactionRequired(tx={}, by={})",
403                transaction_id, requested_by
404            ),
405            Decision::SettlementRequired { transaction_id } => {
406                write!(f, "SettlementRequired(tx={})", transaction_id)
407            }
408        }
409    }
410}
411
412// ---------------------------------------------------------------------------
413// Transition result
414// ---------------------------------------------------------------------------
415
416/// The outcome of applying an event to the FSM.
417#[derive(Debug, Clone)]
418pub struct Transition {
419    /// The state before the transition.
420    pub from_state: TransactionState,
421    /// The state after the transition.
422    pub to_state: TransactionState,
423    /// The event that triggered this transition.
424    pub event: FsmEvent,
425    /// If the new state is a decision point, this describes the decision
426    /// that must be made by the external system.
427    pub decision: Option<Decision>,
428}
429
430// ---------------------------------------------------------------------------
431// Transaction FSM context
432// ---------------------------------------------------------------------------
433
434/// Error returned when an invalid transition is attempted.
435#[derive(Debug, Clone)]
436pub struct InvalidTransition {
437    pub current_state: TransactionState,
438    pub event: FsmEvent,
439    pub reason: String,
440}
441
442impl fmt::Display for InvalidTransition {
443    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
444        write!(
445            f,
446            "Invalid transition: cannot apply {} in state {} ({})",
447            self.event, self.current_state, self.reason
448        )
449    }
450}
451
452impl std::error::Error for InvalidTransition {}
453
454/// The in-memory state of a single transaction tracked by the FSM.
455///
456/// This struct holds the current state plus per-agent tracking. It is the
457/// core data structure manipulated by [`TransactionFsm::apply`].
458#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct TransactionContext {
460    /// Unique transaction identifier (the DIDComm message ID of the
461    /// initiating Transfer/Payment).
462    pub transaction_id: String,
463
464    /// Current top-level state.
465    pub state: TransactionState,
466
467    /// Per-agent authorization state keyed by agent DID.
468    pub agents: HashMap<String, AgentState>,
469
470    /// Whether outstanding policies have been received that must be
471    /// satisfied before authorization can proceed.
472    pub has_pending_policies: bool,
473}
474
475impl TransactionContext {
476    /// Create a new transaction context in the `Received` state.
477    pub fn new(transaction_id: String, agent_dids: Vec<String>) -> Self {
478        let agents = agent_dids
479            .into_iter()
480            .map(|did| (did, AgentState::Pending))
481            .collect();
482
483        Self {
484            transaction_id,
485            state: TransactionState::Received,
486            agents,
487            has_pending_policies: false,
488        }
489    }
490
491    /// Returns true if all tracked agents have authorized.
492    pub fn all_agents_authorized(&self) -> bool {
493        if self.agents.is_empty() {
494            return true;
495        }
496        self.agents
497            .values()
498            .filter(|s| **s != AgentState::Removed)
499            .all(|s| *s == AgentState::Authorized)
500    }
501
502    /// Returns the DIDs of agents still in `Pending` state.
503    pub fn pending_agents(&self) -> Vec<String> {
504        self.agents
505            .iter()
506            .filter(|(_, s)| **s == AgentState::Pending)
507            .map(|(did, _)| did.clone())
508            .collect()
509    }
510}
511
512// ---------------------------------------------------------------------------
513// The FSM engine
514// ---------------------------------------------------------------------------
515
516/// Pure-logic FSM engine for TAP transactions.
517///
518/// This struct contains no I/O — it operates only on [`TransactionContext`]
519/// and returns [`Transition`] values describing what happened and what
520/// decisions are needed. The caller (typically the `StandardTransactionProcessor`
521/// or a higher-level orchestrator) is responsible for:
522///
523/// 1. Persisting state changes
524/// 2. Publishing events
525/// 3. Presenting decisions to external systems
526/// 4. Sending response messages
527pub struct TransactionFsm;
528
529impl TransactionFsm {
530    /// Apply an event to a transaction context, producing a state transition.
531    ///
532    /// Returns `Ok(Transition)` on success, describing the state change and
533    /// any decision required. Returns `Err(InvalidTransition)` if the event
534    /// is not valid in the current state.
535    pub fn apply(
536        ctx: &mut TransactionContext,
537        event: FsmEvent,
538    ) -> Result<Transition, InvalidTransition> {
539        let from_state = ctx.state.clone();
540
541        // Terminal states accept no further events
542        if ctx.state.is_terminal() {
543            return Err(InvalidTransition {
544                current_state: from_state,
545                event,
546                reason: "transaction is in a terminal state".to_string(),
547            });
548        }
549
550        match (&ctx.state, &event) {
551            // ----- Initiation -----
552            (TransactionState::Received, FsmEvent::TransactionReceived { .. }) => {
553                // This is the initial setup — context was just created.
554                // Stay in Received; the decision is whether to authorize.
555                let decision = Some(Decision::AuthorizationRequired {
556                    transaction_id: ctx.transaction_id.clone(),
557                    pending_agents: ctx.pending_agents(),
558                });
559                Ok(Transition {
560                    from_state,
561                    to_state: ctx.state.clone(),
562                    event,
563                    decision,
564                })
565            }
566
567            // ----- Policy exchange -----
568            (
569                TransactionState::Received | TransactionState::PolicyRequired,
570                FsmEvent::PoliciesReceived { from_did },
571            ) => {
572                ctx.has_pending_policies = true;
573                ctx.state = TransactionState::PolicyRequired;
574                let decision = Some(Decision::PolicySatisfactionRequired {
575                    transaction_id: ctx.transaction_id.clone(),
576                    requested_by: from_did.clone(),
577                });
578                Ok(Transition {
579                    from_state,
580                    to_state: ctx.state.clone(),
581                    event,
582                    decision,
583                })
584            }
585
586            (TransactionState::PolicyRequired, FsmEvent::PresentationReceived { .. }) => {
587                // Presentation received — assume policies are satisfied for now.
588                // A real implementation would check if ALL policies are met.
589                ctx.has_pending_policies = false;
590                ctx.state = TransactionState::Received;
591                let decision = Some(Decision::AuthorizationRequired {
592                    transaction_id: ctx.transaction_id.clone(),
593                    pending_agents: ctx.pending_agents(),
594                });
595                Ok(Transition {
596                    from_state,
597                    to_state: ctx.state.clone(),
598                    event,
599                    decision,
600                })
601            }
602
603            // ----- Authorization -----
604            (
605                TransactionState::Received
606                | TransactionState::PartiallyAuthorized
607                | TransactionState::PolicyRequired,
608                FsmEvent::AuthorizeReceived { agent_did, .. },
609            ) => {
610                // Record the agent's authorization
611                if let Some(agent_state) = ctx.agents.get_mut(agent_did) {
612                    *agent_state = AgentState::Authorized;
613                }
614
615                // Determine new transaction state
616                if ctx.all_agents_authorized() {
617                    ctx.state = TransactionState::ReadyToSettle;
618                    let decision = Some(Decision::SettlementRequired {
619                        transaction_id: ctx.transaction_id.clone(),
620                    });
621                    Ok(Transition {
622                        from_state,
623                        to_state: ctx.state.clone(),
624                        event,
625                        decision,
626                    })
627                } else {
628                    ctx.state = TransactionState::PartiallyAuthorized;
629                    Ok(Transition {
630                        from_state,
631                        to_state: ctx.state.clone(),
632                        event,
633                        decision: None,
634                    })
635                }
636            }
637
638            // Authorization can also arrive in ReadyToSettle if a new agent
639            // was added after others already authorized.
640            (TransactionState::ReadyToSettle, FsmEvent::AuthorizeReceived { agent_did, .. }) => {
641                if let Some(agent_state) = ctx.agents.get_mut(agent_did) {
642                    *agent_state = AgentState::Authorized;
643                }
644                // Re-check if all are still authorized
645                if ctx.all_agents_authorized() {
646                    let decision = Some(Decision::SettlementRequired {
647                        transaction_id: ctx.transaction_id.clone(),
648                    });
649                    Ok(Transition {
650                        from_state,
651                        to_state: ctx.state.clone(),
652                        event,
653                        decision,
654                    })
655                } else {
656                    ctx.state = TransactionState::PartiallyAuthorized;
657                    Ok(Transition {
658                        from_state,
659                        to_state: ctx.state.clone(),
660                        event,
661                        decision: None,
662                    })
663                }
664            }
665
666            // ----- Rejection -----
667            (_, FsmEvent::RejectReceived { agent_did, .. }) => {
668                if let Some(agent_state) = ctx.agents.get_mut(agent_did) {
669                    *agent_state = AgentState::Rejected;
670                }
671                ctx.state = TransactionState::Rejected;
672                Ok(Transition {
673                    from_state,
674                    to_state: ctx.state.clone(),
675                    event,
676                    decision: None,
677                })
678            }
679
680            // ----- Cancellation -----
681            (_, FsmEvent::CancelReceived { .. }) => {
682                ctx.state = TransactionState::Cancelled;
683                Ok(Transition {
684                    from_state,
685                    to_state: ctx.state.clone(),
686                    event,
687                    decision: None,
688                })
689            }
690
691            // ----- Settlement -----
692            (TransactionState::ReadyToSettle, FsmEvent::SettleReceived { .. }) => {
693                ctx.state = TransactionState::Settled;
694                Ok(Transition {
695                    from_state,
696                    to_state: ctx.state.clone(),
697                    event,
698                    decision: None,
699                })
700            }
701
702            // ----- Revert -----
703            (TransactionState::Settled, FsmEvent::RevertReceived { .. }) => {
704                ctx.state = TransactionState::Reverted;
705                Ok(Transition {
706                    from_state,
707                    to_state: ctx.state.clone(),
708                    event,
709                    decision: None,
710                })
711            }
712
713            // ----- Agent management (TAIP-5) -----
714            (_, FsmEvent::AgentsAdded { agent_dids }) => {
715                for did in agent_dids {
716                    ctx.agents.entry(did.clone()).or_insert(AgentState::Pending);
717                }
718                // Adding agents may move us out of ReadyToSettle if new
719                // agents are pending.
720                if from_state == TransactionState::ReadyToSettle && !ctx.all_agents_authorized() {
721                    ctx.state = TransactionState::PartiallyAuthorized;
722                }
723                Ok(Transition {
724                    from_state,
725                    to_state: ctx.state.clone(),
726                    event,
727                    decision: None,
728                })
729            }
730
731            (_, FsmEvent::AgentRemoved { agent_did }) => {
732                if let Some(agent_state) = ctx.agents.get_mut(agent_did) {
733                    *agent_state = AgentState::Removed;
734                }
735                // Removing an agent may make all remaining agents authorized.
736                if matches!(
737                    ctx.state,
738                    TransactionState::PartiallyAuthorized | TransactionState::Received
739                ) && ctx.all_agents_authorized()
740                {
741                    ctx.state = TransactionState::ReadyToSettle;
742                    let decision = Some(Decision::SettlementRequired {
743                        transaction_id: ctx.transaction_id.clone(),
744                    });
745                    return Ok(Transition {
746                        from_state,
747                        to_state: ctx.state.clone(),
748                        event,
749                        decision,
750                    });
751                }
752                Ok(Transition {
753                    from_state,
754                    to_state: ctx.state.clone(),
755                    event,
756                    decision: None,
757                })
758            }
759
760            // ----- Invalid transitions -----
761            _ => Err(InvalidTransition {
762                current_state: from_state,
763                event: event.clone(),
764                reason: format!("event {} is not valid in state {}", event, ctx.state),
765            }),
766        }
767    }
768
769    /// Returns all valid events for a given state (for documentation/UI).
770    pub fn valid_events(state: &TransactionState) -> Vec<&'static str> {
771        match state {
772            TransactionState::Received => vec![
773                "TransactionReceived",
774                "AuthorizeReceived",
775                "RejectReceived",
776                "CancelReceived",
777                "PoliciesReceived",
778                "AgentsAdded",
779                "AgentRemoved",
780            ],
781            TransactionState::PolicyRequired => vec![
782                "PresentationReceived",
783                "PoliciesReceived",
784                "AuthorizeReceived",
785                "RejectReceived",
786                "CancelReceived",
787                "AgentsAdded",
788                "AgentRemoved",
789            ],
790            TransactionState::PartiallyAuthorized => vec![
791                "AuthorizeReceived",
792                "RejectReceived",
793                "CancelReceived",
794                "AgentsAdded",
795                "AgentRemoved",
796            ],
797            TransactionState::ReadyToSettle => vec![
798                "SettleReceived",
799                "AuthorizeReceived",
800                "RejectReceived",
801                "CancelReceived",
802                "AgentsAdded",
803                "AgentRemoved",
804            ],
805            TransactionState::Settled => vec!["RevertReceived"],
806            TransactionState::Rejected
807            | TransactionState::Cancelled
808            | TransactionState::Reverted => {
809                vec![]
810            }
811        }
812    }
813}
814
815// ---------------------------------------------------------------------------
816// Decision handler configuration
817// ---------------------------------------------------------------------------
818
819/// Controls how the node handles decision points during transaction
820/// processing.
821///
822/// This enum is set on [`NodeConfig`] and determines which
823/// [`DecisionHandler`] implementation the `StandardTransactionProcessor`
824/// uses at runtime.
825#[derive(Debug, Clone, Default)]
826pub enum DecisionMode {
827    /// Automatically approve all decisions — the node will immediately
828    /// send Authorize messages for registered agents and Settle when all
829    /// agents have authorized. This is the current default behavior and
830    /// is suitable for testing or fully-automated deployments.
831    #[default]
832    AutoApprove,
833
834    /// Publish each decision as a [`NodeEvent::DecisionRequired`] on the
835    /// event bus. No automatic action is taken — an external subscriber
836    /// (compliance engine, human operator UI, business rules engine) must
837    /// listen for these events and call back into the node to advance the
838    /// transaction.
839    EventBus,
840
841    /// Use a custom decision handler provided by the caller.
842    Custom(Arc<dyn DecisionHandler>),
843}
844
845// ---------------------------------------------------------------------------
846// Decision handler trait
847// ---------------------------------------------------------------------------
848
849/// Trait for handling FSM decision points.
850///
851/// When the FSM reaches a state that requires an external decision
852/// (e.g., whether to authorize a new transfer), the
853/// `StandardTransactionProcessor` calls the configured `DecisionHandler`.
854///
855/// Implementations can auto-approve, publish to an event bus, call out
856/// to a compliance API, present a UI to a human operator, etc.
857#[async_trait]
858pub trait DecisionHandler: Send + Sync + fmt::Debug {
859    /// Called when the FSM produces a [`Decision`].
860    ///
861    /// The handler receives the full [`TransactionContext`] (current state,
862    /// per-agent status) and the [`Decision`] describing what needs to be
863    /// resolved.
864    ///
865    /// Implementations that auto-resolve should return the same `Decision`
866    /// back. Implementations that defer to external systems should publish
867    /// the decision and return it for auditing.
868    async fn handle_decision(&self, ctx: &TransactionContext, decision: &Decision);
869}
870
871// ---------------------------------------------------------------------------
872// Built-in: AutoApproveHandler
873// ---------------------------------------------------------------------------
874
875/// Decision handler that automatically approves all decisions.
876///
877/// - `AuthorizationRequired` → queues Authorize messages for all registered
878///   agents (the actual sending is done by the processor, not this handler)
879/// - `SettlementRequired` → allows the processor to send Settle
880/// - `PolicySatisfactionRequired` → logged, no action (policies are not
881///   auto-satisfiable)
882///
883/// This preserves the existing tap-node behavior where registered agents
884/// are auto-authorized and settlement is automatic.
885#[derive(Debug)]
886pub struct AutoApproveHandler;
887
888#[async_trait]
889impl DecisionHandler for AutoApproveHandler {
890    async fn handle_decision(&self, _ctx: &TransactionContext, decision: &Decision) {
891        log::debug!("AutoApproveHandler: auto-resolving {}", decision);
892    }
893}
894
895// ---------------------------------------------------------------------------
896// Built-in: LogOnlyHandler
897// ---------------------------------------------------------------------------
898
899/// Decision handler that only logs decisions without taking action.
900///
901/// Useful for monitoring/observability when an external system handles
902/// decisions through the event bus channel subscription instead of the
903/// `DecisionHandler` trait.
904#[derive(Debug)]
905pub struct LogOnlyHandler;
906
907#[async_trait]
908impl DecisionHandler for LogOnlyHandler {
909    async fn handle_decision(&self, ctx: &TransactionContext, decision: &Decision) {
910        log::info!(
911            "Decision required for transaction {} (state={}): {}",
912            ctx.transaction_id,
913            ctx.state,
914            decision
915        );
916    }
917}
918
919#[cfg(test)]
920mod tests {
921    use super::*;
922
923    fn make_ctx(agents: &[&str]) -> TransactionContext {
924        TransactionContext::new(
925            "tx-001".to_string(),
926            agents.iter().map(|s| s.to_string()).collect(),
927        )
928    }
929
930    #[test]
931    fn test_happy_path_single_agent() {
932        let mut ctx = make_ctx(&["did:example:compliance"]);
933        assert_eq!(ctx.state, TransactionState::Received);
934
935        // Receive transaction
936        let t = TransactionFsm::apply(
937            &mut ctx,
938            FsmEvent::TransactionReceived {
939                agent_dids: vec!["did:example:compliance".to_string()],
940            },
941        )
942        .unwrap();
943        assert_eq!(t.to_state, TransactionState::Received);
944        assert!(t.decision.is_some());
945        assert!(matches!(
946            t.decision.unwrap(),
947            Decision::AuthorizationRequired { .. }
948        ));
949
950        // Agent authorizes
951        let t = TransactionFsm::apply(
952            &mut ctx,
953            FsmEvent::AuthorizeReceived {
954                agent_did: "did:example:compliance".to_string(),
955                settlement_address: None,
956                expiry: None,
957            },
958        )
959        .unwrap();
960        assert_eq!(t.to_state, TransactionState::ReadyToSettle);
961        assert!(matches!(
962            t.decision.unwrap(),
963            Decision::SettlementRequired { .. }
964        ));
965
966        // Settle
967        let t = TransactionFsm::apply(
968            &mut ctx,
969            FsmEvent::SettleReceived {
970                settlement_id: Some("eip155:1:tx/0xabc".to_string()),
971                amount: None,
972            },
973        )
974        .unwrap();
975        assert_eq!(t.to_state, TransactionState::Settled);
976        assert!(t.decision.is_none());
977    }
978
979    #[test]
980    fn test_happy_path_multi_agent() {
981        let mut ctx = make_ctx(&["did:example:a", "did:example:b"]);
982
983        // First agent authorizes
984        let t = TransactionFsm::apply(
985            &mut ctx,
986            FsmEvent::AuthorizeReceived {
987                agent_did: "did:example:a".to_string(),
988                settlement_address: None,
989                expiry: None,
990            },
991        )
992        .unwrap();
993        assert_eq!(t.to_state, TransactionState::PartiallyAuthorized);
994        assert!(t.decision.is_none());
995
996        // Second agent authorizes
997        let t = TransactionFsm::apply(
998            &mut ctx,
999            FsmEvent::AuthorizeReceived {
1000                agent_did: "did:example:b".to_string(),
1001                settlement_address: None,
1002                expiry: None,
1003            },
1004        )
1005        .unwrap();
1006        assert_eq!(t.to_state, TransactionState::ReadyToSettle);
1007        assert!(matches!(
1008            t.decision.unwrap(),
1009            Decision::SettlementRequired { .. }
1010        ));
1011    }
1012
1013    #[test]
1014    fn test_rejection() {
1015        let mut ctx = make_ctx(&["did:example:a"]);
1016
1017        let t = TransactionFsm::apply(
1018            &mut ctx,
1019            FsmEvent::RejectReceived {
1020                agent_did: "did:example:a".to_string(),
1021                reason: Some("sanctions screening failed".to_string()),
1022            },
1023        )
1024        .unwrap();
1025        assert_eq!(t.to_state, TransactionState::Rejected);
1026        assert!(ctx.state.is_terminal());
1027    }
1028
1029    #[test]
1030    fn test_cancellation() {
1031        let mut ctx = make_ctx(&["did:example:a"]);
1032
1033        let t = TransactionFsm::apply(
1034            &mut ctx,
1035            FsmEvent::CancelReceived {
1036                by_did: "did:example:originator".to_string(),
1037                reason: None,
1038            },
1039        )
1040        .unwrap();
1041        assert_eq!(t.to_state, TransactionState::Cancelled);
1042        assert!(ctx.state.is_terminal());
1043    }
1044
1045    #[test]
1046    fn test_policy_flow() {
1047        let mut ctx = make_ctx(&["did:example:a"]);
1048
1049        // Counterparty sends policies
1050        let t = TransactionFsm::apply(
1051            &mut ctx,
1052            FsmEvent::PoliciesReceived {
1053                from_did: "did:example:beneficiary-vasp".to_string(),
1054            },
1055        )
1056        .unwrap();
1057        assert_eq!(t.to_state, TransactionState::PolicyRequired);
1058        assert!(matches!(
1059            t.decision.unwrap(),
1060            Decision::PolicySatisfactionRequired { .. }
1061        ));
1062
1063        // We send a presentation satisfying the policies
1064        let t = TransactionFsm::apply(
1065            &mut ctx,
1066            FsmEvent::PresentationReceived {
1067                from_did: "did:example:originator-vasp".to_string(),
1068            },
1069        )
1070        .unwrap();
1071        assert_eq!(t.to_state, TransactionState::Received);
1072        assert!(matches!(
1073            t.decision.unwrap(),
1074            Decision::AuthorizationRequired { .. }
1075        ));
1076
1077        // Now agent can authorize
1078        let t = TransactionFsm::apply(
1079            &mut ctx,
1080            FsmEvent::AuthorizeReceived {
1081                agent_did: "did:example:a".to_string(),
1082                settlement_address: None,
1083                expiry: None,
1084            },
1085        )
1086        .unwrap();
1087        assert_eq!(t.to_state, TransactionState::ReadyToSettle);
1088    }
1089
1090    #[test]
1091    fn test_revert() {
1092        let mut ctx = make_ctx(&["did:example:a"]);
1093
1094        // Authorize → Settle → Revert
1095        TransactionFsm::apply(
1096            &mut ctx,
1097            FsmEvent::AuthorizeReceived {
1098                agent_did: "did:example:a".to_string(),
1099                settlement_address: None,
1100                expiry: None,
1101            },
1102        )
1103        .unwrap();
1104
1105        TransactionFsm::apply(
1106            &mut ctx,
1107            FsmEvent::SettleReceived {
1108                settlement_id: None,
1109                amount: None,
1110            },
1111        )
1112        .unwrap();
1113
1114        let t = TransactionFsm::apply(
1115            &mut ctx,
1116            FsmEvent::RevertReceived {
1117                by_did: "did:example:beneficiary".to_string(),
1118                reason: "incorrect amount".to_string(),
1119            },
1120        )
1121        .unwrap();
1122        assert_eq!(t.to_state, TransactionState::Reverted);
1123        assert!(ctx.state.is_terminal());
1124    }
1125
1126    #[test]
1127    fn test_terminal_state_rejects_events() {
1128        let mut ctx = make_ctx(&["did:example:a"]);
1129        ctx.state = TransactionState::Rejected;
1130
1131        let result = TransactionFsm::apply(
1132            &mut ctx,
1133            FsmEvent::AuthorizeReceived {
1134                agent_did: "did:example:a".to_string(),
1135                settlement_address: None,
1136                expiry: None,
1137            },
1138        );
1139        assert!(result.is_err());
1140    }
1141
1142    #[test]
1143    fn test_settle_only_from_ready_to_settle() {
1144        let mut ctx = make_ctx(&["did:example:a"]);
1145        // Still in Received state, try to settle
1146        let result = TransactionFsm::apply(
1147            &mut ctx,
1148            FsmEvent::SettleReceived {
1149                settlement_id: None,
1150                amount: None,
1151            },
1152        );
1153        assert!(result.is_err());
1154    }
1155
1156    #[test]
1157    fn test_add_agents_blocks_settlement() {
1158        let mut ctx = make_ctx(&["did:example:a"]);
1159
1160        // Authorize the first agent
1161        TransactionFsm::apply(
1162            &mut ctx,
1163            FsmEvent::AuthorizeReceived {
1164                agent_did: "did:example:a".to_string(),
1165                settlement_address: None,
1166                expiry: None,
1167            },
1168        )
1169        .unwrap();
1170        assert_eq!(ctx.state, TransactionState::ReadyToSettle);
1171
1172        // Add a new agent — should move back to PartiallyAuthorized
1173        let t = TransactionFsm::apply(
1174            &mut ctx,
1175            FsmEvent::AgentsAdded {
1176                agent_dids: vec!["did:example:b".to_string()],
1177            },
1178        )
1179        .unwrap();
1180        assert_eq!(t.to_state, TransactionState::PartiallyAuthorized);
1181    }
1182
1183    #[test]
1184    fn test_remove_agent_enables_settlement() {
1185        let mut ctx = make_ctx(&["did:example:a", "did:example:b"]);
1186
1187        // Only authorize agent a
1188        TransactionFsm::apply(
1189            &mut ctx,
1190            FsmEvent::AuthorizeReceived {
1191                agent_did: "did:example:a".to_string(),
1192                settlement_address: None,
1193                expiry: None,
1194            },
1195        )
1196        .unwrap();
1197        assert_eq!(ctx.state, TransactionState::PartiallyAuthorized);
1198
1199        // Remove agent b — now all remaining agents are authorized
1200        let t = TransactionFsm::apply(
1201            &mut ctx,
1202            FsmEvent::AgentRemoved {
1203                agent_did: "did:example:b".to_string(),
1204            },
1205        )
1206        .unwrap();
1207        assert_eq!(t.to_state, TransactionState::ReadyToSettle);
1208        assert!(matches!(
1209            t.decision.unwrap(),
1210            Decision::SettlementRequired { .. }
1211        ));
1212    }
1213
1214    #[test]
1215    fn test_no_agents_goes_straight_to_ready() {
1216        let mut ctx = make_ctx(&[]);
1217
1218        // With no agents, any authorize immediately reaches ReadyToSettle.
1219        // Actually, with no agents all_agents_authorized() is true from the
1220        // start. A TransactionReceived should reflect this.
1221        let t = TransactionFsm::apply(
1222            &mut ctx,
1223            FsmEvent::TransactionReceived { agent_dids: vec![] },
1224        )
1225        .unwrap();
1226        // Still Received — the decision is AuthorizationRequired even with
1227        // no agents, because the *parties* themselves may still need to decide.
1228        assert_eq!(t.to_state, TransactionState::Received);
1229    }
1230
1231    #[test]
1232    fn test_valid_events() {
1233        let events = TransactionFsm::valid_events(&TransactionState::Received);
1234        assert!(events.contains(&"AuthorizeReceived"));
1235        assert!(events.contains(&"PoliciesReceived"));
1236
1237        let events = TransactionFsm::valid_events(&TransactionState::Settled);
1238        assert_eq!(events, vec!["RevertReceived"]);
1239
1240        let events = TransactionFsm::valid_events(&TransactionState::Rejected);
1241        assert!(events.is_empty());
1242    }
1243
1244    #[test]
1245    fn test_display_implementations() {
1246        assert_eq!(TransactionState::Received.to_string(), "received");
1247        assert_eq!(
1248            TransactionState::PolicyRequired.to_string(),
1249            "policy_required"
1250        );
1251        assert_eq!(
1252            TransactionState::PartiallyAuthorized.to_string(),
1253            "partially_authorized"
1254        );
1255        assert_eq!(
1256            TransactionState::ReadyToSettle.to_string(),
1257            "ready_to_settle"
1258        );
1259        assert_eq!(TransactionState::Settled.to_string(), "settled");
1260        assert_eq!(TransactionState::Rejected.to_string(), "rejected");
1261        assert_eq!(TransactionState::Cancelled.to_string(), "cancelled");
1262        assert_eq!(TransactionState::Reverted.to_string(), "reverted");
1263    }
1264
1265    #[test]
1266    fn test_agent_state_display() {
1267        assert_eq!(AgentState::Pending.to_string(), "pending");
1268        assert_eq!(AgentState::Authorized.to_string(), "authorized");
1269        assert_eq!(AgentState::Rejected.to_string(), "rejected");
1270        assert_eq!(AgentState::Removed.to_string(), "removed");
1271    }
1272
1273    #[test]
1274    fn test_reject_during_partial_authorization() {
1275        let mut ctx = make_ctx(&["did:example:a", "did:example:b"]);
1276
1277        // Agent a authorizes
1278        TransactionFsm::apply(
1279            &mut ctx,
1280            FsmEvent::AuthorizeReceived {
1281                agent_did: "did:example:a".to_string(),
1282                settlement_address: None,
1283                expiry: None,
1284            },
1285        )
1286        .unwrap();
1287        assert_eq!(ctx.state, TransactionState::PartiallyAuthorized);
1288
1289        // Agent b rejects
1290        let t = TransactionFsm::apply(
1291            &mut ctx,
1292            FsmEvent::RejectReceived {
1293                agent_did: "did:example:b".to_string(),
1294                reason: Some("compliance failure".to_string()),
1295            },
1296        )
1297        .unwrap();
1298        assert_eq!(t.to_state, TransactionState::Rejected);
1299    }
1300
1301    #[test]
1302    fn test_cancel_during_ready_to_settle() {
1303        let mut ctx = make_ctx(&["did:example:a"]);
1304
1305        TransactionFsm::apply(
1306            &mut ctx,
1307            FsmEvent::AuthorizeReceived {
1308                agent_did: "did:example:a".to_string(),
1309                settlement_address: None,
1310                expiry: None,
1311            },
1312        )
1313        .unwrap();
1314        assert_eq!(ctx.state, TransactionState::ReadyToSettle);
1315
1316        // Cancel even though ready to settle
1317        let t = TransactionFsm::apply(
1318            &mut ctx,
1319            FsmEvent::CancelReceived {
1320                by_did: "did:example:originator".to_string(),
1321                reason: Some("changed mind".to_string()),
1322            },
1323        )
1324        .unwrap();
1325        assert_eq!(t.to_state, TransactionState::Cancelled);
1326    }
1327
1328    #[test]
1329    fn test_authorize_from_policy_required() {
1330        let mut ctx = make_ctx(&["did:example:a"]);
1331
1332        // Receive policies
1333        TransactionFsm::apply(
1334            &mut ctx,
1335            FsmEvent::PoliciesReceived {
1336                from_did: "did:example:b".to_string(),
1337            },
1338        )
1339        .unwrap();
1340        assert_eq!(ctx.state, TransactionState::PolicyRequired);
1341
1342        // Agent can still authorize even in PolicyRequired state
1343        // (they may have already satisfied the policies externally)
1344        let t = TransactionFsm::apply(
1345            &mut ctx,
1346            FsmEvent::AuthorizeReceived {
1347                agent_did: "did:example:a".to_string(),
1348                settlement_address: None,
1349                expiry: None,
1350            },
1351        )
1352        .unwrap();
1353        assert_eq!(t.to_state, TransactionState::ReadyToSettle);
1354    }
1355}