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    /// Keys Id for generating the signers
51    pub(crate) keys_id: KeysId,
52}
53
54impl OfferedContract {
55    /// Validate that the contract info covers all the possible outcomes that
56    /// can be attested by the oracle(s).
57    pub fn validate(&self) -> Result<(), crate::error::Error> {
58        ddk_dlc::util::validate_fee_rate(self.fee_rate_per_vb).map_err(|_| {
59            crate::error::Error::InvalidParameters("Fee rate is too high".to_string())
60        })?;
61
62        for info in &self.contract_info {
63            info.validate()?;
64            let payouts = match &info.contract_descriptor {
65                ContractDescriptor::Enum(e) => e.get_payouts(),
66                ContractDescriptor::Numerical(e) => e.get_payouts(self.total_collateral)?,
67            };
68            let valid = payouts
69                .iter()
70                .all(|p| p.accept + p.offer == self.total_collateral);
71            if !valid {
72                return Err(crate::error::Error::InvalidParameters(
73                    "Sum of payout doesn't equal total collateral".to_string(),
74                ));
75            }
76        }
77
78        Ok(())
79    }
80
81    /// Creates a new [`OfferedContract`] from the given parameters.
82    #[allow(clippy::too_many_arguments)]
83    pub fn new(
84        id: ContractId,
85        contract: &ContractInput,
86        oracle_announcements: Vec<Vec<OracleAnnouncement>>,
87        offer_params: &PartyParams,
88        funding_inputs: &[FundingInput],
89        counter_party: &PublicKey,
90        refund_delay: u32,
91        cet_locktime: u32,
92        keys_id: KeysId,
93    ) -> Self {
94        let total_collateral = contract.offer_collateral + contract.accept_collateral;
95
96        assert_eq!(contract.contract_infos.len(), oracle_announcements.len());
97
98        let latest_maturity = crate::utils::get_latest_maturity_date(&oracle_announcements)
99            .expect("to be able to retrieve latest maturity date");
100
101        let fund_output_serial_id = get_new_serial_id();
102        let contract_info = contract
103            .contract_infos
104            .iter()
105            .zip(oracle_announcements)
106            .map(|(x, y)| ContractInfo {
107                contract_descriptor: x.contract_descriptor.clone(),
108                oracle_announcements: y,
109                threshold: x.oracles.threshold as usize,
110            })
111            .collect::<Vec<ContractInfo>>();
112        OfferedContract {
113            id,
114            is_offer_party: true,
115            contract_info,
116            offer_params: offer_params.clone(),
117            total_collateral,
118            funding_inputs: funding_inputs.to_vec(),
119            fund_output_serial_id,
120            fee_rate_per_vb: contract.fee_rate,
121            cet_locktime,
122            refund_locktime: latest_maturity + refund_delay,
123            counter_party: *counter_party,
124            keys_id,
125        }
126    }
127
128    /// Convert an [`OfferDlc`] message to an [`OfferedContract`].
129    pub fn try_from_offer_dlc(
130        offer_dlc: &OfferDlc,
131        counter_party: PublicKey,
132        keys_id: KeysId,
133    ) -> Result<OfferedContract, crate::conversion_utils::Error> {
134        let contract_info = get_contract_info_and_announcements(&offer_dlc.contract_info)?;
135
136        let (inputs, input_amount) = get_tx_input_infos(&offer_dlc.funding_inputs)?;
137        let dlc_inputs = get_dlc_inputs_from_funding_inputs(&offer_dlc.funding_inputs);
138
139        Ok(OfferedContract {
140            id: offer_dlc.temporary_contract_id,
141            is_offer_party: false,
142            contract_info,
143            offer_params: PartyParams {
144                fund_pubkey: offer_dlc.funding_pubkey,
145                change_script_pubkey: offer_dlc.change_spk.clone(),
146                change_serial_id: offer_dlc.change_serial_id,
147                payout_script_pubkey: offer_dlc.payout_spk.clone(),
148                payout_serial_id: offer_dlc.payout_serial_id,
149                collateral: offer_dlc.offer_collateral,
150                inputs,
151                dlc_inputs,
152                input_amount,
153            },
154            cet_locktime: offer_dlc.cet_locktime,
155            refund_locktime: offer_dlc.refund_locktime,
156            fee_rate_per_vb: offer_dlc.fee_rate_per_vb,
157            fund_output_serial_id: offer_dlc.fund_output_serial_id,
158            funding_inputs: offer_dlc.funding_inputs.clone(),
159            total_collateral: offer_dlc.contract_info.get_total_collateral(),
160            counter_party,
161            keys_id,
162        })
163    }
164}
165
166impl From<&OfferedContract> for OfferDlc {
167    fn from(offered_contract: &OfferedContract) -> OfferDlc {
168        OfferDlc {
169            protocol_version: PROTOCOL_VERSION,
170            temporary_contract_id: offered_contract.id,
171            contract_flags: 0,
172            chain_hash: BITCOIN_CHAINHASH,
173            contract_info: offered_contract.into(),
174            funding_pubkey: offered_contract.offer_params.fund_pubkey,
175            payout_spk: offered_contract.offer_params.payout_script_pubkey.clone(),
176            payout_serial_id: offered_contract.offer_params.payout_serial_id,
177            offer_collateral: offered_contract.offer_params.collateral,
178            funding_inputs: offered_contract.funding_inputs.clone(),
179            change_spk: offered_contract.offer_params.change_script_pubkey.clone(),
180            change_serial_id: offered_contract.offer_params.change_serial_id,
181            cet_locktime: offered_contract.cet_locktime,
182            refund_locktime: offered_contract.refund_locktime,
183            fee_rate_per_vb: offered_contract.fee_rate_per_vb,
184            fund_output_serial_id: offered_contract.fund_output_serial_id,
185        }
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    fn validate_offer_test_common(input: &str) {
194        let offer: OfferedContract = serde_json::from_str(input).unwrap();
195        assert!(offer.validate().is_err());
196    }
197
198    #[test]
199    fn offer_enum_missing_payout() {
200        validate_offer_test_common(include_str!(
201            "../../test_inputs/offer_enum_missing_payout.json"
202        ));
203    }
204
205    #[test]
206    fn offer_enum_oracle_with_diff_payout() {
207        validate_offer_test_common(include_str!(
208            "../../test_inputs/offer_enum_oracle_with_diff_payout.json"
209        ));
210    }
211
212    #[test]
213    fn offer_numerical_bad_first_payout() {
214        validate_offer_test_common(include_str!(
215            "../../test_inputs/offer_numerical_bad_first_payout.json"
216        ));
217    }
218
219    #[test]
220    fn offer_numerical_bad_last_payout() {
221        validate_offer_test_common(include_str!(
222            "../../test_inputs/offer_numerical_bad_last_payout.json"
223        ));
224    }
225
226    #[test]
227    fn offer_numerical_non_continuous() {
228        validate_offer_test_common(include_str!(
229            "../../test_inputs/offer_numerical_non_continuous.json"
230        ));
231    }
232
233    #[test]
234    fn offer_enum_collateral_not_equal_payout() {
235        validate_offer_test_common(include_str!(
236            "../../test_inputs/offer_enum_collateral_not_equal_payout.json"
237        ));
238    }
239
240    #[test]
241    fn offer_numerical_collateral_less_than_payout() {
242        validate_offer_test_common(include_str!(
243            "../../test_inputs/offer_numerical_collateral_less_than_payout.json"
244        ));
245    }
246
247    #[test]
248    fn offer_numerical_invalid_rounding_interval() {
249        validate_offer_test_common(include_str!(
250            "../../test_inputs/offer_numerical_invalid_rounding_interval.json"
251        ));
252    }
253
254    #[test]
255    fn offer_numerical_empty_rounding_interval() {
256        validate_offer_test_common(include_str!(
257            "../../test_inputs/offer_numerical_empty_rounding_interval.json"
258        ));
259    }
260}