ddk_manager/contract/
contract_input.rs1use crate::error::Error;
4
5use super::ContractDescriptor;
6use bitcoin::Amount;
7use secp256k1_zkp::XOnlyPublicKey;
8#[cfg(feature = "use-serde")]
9use serde::{Deserialize, Serialize};
10
11const DUST_LIMIT: Amount = Amount::from_sat(1000);
12
13#[derive(Debug, Clone)]
15#[cfg_attr(
16 feature = "use-serde",
17 derive(Serialize, Deserialize),
18 serde(rename_all = "camelCase")
19)]
20pub struct OracleInput {
21 pub public_keys: Vec<XOnlyPublicKey>,
23 pub event_id: String,
26 pub threshold: u16,
29}
30
31impl OracleInput {
32 pub fn validate(&self) -> Result<(), Error> {
34 if self.public_keys.is_empty() {
35 return Err(Error::InvalidParameters(
36 "OracleInput must have at least one public key.".to_string(),
37 ));
38 }
39
40 if self.threshold > self.public_keys.len() as u16 {
41 return Err(Error::InvalidParameters(
42 "Threshold cannot be larger than number of oracles.".to_string(),
43 ));
44 }
45
46 if self.threshold == 0 {
47 return Err(Error::InvalidParameters(
48 "Threshold cannot be zero.".to_string(),
49 ));
50 }
51
52 Ok(())
53 }
54}
55
56#[derive(Debug, Clone)]
58#[cfg_attr(
59 feature = "use-serde",
60 derive(Serialize, Deserialize),
61 serde(rename_all = "camelCase")
62)]
63pub struct ContractInputInfo {
64 pub contract_descriptor: ContractDescriptor,
66 pub oracles: OracleInput,
68}
69
70#[derive(Debug, Clone)]
71#[cfg_attr(
72 feature = "use-serde",
73 derive(Serialize, Deserialize),
74 serde(rename_all = "camelCase")
75)]
76pub struct ContractInput {
78 pub offer_collateral: Amount,
80 pub accept_collateral: Amount,
82 pub fee_rate: u64,
84 #[cfg_attr(feature = "use-serde", serde(default))]
86 pub contract_flags: u8,
87 pub contract_infos: Vec<ContractInputInfo>,
90}
91
92impl ContractInput {
93 pub fn validate(&self) -> Result<(), Error> {
95 if self.offer_collateral > Amount::ZERO && self.offer_collateral < DUST_LIMIT {
97 return Err(Error::InvalidParameters(
98 "Non-zero offer collateral must be greater than dust limit.".to_string(),
99 ));
100 }
101
102 let total_collateral = self.offer_collateral + self.accept_collateral;
103 if total_collateral < DUST_LIMIT {
104 return Err(Error::InvalidParameters(
105 "Total collateral must be greater than dust limit.".to_string(),
106 ));
107 }
108
109 if self.contract_infos.is_empty() {
110 return Err(Error::InvalidParameters(
111 "Need at least one contract info".to_string(),
112 ));
113 }
114
115 for contract_info in &self.contract_infos {
116 contract_info.oracles.validate()?;
117 }
118
119 ddk_dlc::util::validate_fee_rate(self.fee_rate)
120 .map_err(|_| Error::InvalidParameters("Fee rate too high.".to_string()))
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use ddk_dlc::{EnumerationPayout, Payout};
127 use secp256k1_zkp::{Keypair, SecretKey, SECP256K1};
128
129 use crate::contract::enum_descriptor::EnumDescriptor;
130
131 use super::*;
132
133 fn get_base_input() -> ContractInput {
134 ContractInput {
135 offer_collateral: Amount::from_sat(1000000),
136 accept_collateral: Amount::from_sat(2000000),
137 fee_rate: 1234,
138 contract_flags: 0,
139 contract_infos: vec![ContractInputInfo {
140 contract_descriptor: ContractDescriptor::Enum(EnumDescriptor {
141 outcome_payouts: vec![
142 EnumerationPayout {
143 outcome: "A".to_string(),
144 payout: Payout {
145 offer: Amount::from_sat(3000000),
146 accept: Amount::ZERO,
147 },
148 },
149 EnumerationPayout {
150 outcome: "B".to_string(),
151 payout: Payout {
152 offer: Amount::ZERO,
153 accept: Amount::from_sat(3000000),
154 },
155 },
156 ],
157 }),
158 oracles: OracleInput {
159 public_keys: vec![
160 XOnlyPublicKey::from_keypair(&Keypair::from_secret_key(
161 SECP256K1,
162 &SecretKey::from_slice(&secp256k1_zkp::constants::ONE).unwrap(),
163 ))
164 .0,
165 ],
166 event_id: "1234".to_string(),
167 threshold: 1,
168 },
169 }],
170 }
171 }
172
173 #[test]
174 fn valid_contract_input_is_valid() {
175 let input = get_base_input();
176 input.validate().expect("the contract input to be valid.");
177 }
178
179 #[test]
180 fn no_contract_info_contract_input_is_not_valid() {
181 let mut input = get_base_input();
182 input.contract_infos.clear();
183 input
184 .validate()
185 .expect_err("the contract input to be invalid.");
186 }
187
188 #[test]
189 fn invalid_fee_rate_contract_input_is_not_valid() {
190 let mut input = get_base_input();
191 input.fee_rate = 251 * 25;
192 input
193 .validate()
194 .expect_err("the contract input to be invalid.");
195 }
196
197 #[test]
198 fn no_public_keys_oracle_input_contract_input_is_not_valid() {
199 let mut input = get_base_input();
200 input.contract_infos[0].oracles.public_keys.clear();
201 input
202 .validate()
203 .expect_err("the contract input to be invalid.");
204 }
205
206 #[test]
207 fn invalid_oracle_info_threshold_oracle_input_contract_input_is_not_valid() {
208 let mut input = get_base_input();
209 input.contract_infos[0].oracles.threshold = 2;
210 input
211 .validate()
212 .expect_err("the contract input to be invalid.");
213 }
214
215 #[test]
216 fn invalid_oracle_info_threshold_zero() {
217 let mut input = get_base_input();
218 input.contract_infos[0].oracles.threshold = 0;
219 input
220 .validate()
221 .expect_err("the contract input to be invalid.");
222 }
223}