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