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 #[cfg_attr(feature = "use-serde", serde(default))]
52 pub contract_flags: u8,
53 pub(crate) keys_id: KeysId,
55}
56
57impl OfferedContract {
58 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 #[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 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}