From 6c674a8530a8d46799af0c1cb1f21723db7a7fd0 Mon Sep 17 00:00:00 2001 From: Mariusz Klochowicz Date: Fri, 3 Dec 2021 12:52:24 +1030 Subject: [PATCH] Add test covering collaborative close To minimise the noise, create a function that "picks up" from the CfdState::Open. --- daemon/src/model/cfd.rs | 2 +- daemon/tests/happy_path.rs | 105 +++++++++++++++++++++++++++++- daemon/tests/harness/mocks/mod.rs | 7 ++ daemon/tests/harness/mod.rs | 37 +++++++++++ 4 files changed, 148 insertions(+), 3 deletions(-) diff --git a/daemon/src/model/cfd.rs b/daemon/src/model/cfd.rs index a2d028c..6d6c20b 100644 --- a/daemon/src/model/cfd.rs +++ b/daemon/src/model/cfd.rs @@ -1632,7 +1632,7 @@ pub struct CollaborativeSettlement { impl CollaborativeSettlement { pub fn new(tx: Transaction, own_script_pubkey: Script, price: Price) -> Result { // Falls back to Amount::ZERO in case we don't find an output that matches out script pubkey - // The assumption is, that this can happen for cases where we were liuqidated + // The assumption is, that this can happen for cases where we were liquidated let payout = match tx .output .iter() diff --git a/daemon/tests/happy_path.rs b/daemon/tests/happy_path.rs index b2dbc7b..2b01383 100644 --- a/daemon/tests/happy_path.rs +++ b/daemon/tests/happy_path.rs @@ -2,10 +2,11 @@ use std::time::Duration; use crate::harness::flow::{is_next_none, next, next_cfd, next_order, next_some}; use crate::harness::{ - deliver_lock_finality_event, dummy_new_order, init_tracing, start_both, Maker, MakerConfig, - Taker, TakerConfig, + deliver_close_finality_event, deliver_lock_finality_event, dummy_new_order, init_tracing, + start_both, Maker, MakerConfig, Taker, TakerConfig, }; use daemon::connection::ConnectionStatus; +use daemon::model::cfd::OrderId; use daemon::model::Usd; use daemon::projection::{CfdState, Identity}; use maia::secp256k1_zkp::schnorrsig; @@ -125,6 +126,41 @@ async fn taker_takes_order_and_maker_accepts_and_contract_setup() { assert!(matches!(maker_cfd.state, CfdState::Open { .. })); } +#[tokio::test] +async fn collaboratively_close_an_open_cfd() { + let _guard = init_tracing(); + let (mut maker, mut taker, order_id) = start_from_open_cfd_state().await; + + taker.propose_settlement(order_id).await; + + let (taker_cfd, maker_cfd) = next_cfd(taker.cfd_feed(), maker.cfd_feed()).await.unwrap(); + assert!(matches!( + taker_cfd.state, + CfdState::OutgoingSettlementProposal { .. } + )); + assert!(matches!( + maker_cfd.state, + CfdState::IncomingSettlementProposal { .. } + )); + + maker.mocks.mock_monitor_collaborative_settlement().await; + taker.mocks.mock_monitor_collaborative_settlement().await; + + maker.accept_settlement_proposal(order_id).await; + sleep(Duration::from_secs(5)).await; // need to wait a bit until both transition + + let (taker_cfd, maker_cfd) = next_cfd(taker.cfd_feed(), maker.cfd_feed()).await.unwrap(); + assert!(matches!(taker_cfd.state, CfdState::PendingClose { .. })); + assert!(matches!(maker_cfd.state, CfdState::PendingClose { .. })); + + deliver_close_finality_event(&maker, &taker, order_id).await; + sleep(Duration::from_secs(5)).await; // need to wait a bit until both transition + + let (taker_cfd, maker_cfd) = next_cfd(taker.cfd_feed(), maker.cfd_feed()).await.unwrap(); + assert!(matches!(taker_cfd.state, CfdState::Closed { .. })); + assert!(matches!(maker_cfd.state, CfdState::Closed { .. })); +} + #[tokio::test] async fn taker_notices_lack_of_maker() { let short_interval = Duration::from_secs(1); @@ -186,3 +222,68 @@ async fn maker_notices_lack_of_taker() { next(maker.connected_takers_feed()).await.unwrap() ); } + +/// Hide the implementation detail of arriving at the Cfd open state. +/// Useful when reading tests that should start at this point. +/// For convenience, returns also OrderId of the opened Cfd. +async fn start_from_open_cfd_state() -> (Maker, Taker, OrderId) { + let heartbeat_interval = Duration::from_secs(60); + let maker_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let mut maker = Maker::start( + &MakerConfig::default().with_heartbeat_interval(heartbeat_interval), + maker_listener, + ) + .await; + let mut taker = Taker::start( + &TakerConfig::default().with_heartbeat_timeout(heartbeat_interval * 2), + maker.listen_addr, + maker.identity_pk, + ) + .await; + + is_next_none(taker.order_feed()).await.unwrap(); + + maker.publish_order(dummy_new_order()).await; + + let (_, received) = next_order(maker.order_feed(), taker.order_feed()) + .await + .unwrap(); + + taker.mocks.mock_oracle_announcement().await; + maker.mocks.mock_oracle_announcement().await; + + taker.take_order(received.clone(), Usd::new(dec!(5))).await; + let (_, _) = next_cfd(taker.cfd_feed(), maker.cfd_feed()).await.unwrap(); + + maker.mocks.mock_party_params().await; + taker.mocks.mock_party_params().await; + + maker.mocks.mock_monitor_oracle_attestation().await; + taker.mocks.mock_monitor_oracle_attestation().await; + + maker.mocks.mock_oracle_monitor_attestation().await; + taker.mocks.mock_oracle_monitor_attestation().await; + + maker.mocks.mock_monitor_start_monitoring().await; + taker.mocks.mock_monitor_start_monitoring().await; + + maker.accept_take_request(received.clone()).await; + + let (taker_cfd, maker_cfd) = next_cfd(taker.cfd_feed(), maker.cfd_feed()).await.unwrap(); + assert!(matches!(taker_cfd.state, CfdState::ContractSetup { .. })); + assert!(matches!(maker_cfd.state, CfdState::ContractSetup { .. })); + + maker.mocks.mock_wallet_sign_and_broadcast().await; + taker.mocks.mock_wallet_sign_and_broadcast().await; + + let (taker_cfd, maker_cfd) = next_cfd(taker.cfd_feed(), maker.cfd_feed()).await.unwrap(); + assert!(matches!(taker_cfd.state, CfdState::PendingOpen { .. })); + assert!(matches!(maker_cfd.state, CfdState::PendingOpen { .. })); + + deliver_lock_finality_event(&maker, &taker, received.id).await; + + let (taker_cfd, maker_cfd) = next_cfd(taker.cfd_feed(), maker.cfd_feed()).await.unwrap(); + assert!(matches!(taker_cfd.state, CfdState::Open { .. })); + assert!(matches!(maker_cfd.state, CfdState::Open { .. })); + (maker, taker, received.id) +} diff --git a/daemon/tests/harness/mocks/mod.rs b/daemon/tests/harness/mocks/mod.rs index 5a67b65..e21f48c 100644 --- a/daemon/tests/harness/mocks/mod.rs +++ b/daemon/tests/harness/mocks/mod.rs @@ -82,6 +82,13 @@ impl Mocks { .expect_start_monitoring() .return_const(()); } + + pub async fn mock_monitor_collaborative_settlement(&mut self) { + self.monitor() + .await + .expect_collaborative_settlement() + .return_const(()); + } } impl Default for Mocks { diff --git a/daemon/tests/harness/mod.rs b/daemon/tests/harness/mod.rs index 69ce9ed..ae8901b 100644 --- a/daemon/tests/harness/mod.rs +++ b/daemon/tests/harness/mod.rs @@ -230,6 +230,15 @@ impl Maker { .unwrap() .unwrap(); } + + pub async fn accept_settlement_proposal(&self, order_id: OrderId) { + self.system + .cfd_actor_addr + .send(maker_cfd::AcceptSettlement { order_id }) + .await + .unwrap() + .unwrap(); + } } /// Taker Test Setup @@ -321,6 +330,18 @@ impl Taker { .unwrap() .unwrap(); } + + pub async fn propose_settlement(&self, order_id: OrderId) { + self.system + .cfd_actor_addr + .send(taker_cfd::ProposeSettlement { + order_id, + current_price: dummy_price(), + }) + .await + .unwrap() + .unwrap(); + } } /// Deliver the event that provokes the transition to cfd's "Open" state @@ -339,6 +360,22 @@ pub async fn deliver_lock_finality_event(maker: &Maker, taker: &Taker, id: Order .unwrap(); } +/// Deliver the event that provokes the transition to cfd's "Close" state +pub async fn deliver_close_finality_event(maker: &Maker, taker: &Taker, id: OrderId) { + taker + .system + .cfd_actor_addr + .send(daemon::monitor::Event::CloseFinality(id)) + .await + .unwrap(); + maker + .system + .cfd_actor_addr + .send(daemon::monitor::Event::CloseFinality(id)) + .await + .unwrap(); +} + async fn in_memory_db() -> SqlitePool { // Note: Every :memory: database is distinct from every other. So, opening two database // connections each with the filename ":memory:" will create two independent in-memory