ddk_manager/contract/
offered_contract.rs

1//! #OfferedContract
2
3use crate::conversion_utils::{
4    get_contract_info_and_announcements, get_tx_input_infos, BITCOIN_CHAINHASH, PROTOCOL_VERSION,
5};
6use crate::utils::get_new_serial_id;
7
8use super::contract_info::ContractInfo;
9use super::contract_input::ContractInput;
10use super::ContractDescriptor;
11use crate::{ContractId, KeysId};
12use dlc::PartyParams;
13use dlc_messages::oracle_msgs::OracleAnnouncement;
14use dlc_messages::{FundingInput, OfferDlc};
15use secp256k1_zkp::PublicKey;
16
17/// Contains information about a contract that was offered.
18#[derive(Clone, Debug)]
19#[cfg_attr(
20    feature = "use-serde",
21    derive(serde::Serialize, serde::Deserialize),
22    serde(rename_all = "camelCase")
23)]
24pub struct OfferedContract {
25    /// The temporary id of the contract.
26    pub id: [u8; 32],
27    /// Indicated whether the contract was proposed or received.
28    pub is_offer_party: bool,
29    /// The set of contract information that are used to generate CET and
30    /// adaptor signatures.
31    pub contract_info: Vec<ContractInfo>,
32    /// The public key of the counter-party's node.
33    pub counter_party: PublicKey,
34    /// The parameters of the offering party.
35    pub offer_params: PartyParams,
36    /// The sum of both parties collateral.
37    pub total_collateral: u64,
38    /// Information about the offering party's funding inputs.
39    pub funding_inputs: Vec<FundingInput>,
40    /// The serial id of the fund output used for output ordering.
41    pub fund_output_serial_id: u64,
42    /// The fee rate to be used to construct the DLC transactions.
43    pub fee_rate_per_vb: u64,
44    /// The time at which the contract is expected to be closeable.
45    pub cet_locktime: u32,
46    /// The time at which the contract becomes refundable.
47    pub refund_locktime: u32,
48    /// Keys Id for generating the signers
49    pub(crate) keys_id: KeysId,
50}
51
52impl OfferedContract {
53    /// Validate that the contract info covers all the possible outcomes that
54    /// can be attested by the oracle(s).
55    pub fn validate(&self) -> Result<(), crate::error::Error> {
56        dlc::util::validate_fee_rate(self.fee_rate_per_vb).map_err(|_| {
57            crate::error::Error::InvalidParameters("Fee rate is too high".to_string())
58        })?;
59
60        for info in &self.contract_info {
61            info.validate()?;
62            let payouts = match &info.contract_descriptor {
63                ContractDescriptor::Enum(e) => e.get_payouts(),
64                ContractDescriptor::Numerical(e) => e.get_payouts(self.total_collateral)?,
65            };
66            let valid = payouts
67                .iter()
68                .all(|p| p.accept + p.offer == self.total_collateral);
69            if !valid {
70                return Err(crate::error::Error::InvalidParameters(
71                    "Sum of payout doesn't equal total collateral".to_string(),
72                ));
73            }
74        }
75
76        Ok(())
77    }
78
79    /// Creates a new [`OfferedContract`] from the given parameters.
80    #[allow(clippy::too_many_arguments)]
81    pub fn new(
82        id: ContractId,
83        contract: &ContractInput,
84        oracle_announcements: Vec<Vec<OracleAnnouncement>>,
85        offer_params: &PartyParams,
86        funding_inputs: &[FundingInput],
87        counter_party: &PublicKey,
88        refund_delay: u32,
89        cet_locktime: u32,
90        keys_id: KeysId,
91    ) -> Self {
92        let total_collateral = contract.offer_collateral + contract.accept_collateral;
93
94        assert_eq!(contract.contract_infos.len(), oracle_announcements.len());
95
96        let latest_maturity = crate::utils::get_latest_maturity_date(&oracle_announcements)
97            .expect("to be able to retrieve latest maturity date");
98
99        let fund_output_serial_id = get_new_serial_id();
100        let contract_info = contract
101            .contract_infos
102            .iter()
103            .zip(oracle_announcements)
104            .map(|(x, y)| ContractInfo {
105                contract_descriptor: x.contract_descriptor.clone(),
106                oracle_announcements: y,
107                threshold: x.oracles.threshold as usize,
108            })
109            .collect::<Vec<ContractInfo>>();
110        OfferedContract {
111            id,
112            is_offer_party: true,
113            contract_info,
114            offer_params: offer_params.clone(),
115            total_collateral,
116            funding_inputs: funding_inputs.to_vec(),
117            fund_output_serial_id,
118            fee_rate_per_vb: contract.fee_rate,
119            cet_locktime,
120            refund_locktime: latest_maturity + refund_delay,
121            counter_party: *counter_party,
122            keys_id,
123        }
124    }
125
126    /// Convert an [`OfferDlc`] message to an [`OfferedContract`].
127    pub fn try_from_offer_dlc(
128        offer_dlc: &OfferDlc,
129        counter_party: PublicKey,
130        keys_id: KeysId,
131    ) -> Result<OfferedContract, crate::conversion_utils::Error> {
132        let contract_info = get_contract_info_and_announcements(&offer_dlc.contract_info)?;
133
134        let (inputs, input_amount) = get_tx_input_infos(&offer_dlc.funding_inputs)?;
135
136        Ok(OfferedContract {
137            id: offer_dlc.temporary_contract_id,
138            is_offer_party: false,
139            contract_info,
140            offer_params: PartyParams {
141                fund_pubkey: offer_dlc.funding_pubkey,
142                change_script_pubkey: offer_dlc.change_spk.clone(),
143                change_serial_id: offer_dlc.change_serial_id,
144                payout_script_pubkey: offer_dlc.payout_spk.clone(),
145                payout_serial_id: offer_dlc.payout_serial_id,
146                collateral: offer_dlc.offer_collateral,
147                inputs,
148                input_amount,
149            },
150            cet_locktime: offer_dlc.cet_locktime,
151            refund_locktime: offer_dlc.refund_locktime,
152            fee_rate_per_vb: offer_dlc.fee_rate_per_vb,
153            fund_output_serial_id: offer_dlc.fund_output_serial_id,
154            funding_inputs: offer_dlc.funding_inputs.clone(),
155            total_collateral: offer_dlc.contract_info.get_total_collateral(),
156            counter_party,
157            keys_id,
158        })
159    }
160}
161
162impl From<&OfferedContract> for OfferDlc {
163    fn from(offered_contract: &OfferedContract) -> OfferDlc {
164        OfferDlc {
165            protocol_version: PROTOCOL_VERSION,
166            temporary_contract_id: offered_contract.id,
167            contract_flags: 0,
168            chain_hash: BITCOIN_CHAINHASH,
169            contract_info: offered_contract.into(),
170            funding_pubkey: offered_contract.offer_params.fund_pubkey,
171            payout_spk: offered_contract.offer_params.payout_script_pubkey.clone(),
172            payout_serial_id: offered_contract.offer_params.payout_serial_id,
173            offer_collateral: offered_contract.offer_params.collateral,
174            funding_inputs: offered_contract.funding_inputs.clone(),
175            change_spk: offered_contract.offer_params.change_script_pubkey.clone(),
176            change_serial_id: offered_contract.offer_params.change_serial_id,
177            cet_locktime: offered_contract.cet_locktime,
178            refund_locktime: offered_contract.refund_locktime,
179            fee_rate_per_vb: offered_contract.fee_rate_per_vb,
180            fund_output_serial_id: offered_contract.fund_output_serial_id,
181        }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    fn validate_offer_test_common(input: &str) {
190        let offer: OfferedContract = serde_json::from_str(input).unwrap();
191        assert!(offer.validate().is_err());
192    }
193
194    #[test]
195    fn offer_enum_missing_payout() {
196        validate_offer_test_common(include_str!(
197            "../../test_inputs/offer_enum_missing_payout.json"
198        ));
199    }
200
201    #[test]
202    fn offer_enum_oracle_with_diff_payout() {
203        validate_offer_test_common(include_str!(
204            "../../test_inputs/offer_enum_oracle_with_diff_payout.json"
205        ));
206    }
207
208    #[test]
209    fn offer_numerical_bad_first_payout() {
210        validate_offer_test_common(include_str!(
211            "../../test_inputs/offer_numerical_bad_first_payout.json"
212        ));
213    }
214
215    #[test]
216    fn offer_numerical_bad_last_payout() {
217        validate_offer_test_common(include_str!(
218            "../../test_inputs/offer_numerical_bad_last_payout.json"
219        ));
220    }
221
222    #[test]
223    fn offer_numerical_non_continuous() {
224        validate_offer_test_common(include_str!(
225            "../../test_inputs/offer_numerical_non_continuous.json"
226        ));
227    }
228
229    #[test]
230    fn offer_enum_collateral_not_equal_payout() {
231        validate_offer_test_common(include_str!(
232            "../../test_inputs/offer_enum_collateral_not_equal_payout.json"
233        ));
234    }
235
236    #[test]
237    fn offer_numerical_collateral_less_than_payout() {
238        validate_offer_test_common(include_str!(
239            "../../test_inputs/offer_numerical_collateral_less_than_payout.json"
240        ));
241    }
242
243    #[test]
244    fn offer_numerical_invalid_rounding_interval() {
245        validate_offer_test_common(include_str!(
246            "../../test_inputs/offer_numerical_invalid_rounding_interval.json"
247        ));
248    }
249
250    #[test]
251    fn offer_numerical_empty_rounding_interval() {
252        validate_offer_test_common(include_str!(
253            "../../test_inputs/offer_numerical_empty_rounding_interval.json"
254        ));
255    }
256}