Repository: farcaster-project/farcaster-node
Pull request #761 (2022-11-08 15:09:10)

Add richer data to the progress messages
There are now three possible progress messages that a swap can issue. I message is just a string containing information. A transition goes from an old state to a new state. A state update updates the current state. Farcasterd only keeps the latest state update around, any previous state updates are discarded.

The progress now serves additional information about the confirmation status of various transactions during a swap. Additionally, it provides the block height of a running swap daemon.

I can be pretty flexible with the provided information here, so please let me know what additions / changes / re-designs you want.

Sample Output:

```
> swap1-cli progress 0xaf373e2400556909ab5b90959242979ac6bb05e0c48888d24533095fc7ead6ab                          

progress:
  - message: Proposing to take swap 0xbad3…3565 to Maker remote peer
  - transition:
      old_state: StartA
      new_state: CommitA
  - transition:
      old_state: CommitA
      new_state: RevealA
  - transition:
      old_state: RevealA
      new_state:
        RefundSigA:
          arb_block_height: 2405384
          acc_block_height: 1219189
          arb_locked: false
          acc_locked: false
          buy_published: false
          cancel_seen: false
          refund_seen: false
          overfunded: false
          arb_lock_confirmations: ~
          acc_lock_confirmations: ~
          blocks_until_cancel_possible: ~
          cancel_confirmations: ~
          blocks_until_punish_possible: ~
          blocks_until_safe_buy: ~
  - StateUpdate:
      RefundSigA:
        arb_block_height: 2405386
        acc_block_height: 1219192
        arb_locked: true
        acc_locked: true
        buy_published: true
        cancel_seen: false
        refund_seen: false
        overfunded: false
        arb_lock_confirmations: 2
        acc_lock_confirmations: 1
        blocks_until_cancel_possible: 2
        cancel_confirmations: ~
        blocks_until_punish_possible: ~
        blocks_until_safe_buy: 0
  - transition:
      old_state:
        RefundSigA:
          arb_block_height: 2405386
          acc_block_height: 1219192
          arb_locked: true
          acc_locked: true
          buy_published: true
          cancel_seen: false
          refund_seen: false
          overfunded: false
          arb_lock_confirmations: 2
          acc_lock_confirmations: 1
          blocks_until_cancel_possible: 2
          cancel_confirmations: ~
          blocks_until_punish_possible: ~
          blocks_until_safe_buy: 0
      new_state:
        FinishA: SuccessSwap
```


Patch:
diff --git a/src/bus/info.rs b/src/bus/info.rs
index 04302589..872215ae 100644
--- a/src/bus/info.rs
+++ b/src/bus/info.rs
@@ -15,9 +15,11 @@ use crate::bus::{
 };
 use crate::cli::OfferSelector;
 use crate::farcasterd::stats::Stats;
+use crate::swapd::StateReport;
 use crate::syncerd::runtime::SyncerdTask;
 
 use super::ctl::FundingInfo;
+use super::StateTransition;
 
 #[derive(Clone, Debug, Display, From, NetworkEncode, NetworkDecode)]
 #[non_exhaustive]
@@ -287,7 +289,6 @@ pub struct SwapInfo {
     #[serde_as(as = "Option<DisplayFromStr>")]
     pub connection: Option<NodeAddr>,
     pub connected: bool,
-    pub state: String,
     #[serde_as(as = "DurationSeconds")]
     pub uptime: Duration,
     pub since: u64,
@@ -295,6 +296,7 @@ pub struct SwapInfo {
     pub local_trade_role: TradeRole,
     pub local_swap_role: SwapRole,
     pub connected_counterparty_node_id: Option<NodeId>,
+    pub state: StateReport,
 }
 
 #[cfg_attr(feature = "serde", serde_as)]
@@ -332,8 +334,10 @@ pub struct FundingInfos {
 pub enum ProgressEvent {
     #[serde(rename = "message")]
     Message(String),
+    #[serde(rename = "update")]
+    StateUpdate(StateReport),
     #[serde(rename = "transition")]
-    StateTransition(String),
+    StateTransition(StateTransition),
     #[serde(rename = "success")]
     Success(OptionDetails),
     #[serde(rename = "failure")]
diff --git a/src/bus/types.rs b/src/bus/types.rs
index 8386f806..19e8b607 100644
--- a/src/bus/types.rs
+++ b/src/bus/types.rs
@@ -10,6 +10,8 @@ use internet2::addr::NodeId;
 use microservices::rpc;
 use strict_encoding::{NetworkDecode, NetworkEncode};
 
+use crate::swapd::StateReport;
+
 #[derive(Clone, PartialEq, Eq, Debug, Display, NetworkEncode, NetworkDecode)]
 #[display("{swap_id}, {public_offer}")]
 #[cfg_attr(
@@ -71,28 +73,41 @@ pub struct OfferStatusPair {
 #[cfg(feature = "serde")]
 impl ToYamlString for OfferStatusPair {}
 
-#[derive(Clone, Debug, Eq, PartialEq, Display, NetworkEncode, NetworkDecode)]
+#[derive(Clone, Debug, Eq, PartialEq, Hash, Display, NetworkEncode, NetworkDecode)]
 #[cfg_attr(
     feature = "serde",
     derive(Serialize, Deserialize),
     serde(crate = "serde_crate")
 )]
 pub enum Outcome {
-    #[display("Success(Swapped)")]
-    Buy,
-    #[display("Failure(Refunded)")]
-    Refund,
-    #[display("Failure(Punished)")]
-    Punish,
-    #[display("Failure(Aborted)")]
-    Abort,
+    #[display("Success Swap")]
+    SuccessSwap,
+    #[display("Failure Refund")]
+    FailureRefund,
+    #[display("Failure Punish")]
+    FailurePunish,
+    #[display("Failure Abort")]
+    FailureAbort,
 }
 
 #[derive(Clone, Debug, Display, NetworkEncode, NetworkDecode)]
 #[display(inner)]
 pub enum Progress {
     Message(String),
-    StateTransition(String),
+    StateUpdate(StateReport),
+    StateTransition(StateTransition),
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Display, NetworkEncode, NetworkDecode)]
+#[cfg_attr(
+    feature = "serde",
+    derive(Serialize, Deserialize),
+    serde(crate = "serde_crate")
+)]
+#[display("State transition: {old_state} -> {new_state}")]
+pub struct StateTransition {
+    pub old_state: StateReport,
+    pub new_state: StateReport,
 }
 
 #[derive(Wrapper, Clone, PartialEq, Eq, Debug, From, Default, NetworkEncode, NetworkDecode)]
diff --git a/src/databased/runtime.rs b/src/databased/runtime.rs
index 143b57ad..4189177c 100644
--- a/src/databased/runtime.rs
+++ b/src/databased/runtime.rs
@@ -747,7 +747,7 @@ fn test_lmdb_state() {
     assert_eq!(offer_1, offers_retrieved[0].offer);
 
     database
-        .set_offer_status(&offer_1, &OfferStatus::Ended(Outcome::Buy))
+        .set_offer_status(&offer_1, &OfferStatus::Ended(Outcome::SuccessSwap))
         .unwrap();
     let offers_retrieved = database.get_offers(OfferStatusSelector::Ended).unwrap();
     assert_eq!(offer_1, offers_retrieved[0].offer);
@@ -758,7 +758,7 @@ fn test_lmdb_state() {
     let offers_retrieved = database.get_offers(OfferStatusSelector::All).unwrap();
     let status_1 = OfferStatusPair {
         offer: offer_1,
-        status: OfferStatus::Ended(Outcome::Buy),
+        status: OfferStatus::Ended(Outcome::SuccessSwap),
     };
     let status_2 = OfferStatusPair {
         offer: offer_2,
diff --git a/src/farcasterd/runtime.rs b/src/farcasterd/runtime.rs
index fa7cefc0..c9e5435d 100644
--- a/src/farcasterd/runtime.rs
+++ b/src/farcasterd/runtime.rs
@@ -367,6 +367,14 @@ impl Runtime {
                 let queue = self.progress.get_mut(&source).expect("checked/added above");
                 let prog = match event {
                     CtlMsg::Progress(p) => {
+                        // Replace the latest state update message in the queue
+                        if let Progress::StateUpdate(_) = p {
+                            if let Some(ProgressStack::Progress(Progress::StateUpdate(_))) =
+                                queue.back()
+                            {
+                                queue.pop_back();
+                            }
+                        }
                         (ProgressStack::Progress(p.clone()), InfoMsg::Progress(p))
                     }
                     CtlMsg::Success(s) => (ProgressStack::Success(s.clone()), InfoMsg::Success(s)),
@@ -500,7 +508,12 @@ impl Runtime {
                             ProgressStack::Progress(Progress::Message(m)) => {
                                 swap_progress
                                     .progress
-                                    .push(ProgressEvent::Message(m.clone()));
+                                    .push(ProgressEvent::Message(m.to_string()));
+                            }
+                            ProgressStack::Progress(Progress::StateUpdate(s)) => {
+                                swap_progress
+                                    .progress
+                                    .push(ProgressEvent::StateUpdate(s.clone()));
                             }
                             ProgressStack::Progress(Progress::StateTransition(t)) => {
                                 swap_progress
diff --git a/src/farcasterd/stats.rs b/src/farcasterd/stats.rs
index 5419b8e8..5db56e77 100644
--- a/src/farcasterd/stats.rs
+++ b/src/farcasterd/stats.rs
@@ -30,10 +30,10 @@ pub struct Stats {
 impl Stats {
     pub fn incr_outcome(&mut self, outcome: &Outcome) {
         match outcome {
-            Outcome::Buy => self.success += 1,
-            Outcome::Refund => self.refund += 1,
-            Outcome::Punish => self.punish += 1,
-            Outcome::Abort => self.abort += 1,
+            Outcome::SuccessSwap => self.success += 1,
+            Outcome::FailureRefund => self.refund += 1,
+            Outcome::FailurePunish => self.punish += 1,
+            Outcome::FailureAbort => self.abort += 1,
         };
     }
 
diff --git a/src/farcasterd/trade_state_machine.rs b/src/farcasterd/trade_state_machine.rs
index 7714a0a0..8032a6fb 100644
--- a/src/farcasterd/trade_state_machine.rs
+++ b/src/farcasterd/trade_state_machine.rs
@@ -1540,16 +1540,16 @@ fn attempt_transition_to_end(
             runtime.clean_up_after_swap(&swap_id, event.endpoints)?;
             runtime.stats.incr_outcome(&outcome);
             match outcome {
-                Outcome::Buy => {
+                Outcome::SuccessSwap => {
                     debug!("Success on swap {}", swap_id);
                 }
-                Outcome::Refund => {
+                Outcome::FailureRefund => {
                     warn!("Refund on swap {}", swap_id);
                 }
-                Outcome::Punish => {
+                Outcome::FailurePunish => {
                     warn!("Punish on swap {}", swap_id);
                 }
-                Outcome::Abort => {
+                Outcome::FailureAbort => {
                     warn!("Aborted swap {}", swap_id);
                 }
             }
diff --git a/src/grpcd/proto/farcaster.proto b/src/grpcd/proto/farcaster.proto
index e58b6171..fd8efdfc 100644
--- a/src/grpcd/proto/farcaster.proto
+++ b/src/grpcd/proto/farcaster.proto
@@ -187,7 +187,141 @@ message ProgressRequest {
 
 message ProgressResponse {
     uint32 id = 1;
-    repeated string progress = 2;
+    repeated Progress progress = 2;
+}
+
+message Progress {
+    oneof progress {
+        string message = 1;
+        State state_update = 2;
+        StateTransition state_transition = 3;
+        string failure = 4;
+        string success = 5;
+    }
+}
+
+message StateTransition {
+    State old_state = 1;
+    State new_state = 2;
+}
+
+message State {
+    oneof state {
+        StartA start_a = 1;
+        CommitA commit_a = 2;
+        RevealA reveal_a = 3;
+        RefundSigA refund_sig_a = 4;
+        FinishA finish_a = 5;
+        StartB start_b = 6;
+        CommitB commit_b = 7;
+        RevealB reveal_b = 8;
+        CoreArbB core_arb_b = 9;
+        BuySigB buy_sig_b = 10;
+        FinishB finish_b = 11;
+    }
+}
+
+message StartA {}
+message CommitA {}
+message RevealA {}
+message FinishA {
+    Outcome outcome = 1;
+}
+message StartB {}
+message CommitB {}
+message RevealB {}
+message FinishB {
+    Outcome outcome = 1;
+}
+
+message RefundSigA {
+    uint64 arb_block_height = 1;
+    uint64 acc_block_height = 2;
+    bool arb_locked = 3;
+    bool acc_locked = 4;
+    bool buy_published = 5;
+    bool cancel_seen = 6;
+    bool refund_seen = 7;
+    bool overfunded = 8;
+    oneof arb_lock_confirmations {
+        uint32 arb_confs = 9;
+    }
+    oneof acc_lock_confirmations {
+        uint32 acc_confs = 10;
+    }
+    oneof blocks_until_cancel_possible {
+        int64 cancel_blocks = 11;
+    }
+    oneof cancel_confirmations {
+        uint32 cancel_confs = 12;
+    }
+    oneof blocks_until_punish_possible {
+        int64 punish_blocks = 13;
+    }
+    oneof blocks_until_safe_buy {
+        uint32 buy_blocks = 14;
+    }
+}
+
+message CoreArbB {
+    uint64 arb_block_height = 1;
+    uint64 acc_block_height = 2;
+    bool arb_locked = 3;
+    bool acc_locked = 4;
+    bool buy_published = 5;
+    bool refund_seen = 7;
+    oneof arb_lock_confirmations {
+        uint32 arb_confs = 9;
+    }
+    oneof acc_lock_confirmations {
+        uint32 acc_confs = 10;
+    }
+    oneof blocks_until_cancel_possible {
+        int64 cancel_blocks = 11;
+    }
+    oneof cancel_confirmations {
+        uint32 cancel_confs = 12;
+    }
+    oneof blocks_until_refund {
+        int64 refund_blocks = 13;
+    }
+    oneof blocks_until_punish_possible {
+        int64 punish_blocks = 14;
+    }
+}
+
+message BuySigB {
+    uint64 arb_block_height = 1;
+    uint64 acc_block_height = 2;
+    bool buy_tx_seen = 3;
+    oneof arb_lock_confirmations {
+        uint32 arb_confs = 4;
+    }
+    oneof acc_lock_confirmations {
+        uint32 acc_confs = 5;
+    }
+    oneof blocks_until_cancel_possible {
+        int64 cancel_blocks = 6;
+    }
+    oneof cancel_confirmations {
+        uint32 cancel_confs = 7;
+    }
+    oneof blocks_until_refund {
+        int64 refund_blocks = 8;
+    }
+    oneof blocks_until_punish_possible {
+        int64 punish_blocks = 9;
+    }
+    oneof blocks_until_safe_monero_buy_sweep {
+        uint32 buy_monero_blocks = 10;
+    }
+}
+
+enum Outcome {
+    SuccessSwap = 0;
+    FailureRefund = 1;
+    FailurePunish = 2;
+    FailureAbort = 3;
 }
 
 message ConnectSwapRequest {
diff --git a/src/grpcd/runtime.rs b/src/grpcd/runtime.rs
index 4ef4a4a8..4940499d 100644
--- a/src/grpcd/runtime.rs
+++ b/src/grpcd/runtime.rs
@@ -4,9 +4,13 @@ use crate::bus::ctl::ProtoPublicOffer;
 use crate::bus::ctl::PubOffer;
 use crate::bus::info::Address;
 use crate::bus::info::OfferStatusSelector;
+use crate::bus::info::ProgressEvent;
 use crate::bus::AddressSecretKey;
 use crate::bus::Failure;
+use crate::bus::OptionDetails;
+use crate::bus::Outcome;
 use crate::service::Endpoints;
+use crate::swapd::StateReport;
 use crate::syncerd::SweepAddressAddendum;
 use crate::syncerd::SweepBitcoinAddress;
 use crate::syncerd::SweepMoneroAddress;
@@ -76,6 +80,10 @@ use self::farcaster::SweepAddressRequest;
 use self::farcaster::SweepAddressResponse;
 use self::farcaster::TakeRequest;
 use self::farcaster::TakeResponse;
+use self::farcaster::{
+    BuySigB, CommitA, CommitB, CoreArbB, FinishA, FinishB, RefundSigA, RevealA, RevealB, StartA,
+    StartB,
+};
 
 pub mod farcaster {
     tonic::include_proto!("farcaster");
@@ -157,6 +165,161 @@ impl From<farcaster::OfferSelector> for OfferStatusSelector {
     }
 }
 
+impl From<Outcome> for farcaster::Outcome {
+    fn from(t: Outcome) -> farcaster::Outcome {
+        match t {
+            Outcome::SuccessSwap => farcaster::Outcome::SuccessSwap,
+            Outcome::FailureRefund => farcaster::Outcome::FailureRefund,
+            Outcome::FailurePunish => farcaster::Outcome::FailurePunish,
+            Outcome::FailureAbort => farcaster::Outcome::FailureAbort,
+        }
+    }
+}
+
+impl From<StateReport> for farcaster::State {
+    fn from(state_report: StateReport) -> farcaster::State {
+        match state_report.clone() {
+            StateReport::StartA => farcaster::State {
+                state: Some(farcaster::state::State::StartA(StartA {})),
+            },
+            StateReport::CommitA => farcaster::State {
+                state: Some(farcaster::state::State::CommitA(CommitA {})),
+            },
+            StateReport::RevealA => farcaster::State {
+                state: Some(farcaster::state::State::RevealA(RevealA {})),
+            },
+            StateReport::RefundSigA {
+                arb_block_height,
+                acc_block_height,
+                arb_locked,
+                acc_locked,
+                buy_published,
+                cancel_seen,
+                refund_seen,
+                overfunded,
+                arb_lock_confirmations,
+                acc_lock_confirmations,
+                blocks_until_cancel_possible,
+                cancel_confirmations,
+                blocks_until_punish_possible,
+                blocks_until_safe_buy,
+            } => farcaster::State {
+                state: Some(farcaster::state::State::RefundSigA(RefundSigA {
+                    arb_block_height,
+                    acc_block_height,
+                    arb_locked,
+                    acc_locked,
+                    buy_published,
+                    cancel_seen,
+                    refund_seen,
+                    overfunded,
+                    arb_lock_confirmations: arb_lock_confirmations
+                        .map(|c| farcaster::refund_sig_a::ArbLockConfirmations::ArbConfs(c)),
+                    acc_lock_confirmations: acc_lock_confirmations
+                        .map(|c| farcaster::refund_sig_a::AccLockConfirmations::AccConfs(c)),
+                    blocks_until_cancel_possible: blocks_until_cancel_possible.map(|b| {
+                        farcaster::refund_sig_a::BlocksUntilCancelPossible::CancelBlocks(b)
+                    }),
+                    cancel_confirmations: cancel_confirmations
+                        .map(|c| farcaster::refund_sig_a::CancelConfirmations::CancelConfs(c)),
+                    blocks_until_punish_possible: blocks_until_punish_possible.map(|b| {
+                        farcaster::refund_sig_a::BlocksUntilPunishPossible::PunishBlocks(b)
+                    }),
+                    blocks_until_safe_buy: blocks_until_safe_buy
+                        .map(|b| farcaster::refund_sig_a::BlocksUntilSafeBuy::BuyBlocks(b)),
+                })),
+            },
+            StateReport::FinishA(outcome) => farcaster::State {
+                state: Some(farcaster::state::State::FinishA(FinishA {
+                    outcome: farcaster::Outcome::from(outcome).into(),
+                })),
+            },
+            StateReport::StartB => farcaster::State {
+                state: Some(farcaster::state::State::StartB(StartB {})),
+            },
+            StateReport::CommitB => farcaster::State {
+                state: Some(farcaster::state::State::CommitB(CommitB {})),
+            },
+            StateReport::RevealB => farcaster::State {
+                state: Some(farcaster::state::State::RevealB(RevealB {})),
+            },
+            StateReport::CoreArbB {
+                arb_block_height,
+                acc_block_height,
+                arb_locked,
+                acc_locked,
+                buy_published,
+                refund_seen,
+                arb_lock_confirmations,
+                acc_lock_confirmations,
+                blocks_until_cancel_possible,
+                cancel_confirmations,
+                blocks_until_refund,
+                blocks_until_punish_possible,
+            } => farcaster::State {
+                state: Some(farcaster::state::State::CoreArbB(CoreArbB {
+                    arb_block_height,
+                    acc_block_height,
+                    arb_locked,
+                    acc_locked,
+                    buy_published,
+                    refund_seen,
+                    arb_lock_confirmations: arb_lock_confirmations
+                        .map(|c| farcaster::core_arb_b::ArbLockConfirmations::ArbConfs(c)),
+                    acc_lock_confirmations: acc_lock_confirmations
+                        .map(|c| farcaster::core_arb_b::AccLockConfirmations::AccConfs(c)),
+                    blocks_until_cancel_possible: blocks_until_cancel_possible
+                        .map(|b| farcaster::core_arb_b::BlocksUntilCancelPossible::CancelBlocks(b)),
+                    cancel_confirmations: cancel_confirmations
+                        .map(|c| farcaster::core_arb_b::CancelConfirmations::CancelConfs(c)),
+                    blocks_until_refund: blocks_until_refund
+                        .map(|b| farcaster::core_arb_b::BlocksUntilRefund::RefundBlocks(b)),
+                    blocks_until_punish_possible: blocks_until_punish_possible
+                        .map(|b| farcaster::core_arb_b::BlocksUntilPunishPossible::PunishBlocks(b)),
+                })),
+            },
+            StateReport::BuySigB {
+                arb_block_height,
+                acc_block_height,
+                buy_tx_seen,
+                arb_lock_confirmations,
+                acc_lock_confirmations,
+                blocks_until_cancel_possible,
+                cancel_confirmations,
+                blocks_until_refund,
+                blocks_until_punish_possible,
+                blocks_until_safe_monero_buy_sweep,
+            } => farcaster::State {
+                state: Some(farcaster::state::State::BuySigB(BuySigB {
+                    arb_block_height,
+                    acc_block_height,
+                    buy_tx_seen,
+                    arb_lock_confirmations: arb_lock_confirmations
+                        .map(|c| farcaster::buy_sig_b::ArbLockConfirmations::ArbConfs(c)),
+                    acc_lock_confirmations: acc_lock_confirmations
+                        .map(|c| farcaster::buy_sig_b::AccLockConfirmations::AccConfs(c)),
+                    blocks_until_cancel_possible: blocks_until_cancel_possible
+                        .map(|b| farcaster::buy_sig_b::BlocksUntilCancelPossible::CancelBlocks(b)),
+                    cancel_confirmations: cancel_confirmations
+                        .map(|c| farcaster::buy_sig_b::CancelConfirmations::CancelConfs(c)),
+                    blocks_until_refund: blocks_until_refund
+                        .map(|b| farcaster::buy_sig_b::BlocksUntilRefund::RefundBlocks(b)),
+                    blocks_until_punish_possible: blocks_until_punish_possible
+                        .map(|b| farcaster::buy_sig_b::BlocksUntilPunishPossible::PunishBlocks(b)),
+                    blocks_until_safe_monero_buy_sweep: blocks_until_safe_monero_buy_sweep.map(
+                        |b| farcaster::buy_sig_b::BlocksUntilSafeMoneroBuySweep::BuyMoneroBlocks(b),
+                    ),
+                })),
+            },
+            StateReport::FinishB(outcome) => farcaster::State {
+                state: Some(farcaster::state::State::FinishB(FinishB {
+                    outcome: farcaster::Outcome::from(outcome).into(),
+                })),
+            },
+        }
+    }
+}
+
 #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, PartialOrd, Hash, Display)]
 #[display(Debug)]
 pub struct IdCounter(u64);
@@ -692,11 +855,45 @@ impl Farcaster for FarcasterService {
             .await?;
 
         match oneshot_rx.await {
-            Ok(BusMsg::Info(InfoMsg::SwapProgress(progress))) => {
+            Ok(BusMsg::Info(InfoMsg::SwapProgress(mut progress))) => {
                 let reply = ProgressResponse {
                     id,
-                    progress: progress.progress.iter().map(|p| p.to_string()).collect(),
+                    progress: progress
+                        .progress
+                        .drain(..)
+                        .map(|p| match p {
+                            ProgressEvent::Message(m) => farcaster::Progress {
+                                progress: Some(farcaster::progress::Progress::Message(
+                                    m.to_string(),
+                                )),
+                            },
+                            ProgressEvent::StateUpdate(su) => farcaster::Progress {
+                                progress: Some(farcaster::progress::Progress::StateUpdate(
+                                    su.into(),
+                                )),
+                            },
+                            ProgressEvent::StateTransition(st) => farcaster::Progress {
+                                progress: Some(farcaster::progress::Progress::StateTransition(
+                                    farcaster::StateTransition {
+                                        old_state: Some(st.old_state.into()),
+                                        new_state: Some(st.new_state.into()),
+                                    },
+                                )),
+                            },
+                            ProgressEvent::Failure(Failure { info, .. }) => farcaster::Progress {
+                                progress: Some(farcaster::progress::Progress::Failure(
+                                    info.to_string(),
+                                )),
+                            },
+                            ProgressEvent::Success(OptionDetails(s)) => farcaster::Progress {
+                                progress: Some(farcaster::progress::Progress::Success(
+                                    s.unwrap_or("".to_string()),
+                                )),
+                            },
+                        })
+                        .collect(),
                 };
+
                 Ok(GrpcResponse::new(reply))
             }
             res => process_error_response(res),
diff --git a/src/service.rs b/src/service.rs
index 14fa82b6..e64b2324 100644
--- a/src/service.rs
+++ b/src/service.rs
@@ -403,23 +403,6 @@ where
         Ok(())
     }
 
-    fn report_state_transition_progress_message_to(
-        &mut self,
-        senders: &mut Endpoints,
-        dest: impl TryToServiceId,
-        msg: impl ToString,
-    ) -> Result<(), Error> {
-        if let Some(dest) = dest.try_to_service_id() {
-            senders.send_to(
-                ServiceBus::Ctl,
-                self.identity(),
-                dest,
-                BusMsg::Ctl(CtlMsg::Progress(Progress::StateTransition(msg.to_string()))),
-            )?;
-        }
-        Ok(())
-    }
-
     fn report_failure_to(
         &mut self,
         senders: &mut Endpoints,
diff --git a/src/swapd/mod.rs b/src/swapd/mod.rs
index aa8e0c1e..5673a9fd 100644
--- a/src/swapd/mod.rs
+++ b/src/swapd/mod.rs
@@ -15,6 +15,7 @@
 #[cfg(feature = "shell")]
 mod opts;
 mod runtime;
+mod state_report;
 mod swap_state;
 mod syncer_client;
 mod temporal_safety;
@@ -24,5 +25,6 @@ pub use opts::Opts;
 pub use runtime::get_swap_id;
 pub use runtime::run;
 pub use runtime::CheckpointSwapd;
+pub use state_report::StateReport;
 pub use swap_state::State;
 pub use swap_state::SwapCheckpointType;
diff --git a/src/swapd/runtime.rs b/src/swapd/runtime.rs
index aa753ab3..5d0da46f 100644
--- a/src/swapd/runtime.rs
+++ b/src/swapd/runtime.rs
@@ -18,7 +18,6 @@ use super::{
     syncer_client::{log_tx_received, log_tx_seen, SyncerState, SyncerTasks},
     temporal_safety::TemporalSafety,
 };
-use crate::databased::checkpoint_send;
 use crate::service::Endpoints;
 use crate::syncerd::bitcoin_syncer::p2wpkh_signed_tx_fee;
 use crate::syncerd::types::{AddressTransaction, Boolean, Event, Task, TransactionConfirmations};
@@ -36,6 +35,7 @@ use crate::{
         Abort, HeightChanged, SweepSuccess, TaskTarget, TransactionRetrieved, XmrAddressAddendum,
     },
 };
+use crate::{databased::checkpoint_send, swapd::StateReport};
 use crate::{CtlServer, Error, LogStyle, Service, ServiceConfig, ServiceId};
 
 use std::collections::HashMap;
@@ -136,8 +136,11 @@ pub fn run(
         awaiting_funding: false,
         xmr_addr_addendum: None,
         btc_fee_estimate_sat_per_kvb: None,
+        confirmations: none!(),
     };
 
+    let state_report = StateReport::new(&init_state, &temporal_safety, &syncer_state);
+
     let runtime = Runtime {
         swap_id,
         identity: ServiceId::Swap(swap_id),
@@ -156,6 +159,7 @@ pub fn run(
         txs: none!(),
         public_offer,
         local_trade_role,
+        latest_state_report: state_report,
     };
     let broker = false;
     Service::run(config, runtime, broker)
@@ -178,6 +182,7 @@ pub struct Runtime {
     txs: HashMap<TxLabel, bitcoin::Transaction>,
     public_offer: PublicOffer,
     local_trade_role: TradeRole,
+    latest_state_report: StateReport,
 }
 
 // FIXME Something more meaningful than ServiceId to index
@@ -476,13 +481,22 @@ impl esb::Handler<ServiceBus> for Runtime {
     ) -> Result<(), Self::Error> {
         match (bus, request) {
             // Peer-to-peer message bus, only accept peer message
-            (ServiceBus::Msg, BusMsg::P2p(req)) => self.handle_msg(endpoints, source, req),
+            (ServiceBus::Msg, BusMsg::P2p(req)) => {
+                self.handle_msg(endpoints, source, req)?;
+                self.report_potential_state_change(endpoints)
+            }
             // Control bus for issuing control commands, only accept Ctl message
-            (ServiceBus::Ctl, BusMsg::Ctl(req)) => self.handle_ctl(endpoints, source, req),
+            (ServiceBus::Ctl, BusMsg::Ctl(req)) => {
+                self.handle_ctl(endpoints, source, req)?;
+                self.report_potential_state_change(endpoints)
+            }
             // Info command bus, only accept Info message
             (ServiceBus::Info, BusMsg::Info(req)) => self.handle_info(endpoints, source, req),
             // Syncer event bus for blockchain tasks and events, only accept Sync message
-            (ServiceBus::Sync, BusMsg::Sync(req)) => self.handle_sync(endpoints, source, req),
+            (ServiceBus::Sync, BusMsg::Sync(req)) => {
+                self.handle_sync(endpoints, source, req)?;
+                self.report_potential_state_change(endpoints)
+            }
             // All other pairs are not supported
             (bus, req) => Err(Error::NotSupported(bus, req.to_string())),
         }
@@ -535,16 +549,14 @@ impl Runtime {
         &mut self.pending_requests
     }
 
-    fn state_update(&mut self, endpoints: &mut Endpoints, next_state: State) -> Result<(), Error> {
+    fn state_update(&mut self, next_state: State) -> Result<(), Error> {
         info!(
             "{} | State transition: {} -> {}",
             self.swap_id.swap_id(),
             self.state.label(),
             next_state.label(),
         );
-        let msg = format!("{} -> {}", self.state, next_state,);
         self.state = next_state;
-        self.report_state_transition_progress_message_to(endpoints, self.enquirer.clone(), msg)?;
         Ok(())
     }
 
@@ -727,7 +739,7 @@ impl Runtime {
                 // trigger state transition
                 debug!("{} | transition state", self.swap_id);
                 let next_state = self.state.clone().sup_commit_to_reveal();
-                self.state_update(endpoints, next_state)?;
+                self.state_update(next_state)?;
             }
 
             // Trade role: both
@@ -897,7 +909,7 @@ impl Runtime {
                     buy_tx_seen: false,
                     buy_proc: None,
                 });
-                self.state_update(endpoints, next_state)?;
+                self.state_update(next_state)?;
             }
 
             // Swap role: Alice, target of this message
@@ -1005,7 +1017,7 @@ impl Runtime {
                     funding_info: None,
                     overfunded: false,
                 });
-                self.state_update(endpoints, next_state)?;
+                self.state_update(next_state)?;
             }
 
             // Swap role: Bob, target of this message
@@ -1241,7 +1253,7 @@ impl Runtime {
                 };
                 // send taker commit message to counter-party
                 self.send_peer(endpoints, PeerMsg::TakerCommit(take_swap))?;
-                self.state_update(endpoints, next_state)?;
+                self.state_update(next_state)?;
             }
 
             // Trade role: Maker, target of this message
@@ -1289,7 +1301,7 @@ impl Runtime {
                 // send maker commit message to counter-party
                 trace!("sending peer MakerCommit msg {}", &local_commit);
                 self.send_peer(endpoints, PeerMsg::MakerCommit(local_commit))?;
-                self.state_update(endpoints, next_state)?;
+                self.state_update(next_state)?;
             }
 
             // Swap role: Bob
@@ -1460,7 +1472,7 @@ impl Runtime {
                     || (self.state.a_refundsig() && !self.state.a_btc_locked()) =>
             {
                 // just cancel the swap, no additional logic required
-                self.state_update(endpoints, State::Alice(AliceState::FinishA(Outcome::Abort)))?;
+                self.state_update(State::Alice(AliceState::FinishA(Outcome::FailureAbort)))?;
                 self.abort_swap(endpoints)?;
                 self.send_client_info(
                     endpoints,
@@ -1471,7 +1483,7 @@ impl Runtime {
 
             CtlMsg::AbortSwap if self.state.b_start() => {
                 // just cancel the swap, no additional logic required, since funding was not yet retrieved
-                self.state_update(endpoints, State::Bob(BobState::FinishB(Outcome::Abort)))?;
+                self.state_update(State::Bob(BobState::FinishB(Outcome::FailureAbort)))?;
                 self.abort_swap(endpoints)?;
                 self.send_client_info(
                     endpoints,
@@ -1494,7 +1506,7 @@ impl Runtime {
                     )),
                 )?;
                 // cancel the swap to invalidate its state
-                self.state_update(endpoints, State::Bob(BobState::FinishB(Outcome::Abort)))?;
+                self.state_update(State::Bob(BobState::FinishB(Outcome::FailureAbort)))?;
                 self.send_client_info(
                     endpoints,
                     source,
@@ -1662,9 +1674,9 @@ impl Runtime {
                 let connection = self.peer_service.node_addr();
                 let info = SwapInfo {
                     swap_id,
-                    state: self.state.to_string(),
                     connection,
                     connected: self.connected,
+                    state: self.latest_state_report.clone(),
                     uptime: SystemTime::now()
                         .duration_since(self.started)
                         .unwrap_or_else(|_| Duration::from_secs(0)),
@@ -1904,7 +1916,7 @@ impl Runtime {
                                 buy_tx_seen: false,
                                 last_checkpoint_type: self.state.last_checkpoint_type().unwrap(),
                             });
-                            self.state_update(endpoints, next_state)?;
+                            self.state_update(next_state)?;
                         }
                     }
 
@@ -1988,17 +2000,13 @@ impl Runtime {
                             BusMsg::Sync(SyncMsg::Task(abort_all)),
                         )?;
                         let success = if self.state.b_buy_sig() {
-                            self.state_update(
-                                endpoints,
-                                State::Bob(BobState::FinishB(Outcome::Buy)),
-                            )?;
-                            Some(Outcome::Buy)
+                            self.state_update(State::Bob(BobState::FinishB(Outcome::SuccessSwap)))?;
+                            Some(Outcome::SuccessSwap)
                         } else if self.state.a_refund_seen() {
-                            self.state_update(
-                                endpoints,
-                                State::Alice(AliceState::FinishA(Outcome::Refund)),
-                            )?;
-                            Some(Outcome::Refund)
+                            self.state_update(State::Alice(AliceState::FinishA(
+                                Outcome::FailureRefund,
+                            )))?;
+                            Some(Outcome::FailureRefund)
                         } else {
                             error!("Unexpected sweeping state, not sending finalization commands to wallet and farcasterd");
                             None
@@ -2151,7 +2159,7 @@ impl Runtime {
                                     buy_tx_seen: false,
                                     last_checkpoint_type: self.state.last_checkpoint_type().unwrap(),
                                 });
-                                self.state_update(endpoints, next_state)?;
+                                self.state_update(next_state)?;
                             }
                             TxLabel::Buy => {
                                 warn!(
@@ -2403,10 +2411,9 @@ impl Runtime {
                                     )?;
                                     self.syncer_state.awaiting_funding = false;
                                 }
-                                self.state_update(
-                                    endpoints,
-                                    State::Alice(AliceState::FinishA(Outcome::Refund)),
-                                )?;
+                                self.state_update(State::Alice(AliceState::FinishA(
+                                    Outcome::FailureRefund,
+                                )))?;
                                 let abort_all = Task::Abort(Abort {
                                     task_target: TaskTarget::AllTasks,
                                     respond: Boolean::False,
@@ -2424,7 +2431,7 @@ impl Runtime {
                                     BusMsg::Sync(SyncMsg::Task(abort_all)),
                                 )?;
                                 let swap_success_req =
-                                    BusMsg::Ctl(CtlMsg::SwapOutcome(Outcome::Refund));
+                                    BusMsg::Ctl(CtlMsg::SwapOutcome(Outcome::FailureRefund));
                                 self.send_wallet(
                                     ServiceBus::Ctl,
                                     endpoints,
@@ -2443,10 +2450,9 @@ impl Runtime {
                             {
                                 // FIXME: swap ends here for alice
                                 // wallet + farcaster
-                                self.state_update(
-                                    endpoints,
-                                    State::Alice(AliceState::FinishA(Outcome::Buy)),
-                                )?;
+                                self.state_update(State::Alice(AliceState::FinishA(
+                                    Outcome::SuccessSwap,
+                                )))?;
                                 let abort_all = Task::Abort(Abort {
                                     task_target: TaskTarget::AllTasks,
                                     respond: Boolean::False,
@@ -2464,7 +2470,7 @@ impl Runtime {
                                     BusMsg::Sync(SyncMsg::Task(abort_all)),
                                 )?;
                                 let swap_success_req =
-                                    BusMsg::Ctl(CtlMsg::SwapOutcome(Outcome::Buy));
+                                    BusMsg::Ctl(CtlMsg::SwapOutcome(Outcome::SuccessSwap));
                                 self.send_wallet(
                                     ServiceBus::Ctl,
                                     endpoints,
@@ -2524,12 +2530,11 @@ impl Runtime {
                                     self.syncer_state.bitcoin_syncer(),
                                     BusMsg::Sync(SyncMsg::Task(abort_all)),
                                 )?;
-                                self.state_update(
-                                    endpoints,
-                                    State::Bob(BobState::FinishB(Outcome::Refund)),
-                                )?;
+                                self.state_update(State::Bob(BobState::FinishB(
+                                    Outcome::FailureRefund,
+                                )))?;
                                 let swap_success_req =
-                                    BusMsg::Ctl(CtlMsg::SwapOutcome(Outcome::Refund));
+                                    BusMsg::Ctl(CtlMsg::SwapOutcome(Outcome::FailureRefund));
                                 self.send_ctl(
                                     endpoints,
                                     ServiceId::Wallet,
@@ -2560,20 +2565,18 @@ impl Runtime {
                                     BusMsg::Sync(SyncMsg::Task(abort_all)),
                                 )?;
                                 match self.state.swap_role() {
-                                    SwapRole::Alice => self.state_update(
-                                        endpoints,
-                                        State::Alice(AliceState::FinishA(Outcome::Punish)),
-                                    )?,
+                                    SwapRole::Alice => self.state_update(State::Alice(
+                                        AliceState::FinishA(Outcome::FailurePunish),
+                                    ))?,
                                     SwapRole::Bob => {
                                         warn!("{}", "You were punished!".err());
-                                        self.state_update(
-                                            endpoints,
-                                            State::Bob(BobState::FinishB(Outcome::Punish)),
-                                        )?
+                                        self.state_update(State::Bob(BobState::FinishB(
+                                            Outcome::FailurePunish,
+                                        )))?
                                     }
                                 }
                                 let swap_success_req =
-                                    BusMsg::Ctl(CtlMsg::SwapOutcome(Outcome::Punish));
+                                    BusMsg::Ctl(CtlMsg::SwapOutcome(Outcome::FailurePunish));
                                 self.send_ctl(
                                     endpoints,
                                     ServiceId::Wallet,
@@ -2622,10 +2625,7 @@ impl Runtime {
                             && self.syncer_state.tasks.sweeping_addr.is_some()
                             && &self.syncer_state.tasks.sweeping_addr.unwrap() == id =>
                     {
-                        self.state_update(
-                            endpoints,
-                            State::Bob(BobState::FinishB(Outcome::Abort)),
-                        )?;
+                        self.state_update(State::Bob(BobState::FinishB(Outcome::FailureAbort)))?;
                         endpoints.send_to(
                             ServiceBus::Ctl,
                             self.identity(),
@@ -2701,6 +2701,27 @@ impl Runtime {
 }
 
 impl Runtime {
+    fn report_potential_state_change(&mut self, endpoints: &mut Endpoints) -> Result<(), Error> {
+        // Generate a new state report for the clients
+        let new_state_report =
+            StateReport::new(&self.state, &self.temporal_safety, &self.syncer_state);
+        if self.latest_state_report != new_state_report {
+            let progress = self
+                .latest_state_report
+                .generate_progress_update_or_transition(&new_state_report);
+            self.latest_state_report = new_state_report;
+            if let Some(enquirer) = self.enquirer.clone() {
+                endpoints.send_to(
+                    ServiceBus::Ctl,
+                    self.identity(),
+                    enquirer,
+                    BusMsg::Ctl(CtlMsg::Progress(progress)),
+                )?;
+            }
+        }
+        Ok(())
+    }
+
     fn ask_bob_to_fund(
         &mut self,
         sat_per_kvb: u64,
@@ -2750,10 +2771,9 @@ impl Runtime {
             "Proposing to take swap {} to Maker remote peer",
             self.swap_id()
         );
-        let enquirer = self.enquirer.clone();
         // Ignoring possible reporting errors here and after: do not want to
         // halt the swap just because the client disconnected
-        let _ = self.report_progress_message_to(endpoints, &enquirer, msg);
+        let _ = self.report_progress_message_to(endpoints, &self.enquirer.clone(), msg);
 
         let engine = CommitmentEngine;
         let commitment = match params {
@@ -2804,7 +2824,7 @@ impl Runtime {
     }
 
     fn abort_swap(&mut self, endpoints: &mut Endpoints) -> Result<(), Error> {
-        let swap_success_req = BusMsg::Ctl(CtlMsg::SwapOutcome(Outcome::Abort));
+        let swap_success_req = BusMsg::Ctl(CtlMsg::SwapOutcome(Outcome::FailureAbort));
         self.send_ctl(endpoints, ServiceId::Wallet, swap_success_req.clone())?;
         self.send_ctl(endpoints, ServiceId::Farcasterd, swap_success_req)?;
         info!("{} | Aborted swap.", self.swap_id.swap_id());
diff --git a/src/swapd/state_report.rs b/src/swapd/state_report.rs
new file mode 100644
index 00000000..424417fe
--- /dev/null
+++ b/src/swapd/state_report.rs
@@ -0,0 +1,186 @@
+use farcaster_core::{blockchain::Blockchain, transaction::TxLabel};
+use strict_encoding::{NetworkDecode, NetworkEncode};
+
+use crate::bus::{Outcome, Progress, StateTransition};
+
+use super::{
+    swap_state::{AliceState, BobState},
+    syncer_client::SyncerState,
+    temporal_safety::TemporalSafety,
+    State,
+};
+
+#[derive(Clone, Debug, Eq, PartialEq, Hash, Display, NetworkEncode, NetworkDecode)]
+#[cfg_attr(
+    feature = "serde",
+    derive(Serialize, Deserialize),
+    serde(crate = "serde_crate")
+)]
+pub enum StateReport {
+    #[display("StartA")]
+    StartA,
+    #[display("CommitA")]
+    CommitA,
+    #[display("RevealA")]
+    RevealA,
+    #[display(Debug)]
+    RefundSigA {
+        arb_block_height: u64,
+        acc_block_height: u64,
+        arb_locked: bool,
+        acc_locked: bool,
+        buy_published: bool,
+        cancel_seen: bool,
+        refund_seen: bool,
+        overfunded: bool,
+        arb_lock_confirmations: Option<u32>,
+        acc_lock_confirmations: Option<u32>,
+        blocks_until_cancel_possible: Option<i64>,
+        cancel_confirmations: Option<u32>,
+        blocks_until_punish_possible: Option<i64>,
+        blocks_until_safe_buy: Option<u32>,
+    },
+    #[display("{0}")]
+    FinishA(Outcome),
+    #[display("StartB")]
+    StartB,
+    #[display("CommitB")]
+    CommitB,
+    #[display("RevealB")]
+    RevealB,
+    #[display(Debug)]
+    CoreArbB {
+        arb_block_height: u64,
+        acc_block_height: u64,
+        arb_locked: bool,
+        acc_locked: bool,
+        buy_published: bool,
+        refund_seen: bool,
+        arb_lock_confirmations: Option<u32>,
+        acc_lock_confirmations: Option<u32>,
+        blocks_until_cancel_possible: Option<i64>,
+        cancel_confirmations: Option<u32>,
+        blocks_until_refund: Option<i64>,
+        blocks_until_punish_possible: Option<i64>,
+    },
+    #[display(Debug)]
+    BuySigB {
+        arb_block_height: u64,
+        acc_block_height: u64,
+        buy_tx_seen: bool,
+        arb_lock_confirmations: Option<u32>,
+        acc_lock_confirmations: Option<u32>,
+        blocks_until_cancel_possible: Option<i64>,
+        cancel_confirmations: Option<u32>,
+        blocks_until_refund: Option<i64>,
+        blocks_until_punish_possible: Option<i64>,
+        blocks_until_safe_monero_buy_sweep: Option<u32>,
+    },
+    #[display("{0}")]
+    FinishB(Outcome),
+}
+
+impl StateReport {
+    pub fn new(
+        state: &State,
+        temp_safety: &TemporalSafety,
+        syncer_state: &SyncerState,
+    ) -> StateReport {
+        match state {
+            State::Alice(AliceState::StartA { .. }) => StateReport::StartA,
+            State::Alice(AliceState::CommitA { .. }) => StateReport::CommitA,
+            State::Alice(AliceState::RevealA { .. }) => StateReport::RevealA,
+            State::Alice(AliceState::RefundSigA {
+                btc_locked,
+                xmr_locked,
+                buy_published,
+                cancel_seen,
+                refund_seen,
+                overfunded,
+                ..
+            }) => StateReport::RefundSigA {
+                arb_block_height: syncer_state.bitcoin_height,
+                acc_block_height: syncer_state.monero_height,
+                arb_locked: *btc_locked,
+                acc_locked: *xmr_locked,
+                buy_published: *buy_published,
+                cancel_seen: *cancel_seen,
+                refund_seen: *refund_seen,
+                overfunded: *overfunded,
+                arb_lock_confirmations: syncer_state.get_confs(TxLabel::Lock),
+                acc_lock_confirmations: syncer_state.get_confs(TxLabel::AccLock),
+                blocks_until_cancel_possible: syncer_state
+                    .get_confs(TxLabel::Lock)
+                    .map(|confs| temp_safety.blocks_until_cancel(confs)),
+                cancel_confirmations: syncer_state.get_confs(TxLabel::Cancel),
+                blocks_until_punish_possible: syncer_state
+                    .get_confs(TxLabel::Cancel)
+                    .map(|confs| temp_safety.blocks_until_punish_after_cancel(confs)),
+                blocks_until_safe_buy: syncer_state
+                    .get_confs(TxLabel::Lock)
+                    .map(|c| temp_safety.btc_finality_thr.checked_sub(c).unwrap_or(0)),
+            },
+            State::Alice(AliceState::FinishA(outcome)) => StateReport::FinishA(outcome.clone()),
+            State::Bob(BobState::StartB { .. }) => StateReport::StartB,
+            State::Bob(BobState::CommitB { .. }) => StateReport::CommitB,
+            State::Bob(BobState::RevealB { .. }) => StateReport::RevealB,
+            State::Bob(BobState::CorearbB { buy_tx_seen, .. }) => StateReport::CoreArbB {
+                buy_published: *buy_tx_seen,
+                arb_block_height: syncer_state.bitcoin_height,
+                acc_block_height: syncer_state.monero_height,
+                arb_locked: temp_safety.final_tx(
+                    syncer_state.get_confs(TxLabel::Lock).unwrap_or(0),
+                    Blockchain::Bitcoin,
+                ),
+                acc_locked: temp_safety.final_tx(
+                    syncer_state.get_confs(TxLabel::AccLock).unwrap_or(0),
+                    Blockchain::Bitcoin,
+                ),
+                refund_seen: syncer_state.get_confs(TxLabel::Refund).is_some(),
+                arb_lock_confirmations: syncer_state.get_confs(TxLabel::Lock),
+                acc_lock_confirmations: syncer_state.get_confs(TxLabel::AccLock),
+                blocks_until_cancel_possible: syncer_state
+                    .get_confs(TxLabel::Lock)
+                    .map(|confs| temp_safety.blocks_until_cancel(confs)),
+                cancel_confirmations: syncer_state.get_confs(TxLabel::Cancel),
+                blocks_until_refund: None,
+                blocks_until_punish_possible: syncer_state
+                    .get_confs(TxLabel::Cancel)
+                    .map(|confs| temp_safety.blocks_until_punish_after_cancel(confs)),
+            },
+            State::Bob(BobState::BuySigB { buy_tx_seen, .. }) => StateReport::BuySigB {
+                buy_tx_seen: *buy_tx_seen,
+                arb_block_height: syncer_state.bitcoin_height,
+                acc_block_height: syncer_state.monero_height,
+                arb_lock_confirmations: syncer_state.get_confs(TxLabel::Lock),
+                acc_lock_confirmations: syncer_state.get_confs(TxLabel::AccLock),
+                blocks_until_cancel_possible: syncer_state
+                    .get_confs(TxLabel::Lock)
+                    .map(|confs| temp_safety.blocks_until_cancel(confs)),
+                cancel_confirmations: syncer_state.get_confs(TxLabel::Cancel),
+                blocks_until_refund: None,
+                blocks_until_punish_possible: syncer_state
+                    .get_confs(TxLabel::Cancel)
+                    .map(|confs| temp_safety.blocks_until_punish_after_cancel(confs)),
+                blocks_until_safe_monero_buy_sweep: syncer_state
+                    .get_confs(TxLabel::AccLock)
+                    .map(|c| temp_safety.sweep_monero_thr.checked_sub(c).unwrap_or(0)),
+            },
+            State::Bob(BobState::FinishB(outcome)) => StateReport::FinishB(outcome.clone()),
+        }
+    }
+
+    pub fn generate_progress_update_or_transition(
+        &self,
+        new_state_report: &StateReport,
+    ) -> Progress {
+        if std::mem::discriminant(self) == std::mem::discriminant(new_state_report) {
+            Progress::StateUpdate(new_state_report.clone())
+        } else {
+            Progress::StateTransition(StateTransition {
+                old_state: self.clone(),
+                new_state: new_state_report.clone(),
+            })
+        }
+    }
+}
diff --git a/src/swapd/swap_state.rs b/src/swapd/swap_state.rs
index 221224b2..7e4e4fb0 100644
--- a/src/swapd/swap_state.rs
+++ b/src/swapd/swap_state.rs
@@ -264,7 +264,7 @@ impl State {
         matches!(self, State::Bob(BobState::BuySigB { .. }))
     }
     pub fn b_outcome_abort(&self) -> bool {
-        matches!(self, State::Bob(BobState::FinishB(Outcome::Abort)))
+        matches!(self, State::Bob(BobState::FinishB(Outcome::FailureAbort)))
     }
     pub fn remote_commit(&self) -> Option<&Commit> {
         match self {
diff --git a/src/swapd/syncer_client.rs b/src/swapd/syncer_client.rs
index f2225830..e00052b7 100644
--- a/src/swapd/syncer_client.rs
+++ b/src/swapd/syncer_client.rs
@@ -54,6 +54,7 @@ pub struct SyncerState {
     pub monero_amount: monero::Amount,
     pub bitcoin_amount: bitcoin::Amount,
     pub xmr_addr_addendum: Option<XmrAddressAddendum>,
+    pub confirmations: HashMap<TxLabel, Option<u32>>,
     pub awaiting_funding: bool,
     pub btc_fee_estimate_sat_per_kvb: Option<u64>,
 }
@@ -385,6 +386,8 @@ impl SyncerState {
                     }
                 }
             }
+            self.confirmations
+                .insert(txlabel.clone(), confirmations.clone());
         } else {
             error!(
                 "received event with unknown transaction and task id {}",
@@ -418,6 +421,10 @@ impl SyncerState {
         )?;
         Ok(())
     }
+
+    pub fn get_confs(&self, label: TxLabel) -> Option<u32> {
+        self.confirmations.get(&label).map(|c| c.clone()).flatten()
+    }
 }
 
 pub fn log_tx_seen(swap_id: SwapId, txlabel: &TxLabel, txid: &Txid) {
diff --git a/src/swapd/temporal_safety.rs b/src/swapd/temporal_safety.rs
index 182974be..7bfdee0d 100644
--- a/src/swapd/temporal_safety.rs
+++ b/src/swapd/temporal_safety.rs
@@ -49,11 +49,19 @@ impl TemporalSafety {
         self.final_tx(lock_confirmations, Blockchain::Bitcoin)
             && lock_confirmations > (self.cancel_timelock - self.race_thr + 1)
     }
+    // blocks remaining until funding will be stopped for safety, because it is too close to cancel. Adds the same +1 offset as in stop_funding_before_cancel
+    pub fn blocks_until_stop_funding(&self, lock_confirmations: u32) -> i64 {
+        self.cancel_timelock as i64 - (self.race_thr as i64 + 1 + lock_confirmations as i64)
+    }
     /// lock must be final, valid after lock_minedblock + cancel_timelock
     pub fn valid_cancel(&self, lock_confirmations: u32) -> bool {
         self.final_tx(lock_confirmations, Blockchain::Bitcoin)
             && lock_confirmations >= self.cancel_timelock
     }
+    /// blocks remaining until cancel, copies logic from valid_cancel
+    pub fn blocks_until_cancel(&self, lock_confirmations: u32) -> i64 {
+        self.cancel_timelock as i64 - lock_confirmations as i64
+    }
     /// lock must be final, but buy shall not be raced with cancel
     pub fn safe_buy(&self, lock_confirmations: u32) -> bool {
         self.final_tx(lock_confirmations, Blockchain::Bitcoin)
@@ -64,8 +72,13 @@ impl TemporalSafety {
         self.final_tx(cancel_confirmations, Blockchain::Bitcoin)
             && cancel_confirmations <= (self.punish_timelock - self.race_thr)
     }
+    /// cancel must be final, valid after cancel_confirmations > punish_timelock
     pub fn valid_punish(&self, cancel_confirmations: u32) -> bool {
         self.final_tx(cancel_confirmations, Blockchain::Bitcoin)
             && cancel_confirmations >= self.punish_timelock
     }
+    /// blocks remaning until punish, copies logic from valid_punish
+    pub fn blocks_until_punish_after_cancel(&self, cancel_confirmations: u32) -> i64 {
+        self.punish_timelock as i64 - cancel_confirmations as i64
+    }
 }
diff --git a/src/walletd/runtime.rs b/src/walletd/runtime.rs
index 8d830da9..3e987f14 100644
--- a/src/walletd/runtime.rs
+++ b/src/walletd/runtime.rs
@@ -1353,7 +1353,7 @@ impl Runtime {
             CtlMsg::SwapOutcome(success) => {
                 let swap_id = get_swap_id(&source)?;
                 let success = match success {
-                    Outcome::Buy => success.bright_green_bold(),
+                    Outcome::SuccessSwap => success.bright_green_bold(),
                     _ => success.err(),
                 };
                 info!(


Test patch:
diff --git a/tests/swap.rs b/tests/swap.rs
index b40b14dc..e9a0176e 100644
--- a/tests/swap.rs
+++ b/tests/swap.rs
@@ -4,8 +4,9 @@ extern crate log;
 use bitcoincore_rpc::RpcApi;
 use farcaster_core::swap::SwapId;
 use farcaster_node::bus::ctl::{BitcoinFundingInfo, FundingInfo, MoneroFundingInfo};
-use farcaster_node::bus::info::{FundingInfos, NodeInfo};
-use farcaster_node::bus::CheckpointEntry;
+use farcaster_node::bus::info::{FundingInfos, NodeInfo, ProgressEvent, SwapProgress};
+use farcaster_node::bus::{CheckpointEntry, Outcome, StateTransition};
+use farcaster_node::swapd::StateReport;
 use futures::future::join_all;
 use std::collections::HashSet;
 use std::sync::Arc;
@@ -812,20 +813,12 @@ async fn run_restore_alice_pre_lock(
         .send_to_address(&address, amount, None, None, None, None, None, None)
         .unwrap();
 
-    info!("waiting for AliceState(RefundSigs");
-    retry_until_finish_state_transition(
-        cli_alice_progress_args.clone(),
-        "AliceState(RefundSigs".to_string(),
-    )
-    .await;
+    info!("waiting for RefundSigA");
+    retry_until_state_transition(cli_alice_progress_args.clone(), "RefundSigA".to_string()).await;
 
-    // run until BobState(CoreArb) is received
-    info!("waiting for BobState(CoreArb)");
-    retry_until_finish_state_transition(
-        cli_bob_progress_args.clone(),
-        "BobState(CoreArb)".to_string(),
-    )
-    .await;
+    // run until CoreArbB is received
+    info!("waiting for CoreArbB");
+    retry_until_state_transition(cli_bob_progress_args.clone(), "CoreArbB".to_string()).await;
 
     // run until the funding infos are cleared again
     info!("waiting for the bitcoin funding info to clear");
@@ -865,12 +858,8 @@ async fn run_restore_alice_pre_lock(
         .await
         .unwrap();
 
-    // run until BobState(BuySig) is received
-    retry_until_finish_state_transition(
-        cli_bob_progress_args.clone(),
-        "BobState(BuySig)".to_string(),
-    )
-    .await;
+    // run until BuySigB is received
+    retry_until_state_transition(cli_bob_progress_args.clone(), "BuySigB".to_string()).await;
 
     tokio::time::sleep(time::Duration::from_secs(10)).await;
 
@@ -879,10 +868,11 @@ async fn run_restore_alice_pre_lock(
         .generate_to_address(5, &reusable_btc_address())
         .unwrap();
 
-    // run until the AliceState(Finish) is received
-    retry_until_finish_state_transition(
+    // run until SuccessSwap is received
+    retry_until_bob_finish_state_transition(
         cli_alice_progress_args.clone(),
-        "AliceState(Finish(Success(Swapped)))".to_string(),
+        StateReport::FinishA(Outcome::SuccessSwap),
+        monero_regtest.clone(),
     )
     .await;
 
@@ -920,10 +910,10 @@ async fn run_restore_alice_pre_lock(
         .await
         .unwrap();
 
-    // run until the BobState(Finish) is received
+    // run until SuccessSwap is received
     retry_until_bob_finish_state_transition(
         cli_bob_progress_args.clone(),
-        "BobState(Finish(Success(Swapped)))".to_string(),
+        StateReport::FinishB(Outcome::SuccessSwap),
         monero_regtest.clone(),
     )
     .await;
@@ -982,20 +972,12 @@ async fn run_restore_checkpoint_bob_pre_buy_alice_pre_buy(
         .send_to_address(&address, amount, None, None, None, None, None, None)
         .unwrap();
 
-    info!("waiting for AliceState(RefundSigs");
-    retry_until_finish_state_transition(
-        cli_alice_progress_args.clone(),
-        "AliceState(RefundSigs".to_string(),
-    )
-    .await;
+    info!("waiting for RefundSigA");
+    retry_until_state_transition(cli_alice_progress_args.clone(), "RefundSigA".to_string()).await;
 
-    // run until BobState(CoreArb) is received
-    info!("waiting for BobState(CoreArb)");
-    retry_until_finish_state_transition(
-        cli_bob_progress_args.clone(),
-        "BobState(CoreArb)".to_string(),
-    )
-    .await;
+    // run until CoreArbB is received
+    info!("waiting for CoreArbB");
+    retry_until_state_transition(cli_bob_progress_args.clone(), "CoreArbB".to_string()).await;
 
     // run until the funding infos are cleared again
     info!("waiting for the bitcoin funding info to clear");
@@ -1024,12 +1006,8 @@ async fn run_restore_checkpoint_bob_pre_buy_alice_pre_buy(
         .await
         .unwrap();
 
-    // run until BobState(BuySig) is received
-    retry_until_finish_state_transition(
-        cli_bob_progress_args.clone(),
-        "BobState(BuySig)".to_string(),
-    )
-    .await;
+    // run until BuySigB is received
+    retry_until_state_transition(cli_bob_progress_args.clone(), "BuySigB".to_string()).await;
 
     tokio::time::sleep(time::Duration::from_secs(10)).await;
 
@@ -1051,10 +1029,10 @@ async fn run_restore_checkpoint_bob_pre_buy_alice_pre_buy(
         .generate_to_address(5, &reusable_btc_address())
         .unwrap();
 
-    // run until the AliceState(Finish) is received
-    retry_until_finish_state_transition(
+    // run until the SuccessSwap outcome is received
+    retry_until_finish_transition(
         cli_alice_progress_args.clone(),
-        "AliceState(Finish(Success(Swapped)))".to_string(),
+        StateReport::FinishA(Outcome::SuccessSwap),
     )
     .await;
 
@@ -1092,10 +1070,10 @@ async fn run_restore_checkpoint_bob_pre_buy_alice_pre_buy(
         .await
         .unwrap();
 
-    // run until the BobState(Finish) is received
+    // run until SuccessSwap is received
     retry_until_bob_finish_state_transition(
         cli_bob_progress_args.clone(),
-        "BobState(Finish(Success(Swapped)))".to_string(),
+        StateReport::FinishB(Outcome::SuccessSwap),
         monero_regtest.clone(),
     )
     .await;
@@ -1154,20 +1132,12 @@ async fn run_restore_checkpoint_bob_pre_buy_alice_pre_lock(
         .send_to_address(&address, amount, None, None, None, None, None, None)
         .unwrap();
 
-    info!("waiting for AliceState(RefundSigs");
-    retry_until_finish_state_transition(
-        cli_alice_progress_args.clone(),
-        "AliceState(RefundSigs".to_string(),
-    )
-    .await;
+    info!("waiting for RefundSigA");
+    retry_until_state_transition(cli_alice_progress_args.clone(), "RefundSigA".to_string()).await;
 
-    // run until BobState(CoreArb) is received
-    info!("waiting for BobState(CoreArb)");
-    retry_until_finish_state_transition(
-        cli_bob_progress_args.clone(),
-        "BobState(CoreArb)".to_string(),
-    )
-    .await;
+    // run until CoreArbB is received
+    info!("waiting for CoreArbB");
+    retry_until_state_transition(cli_bob_progress_args.clone(), "CoreArbB".to_string()).await;
 
     // run until the funding infos are cleared again
     info!("waiting for the bitcoin funding info to clear");
@@ -1225,10 +1195,10 @@ async fn run_restore_checkpoint_bob_pre_buy_alice_pre_lock(
         .generate_to_address(3, &reusable_btc_address())
         .unwrap();
 
-    // run until the BobState(Finish(Failure(Refunded))) is received
-    retry_until_finish_state_transition(
+    // run until FailureRefund is received
+    retry_until_finish_transition(
         cli_bob_progress_args.clone(),
-        "BobState(Finish(Failure(Refunded)))".to_string(),
+        StateReport::FinishB(Outcome::FailureRefund),
     )
     .await;
 
@@ -1266,10 +1236,10 @@ async fn run_restore_checkpoint_bob_pre_buy_alice_pre_lock(
         .await
         .unwrap();
 
-    // run until the BobState(Finish) is received
-    retry_until_finish_state_transition(
+    // run until FailureRefund is received
+    retry_until_finish_transition(
         cli_alice_progress_args.clone(),
-        "AliceState(Finish(Failure(Refunded)))".to_string(),
+        StateReport::FinishA(Outcome::FailureRefund),
     )
     .await;
 
@@ -1330,20 +1300,12 @@ async fn run_refund_swap_alice_overfunds(
         .send_to_address(&address, amount, None, None, None, None, None, None)
         .unwrap();
 
-    info!("waiting for AliceState(RefundSigs");
-    retry_until_finish_state_transition(
-        cli_alice_progress_args.clone(),
-        "AliceState(RefundSigs".to_string(),
-    )
-    .await;
+    info!("waiting for RefundSigA");
+    retry_until_state_transition(cli_alice_progress_args.clone(), "RefundSigA".to_string()).await;
 
-    // run until BobState(CoreArb) is received
-    info!("waiting for BobState(CoreArb)");
-    retry_until_finish_state_transition(
-        cli_bob_progress_args.clone(),
-        "BobState(CoreArb)".to_string(),
-    )
-    .await;
+    // run until CoreArbB is received
+    info!("waiting for CoreArbB");
+    retry_until_state_transition(cli_bob_progress_args.clone(), "CoreArbB".to_string()).await;
 
     // run until the funding infos are cleared again
     info!("waiting for the bitcoin funding info to clear");
@@ -1378,12 +1340,8 @@ async fn run_refund_swap_alice_overfunds(
         .await
         .unwrap();
 
-    // run until BobState(BuySig) is received
-    retry_until_finish_state_transition(
-        cli_bob_progress_args.clone(),
-        "BobState(BuySig)".to_string(),
-    )
-    .await;
+    // run until BuySigB is received
+    retry_until_state_transition(cli_bob_progress_args.clone(), "BuySigB".to_string()).await;
 
     tokio::time::sleep(time::Duration::from_secs(20)).await;
 
@@ -1410,10 +1368,10 @@ async fn run_refund_swap_alice_overfunds(
         .generate_to_address(3, &reusable_btc_address())
         .unwrap();
 
-    // run until the BobState(Finish(Failure(Refunded))) is received
-    retry_until_finish_state_transition(
+    // run until FailureRefund is received
+    retry_until_finish_transition(
         cli_bob_progress_args.clone(),
-        "BobState(Finish(Failure(Refunded)))".to_string(),
+        StateReport::FinishB(Outcome::FailureRefund),
     )
     .await;
 
@@ -1451,10 +1409,10 @@ async fn run_refund_swap_alice_overfunds(
         .await
         .unwrap();
 
-    // run until the AliceState(Finish) is received
-    retry_until_finish_state_transition(
+    // run until FailureRefund is received
+    retry_until_finish_transition(
         cli_alice_progress_args.clone(),
-        "AliceState(Finish(Failure(Refunded)))".to_string(),
+        StateReport::FinishA(Outcome::FailureRefund),
     )
     .await;
 
@@ -1509,20 +1467,12 @@ async fn run_refund_swap_race_cancel(
         .send_to_address(&address, amount, None, None, None, None, None, None)
         .unwrap();
 
-    info!("waiting for AliceState(RefundSigs");
-    retry_until_finish_state_transition(
-        cli_alice_progress_args.clone(),
-        "AliceState(RefundSigs".to_string(),
-    )
-    .await;
+    info!("waiting for RefundSigA");
+    retry_until_state_transition(cli_alice_progress_args.clone(), "RefundSigA".to_string()).await;
 
-    // run until BobState(CoreArb) is received
-    info!("waiting for BobState(CoreArb)");
-    retry_until_finish_state_transition(
-        cli_bob_progress_args.clone(),
-        "BobState(CoreArb)".to_string(),
-    )
-    .await;
+    // run until CoreArbB is received
+    info!("waiting for CoreArbB");
+    retry_until_state_transition(cli_bob_progress_args.clone(), "CoreArbB".to_string()).await;
 
     // run until the funding infos are cleared again
     info!("waiting for the bitcoin funding info to clear");
@@ -1565,10 +1515,10 @@ async fn run_refund_swap_race_cancel(
         .generate_to_address(3, &reusable_btc_address())
         .unwrap();
 
-    // run until the BobState(Finish(Failure(Refunded))) is received
-    retry_until_finish_state_transition(
+    // run until FailureRefund is received
+    retry_until_finish_transition(
         cli_bob_progress_args.clone(),
-        "BobState(Finish(Failure(Refunded)))".to_string(),
+        StateReport::FinishB(Outcome::FailureRefund),
     )
     .await;
 
@@ -1606,10 +1556,10 @@ async fn run_refund_swap_race_cancel(
         .await
         .unwrap();
 
-    // run until the BobState(Finish) is received
-    retry_until_finish_state_transition(
+    // run until FailureRefund is received
+    retry_until_finish_transition(
         cli_alice_progress_args.clone(),
-        "AliceState(Finish(Failure(Refunded)))".to_string(),
+        StateReport::FinishA(Outcome::FailureRefund),
     )
     .await;
 
@@ -1663,20 +1613,12 @@ async fn run_refund_swap_kill_alice_after_funding(
         .send_to_address(&address, amount, None, None, None, None, None, None)
         .unwrap();
 
-    info!("waiting for AliceState(RefundSigs");
-    retry_until_finish_state_transition(
-        cli_alice_progress_args.clone(),
-        "AliceState(RefundSigs".to_string(),
-    )
-    .await;
+    info!("waiting for RefundSigA");
+    retry_until_state_transition(cli_alice_progress_args.clone(), "RefundSigA".to_string()).await;
 
-    // run until BobState(CoreArb) is received
-    info!("waiting for BobState(CoreArb)");
-    retry_until_finish_state_transition(
-        cli_bob_progress_args.clone(),
-        "BobState(CoreArb)".to_string(),
-    )
-    .await;
+    // run until CoreArbB is received
+    info!("waiting for CoreArbB");
+    retry_until_state_transition(cli_bob_progress_args.clone(), "CoreArbB".to_string()).await;
 
     // run until the funding infos are cleared again
     info!("waiting for the bitcoin funding info to clear");
@@ -1722,10 +1664,10 @@ async fn run_refund_swap_kill_alice_after_funding(
         .generate_to_address(3, &reusable_btc_address())
         .unwrap();
 
-    // run until the BobState(Finish(Failure(Refunded))) is received
-    retry_until_finish_state_transition(
+    // run until FailureRefund is received
+    retry_until_finish_transition(
         cli_bob_progress_args.clone(),
-        "BobState(Finish(Failure(Refunded)))".to_string(),
+        StateReport::FinishB(Outcome::FailureRefund),
     )
     .await;
 
@@ -1774,20 +1716,12 @@ async fn run_refund_swap_alice_does_not_fund(
         .send_to_address(&address, amount, None, None, None, None, None, None)
         .unwrap();
 
-    info!("waiting for AliceState(RefundSigs");
-    retry_until_finish_state_transition(
-        cli_alice_progress_args.clone(),
-        "AliceState(RefundSigs".to_string(),
-    )
-    .await;
+    info!("waiting for RefundSigA");
+    retry_until_state_transition(cli_alice_progress_args.clone(), "RefundSigA".to_string()).await;
 
-    // run until BobState(CoreArb) is received
-    info!("waiting for BobState(CoreArb)");
-    retry_until_finish_state_transition(
-        cli_bob_progress_args.clone(),
-        "BobState(CoreArb)".to_string(),
-    )
-    .await;
+    // run until CoreArbB is received
+    info!("waiting for CoreArbB");
+    retry_until_state_transition(cli_bob_progress_args.clone(), "CoreArbB".to_string()).await;
 
     // run until the funding infos are cleared again
     info!("waiting for the bitcoin funding info to clear");
@@ -1814,10 +1748,10 @@ async fn run_refund_swap_alice_does_not_fund(
         .generate_to_address(3, &reusable_btc_address())
         .unwrap();
 
-    // run until the AliceState(Finish(Failure(Refunded))) is received
-    retry_until_finish_state_transition(
+    // run until FailureRefund is received
+    retry_until_finish_transition(
         cli_alice_progress_args.clone(),
-        "AliceState(Finish(Failure(Refunded)))".to_string(),
+        StateReport::FinishA(Outcome::FailureRefund),
     )
     .await;
 
@@ -1831,10 +1765,10 @@ async fn run_refund_swap_alice_does_not_fund(
         .generate_to_address(3, &reusable_btc_address())
         .unwrap();
 
-    // run until the BobState(Finish(Failure(Refunded))) is received
-    retry_until_finish_state_transition(
+    // run until FailureRefund is received
+    retry_until_finish_transition(
         cli_bob_progress_args.clone(),
-        "BobState(Finish(Failure(Refunded)))".to_string(),
+        StateReport::FinishB(Outcome::FailureRefund),
     )
     .await;
 
@@ -1886,20 +1820,12 @@ async fn run_punish_swap_kill_bob_before_monero_funding(
         .send_to_address(&address, amount, None, None, None, None, None, None)
         .unwrap();
 
-    info!("waiting for AliceState(RefundSigs");
-    retry_until_finish_state_transition(
-        cli_alice_progress_args.clone(),
-        "AliceState(RefundSigs".to_string(),
-    )
-    .await;
+    info!("waiting for RefundSigA");
+    retry_until_state_transition(cli_alice_progress_args.clone(), "RefundSigA".to_string()).await;
 
-    // run until BobState(CoreArb) is received
-    info!("waiting for BobState(CoreArb)");
-    retry_until_finish_state_transition(
-        cli_bob_progress_args.clone(),
-        "BobState(CoreArb)".to_string(),
-    )
-    .await;
+    // run until CoreArbB is received
+    info!("waiting for CoreArbB");
+    retry_until_state_transition(cli_bob_progress_args.clone(), "CoreArbB".to_string()).await;
 
     // run until the funding infos are cleared again
     info!("waiting for the bitcoin funding info to clear");
@@ -1974,10 +1900,10 @@ async fn run_punish_swap_kill_bob_before_monero_funding(
 
     info!("generated 20 bitcoin blocks");
 
-    // run until the AliceState(Finish) is received
-    retry_until_finish_state_transition(
+    // run until the FailurePunish is received
+    retry_until_finish_transition(
         cli_alice_progress_args.clone(),
-        "AliceState(Finish(Failure(Punished)))".to_string(),
+        StateReport::FinishA(Outcome::FailurePunish),
     )
     .await;
 
@@ -2219,20 +2145,20 @@ async fn run_swaps_parallel(
     }
 
     for (swap_id, SwapParams { data_dir_alice, .. }) in swap_info.iter() {
-        info!("waiting for AliceState(RefundSigs");
-        retry_until_finish_state_transition(
+        info!("waiting for RefundSigA");
+        retry_until_state_transition(
             progress_args(data_dir_alice.clone(), *swap_id),
-            "AliceState(RefundSigs".to_string(),
+            "RefundSigA".to_string(),
         )
         .await;
     }
 
-    // run until BobState(CoreArb) is received
+    // run until CoreArbB is received
     for (swap_id, SwapParams { data_dir_bob, .. }) in swap_info.iter() {
-        info!("waiting for BobState(CoreArb)");
-        retry_until_finish_state_transition(
+        info!("waiting for CoreArbB");
+        retry_until_state_transition(
             progress_args(data_dir_bob.clone(), *swap_id),
-            "BobState(CoreArb)".to_string(),
+            "CoreArbB".to_string(),
         )
         .await;
     }
@@ -2280,12 +2206,12 @@ async fn run_swaps_parallel(
         .await
         .unwrap();
 
-    // run until BobState(BuySig) is received
+    // run until BuySigB is received
     for (swap_id, SwapParams { data_dir_bob, .. }) in swap_info.iter() {
-        info!("waiting for BobState(BuySig)");
-        retry_until_finish_state_transition(
+        info!("waiting for BuySigB");
+        retry_until_state_transition(
             progress_args(data_dir_bob.clone(), *swap_id),
-            "BobState(BuySig)".to_string(),
+            "BuySigB".to_string(),
         )
         .await;
     }
@@ -2297,11 +2223,11 @@ async fn run_swaps_parallel(
         .generate_to_address(5, &reusable_btc_address())
         .unwrap();
 
-    // run until the AliceState(Finish) is received
+    // run until the SuccessSwap is received
     for (swap_id, SwapParams { data_dir_alice, .. }) in swap_info.iter() {
-        retry_until_finish_state_transition(
+        retry_until_finish_transition(
             progress_args(data_dir_alice.clone(), *swap_id),
-            "AliceState(Finish(Success(Swapped)))".to_string(),
+            StateReport::FinishA(Outcome::SuccessSwap),
         )
         .await;
     }
@@ -2364,12 +2290,11 @@ async fn run_swaps_parallel(
         .await
         .unwrap();
 
-    // run until the BobState(Finish) is received
+    // run until SuccessSwap is received
     for (swap_id, SwapParams { data_dir_bob, .. }) in swap_info.iter() {
-        retry_until_finish_state_transition(
+        retry_until_finish_transition(
             progress_args(data_dir_bob.clone(), *swap_id),
-            "BobState(Finish(Success(Swapped)))".to_string(),
-            // monero_regtest.clone(),
+            StateReport::FinishB(Outcome::SuccessSwap),
         )
         .await;
     }
@@ -2588,20 +2513,12 @@ async fn run_swap_bob_maker_manual_monero_sweep(
         .send_to_address(&address, amount, None, None, None, None, None, None)
         .unwrap();
 
-    info!("waiting for AliceState(RefundSigs");
-    retry_until_finish_state_transition(
-        cli_alice_progress_args.clone(),
-        "AliceState(RefundSigs".to_string(),
-    )
-    .await;
+    info!("waiting for RefundSigA");
+    retry_until_state_transition(cli_alice_progress_args.clone(), "RefundSigA".to_string()).await;
 
-    // run until BobState(CoreArb) is received
-    info!("waiting for BobState(CoreArb)");
-    retry_until_finish_state_transition(
-        cli_bob_progress_args.clone(),
-        "BobState(CoreArb)".to_string(),
-    )
-    .await;
+    // run until CoreArbB is received
+    info!("waiting for CoreArbB");
+    retry_until_state_transition(cli_bob_progress_args.clone(), "CoreArbB".to_string()).await;
 
     // run until the funding infos are cleared again
     info!("waiting for the bitcoin funding info to clear");
@@ -2629,12 +2546,8 @@ async fn run_swap_bob_maker_manual_monero_sweep(
         .await
         .unwrap();
 
-    // run until BobState(BuySig) is received
-    retry_until_finish_state_transition(
-        cli_bob_progress_args.clone(),
-        "BobState(BuySig)".to_string(),
-    )
-    .await;
+    // run until BuySigB is received
+    retry_until_state_transition(cli_bob_progress_args.clone(), "BuySigB".to_string()).await;
 
     tokio::time::sleep(time::Duration::from_secs(10)).await;
 
@@ -2643,10 +2556,10 @@ async fn run_swap_bob_maker_manual_monero_sweep(
         .generate_to_address(5, &reusable_btc_address())
         .unwrap();
 
-    // run until the AliceState(Finish) is received
-    retry_until_finish_state_transition(
+    // run until the SuccessSwap is received
+    retry_until_finish_transition(
         cli_alice_progress_args.clone(),
-        "AliceState(Finish(Success(Swapped)))".to_string(),
+        StateReport::FinishA(Outcome::SuccessSwap),
     )
     .await;
 
@@ -2752,20 +2665,12 @@ async fn run_swap(
         .send_to_address(&address, amount, None, None, None, None, None, None)
         .unwrap();
 
-    info!("waiting for AliceState(RefundSigs");
-    retry_until_finish_state_transition(
-        cli_alice_progress_args.clone(),
-        "AliceState(RefundSigs".to_string(),
-    )
-    .await;
+    info!("waiting for RefundSigA");
+    retry_until_state_transition(cli_alice_progress_args.clone(), "RefundSigA".to_string()).await;
 
-    // run until BobState(CoreArb) is received
-    info!("waiting for BobState(CoreArb)");
-    retry_until_finish_state_transition(
-        cli_bob_progress_args.clone(),
-        "BobState(CoreArb)".to_string(),
-    )
-    .await;
+    // run until CoreArbB is received
+    info!("waiting for CoreArbB");
+    retry_until_state_transition(cli_bob_progress_args.clone(), "CoreArbB".to_string()).await;
 
     // run until the funding infos are cleared again
     info!("waiting for the bitcoin funding info to clear");
@@ -2793,12 +2698,8 @@ async fn run_swap(
         .await
         .unwrap();
 
-    // run until BobState(BuySig) is received
-    retry_until_finish_state_transition(
-        cli_bob_progress_args.clone(),
-        "BobState(BuySig)".to_string(),
-    )
-    .await;
+    // run until BuySigB is received
+    retry_until_state_transition(cli_bob_progress_args.clone(), "BuySigB".to_string()).await;
 
     tokio::time::sleep(time::Duration::from_secs(10)).await;
 
@@ -2807,10 +2708,10 @@ async fn run_swap(
         .generate_to_address(5, &reusable_btc_address())
         .unwrap();
 
-    // run until the AliceState(Finish) is received
-    retry_until_finish_state_transition(
+    // run until the SuccessSwap is received
+    retry_until_finish_transition(
         cli_alice_progress_args.clone(),
-        "AliceState(Finish(Success(Swapped)))".to_string(),
+        StateReport::FinishA(Outcome::SuccessSwap),
     )
     .await;
 
@@ -2848,10 +2749,10 @@ async fn run_swap(
         .await
         .unwrap();
 
-    // run until the BobState(Finish) is received
+    // run until SuccessSwap is received
     retry_until_bob_finish_state_transition(
         cli_bob_progress_args.clone(),
-        "BobState(Finish(Success(Swapped)))".to_string(),
+        StateReport::FinishB(Outcome::SuccessSwap),
         monero_regtest.clone(),
     )
     .await;
@@ -3290,28 +3191,37 @@ async fn retry_until_monero_funding_address(
     panic!("timeout before any monero funding address could be retrieved");
 }
 
+fn output_to_progress(stdout: Vec<String>) -> SwapProgress {
+    serde_yaml::from_str(
+        &stdout
+            .iter()
+            .map(|line| format!("{}{}", line, "\n"))
+            .collect::<String>(),
+    )
+    .unwrap()
+}
+
 async fn retry_until_bob_finish_state_transition(
     args: Vec<String>,
-    finish_state: String,
+    finish_state: StateReport,
     monero_regtest: monero_rpc::RegtestDaemonJsonRpcClient,
-) -> Vec<String> {
+) -> bool {
     for _ in 0..ALLOWED_RETRIES {
         let (stdout, _stderr) = run("../swap-cli", args.clone()).unwrap();
 
-        let bob_finish: Vec<String> = stdout
-            .iter()
-            .filter_map(|element| {
-                info!("cli: {}", element);
-                if element.contains(&finish_state) {
-                    Some(element.to_string())
+        let progress = output_to_progress(stdout);
+        if progress.progress.iter().any(|v| {
+            if let ProgressEvent::StateTransition(StateTransition { new_state, .. }) = v {
+                if *new_state == finish_state {
+                    true
                 } else {
-                    None
+                    false
                 }
-            })
-            .collect();
-
-        if !bob_finish.is_empty() {
-            return bob_finish;
+            } else {
+                false
+            }
+        }) {
+            return true;
         }
 
         monero_regtest
@@ -3327,27 +3237,46 @@ async fn retry_until_bob_finish_state_transition(
     );
 }
 
-async fn retry_until_finish_state_transition(
-    args: Vec<String>,
-    finish_state: String,
-) -> Vec<String> {
+async fn retry_until_state_transition(args: Vec<String>, finish_state: String) -> bool {
     for _ in 0..ALLOWED_RETRIES {
         let (stdout, _stderr) = run("../swap-cli", args.clone()).unwrap();
 
-        let alice_finish: Vec<String> = stdout
-            .iter()
-            .filter_map(|element| {
-                info!("cli: {}", element);
-                if element.contains(&finish_state) {
-                    Some(element.to_string())
+        let progress = output_to_progress(stdout);
+        if progress.progress.iter().any(|v| {
+            if let ProgressEvent::StateTransition(_) = v {
+                v.to_string().contains(&finish_state)
+            } else {
+                false
+            }
+        }) {
+            return true;
+        }
+
+        tokio::time::sleep(time::Duration::from_secs(1)).await;
+    }
+    panic!(
+        "timeout before finish state {:?} could be retrieved",
+        finish_state
+    );
+}
+
+async fn retry_until_finish_transition(args: Vec<String>, finish_state: StateReport) -> bool {
+    for _ in 0..ALLOWED_RETRIES {
+        let (stdout, _stderr) = run("../swap-cli", args.clone()).unwrap();
+
+        let progress = output_to_progress(stdout);
+        if progress.progress.iter().any(|v| {
+            if let ProgressEvent::StateTransition(StateTransition { new_state, .. }) = v {
+                if *new_state == finish_state {
+                    true
                 } else {
-                    None
+                    false
                 }
-            })
-            .collect();
-
-        if !alice_finish.is_empty() {
-            return alice_finish;
+            } else {
+                false
+            }
+        }) {
+            return true;
         }
 
         tokio::time::sleep(time::Duration::from_secs(1)).await;