Skip to main content

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