1use 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#[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 pub id: [u8; 32],
29 pub is_offer_party: bool,
31 pub contract_info: Vec<ContractInfo>,
34 pub counter_party: PublicKey,
36 pub offer_params: PartyParams,
38 pub total_collateral: Amount,
40 pub funding_inputs: Vec<FundingInput>,
42 pub fund_output_serial_id: u64,
44 pub fee_rate_per_vb: u64,
46 pub cet_locktime: u32,
48 pub refund_locktime: u32,
50 pub(crate) keys_id: KeysId,
52}
53
54impl OfferedContract {
55 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 #[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 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}