Skip to main content

ddk_manager/contract/
contract_input.rs

1//! #ContractInput
2
3use 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/// Oracle information required for the initial creation of a contract.
14#[derive(Debug, Clone)]
15#[cfg_attr(
16    feature = "use-serde",
17    derive(Serialize, Deserialize),
18    serde(rename_all = "camelCase")
19)]
20pub struct OracleInput {
21    /// The set of public keys for each of the used oracles.
22    pub public_keys: Vec<XOnlyPublicKey>,
23    /// The id of the event being used for the contract. Note that at the moment
24    /// a single event id is used, while multiple ids would be preferable.
25    pub event_id: String,
26    /// The number of oracles that need to provide attestations satisfying the
27    /// contract conditions to be able to close the contract.
28    pub threshold: u16,
29}
30
31impl OracleInput {
32    /// Checks whether the data within the struct is consistent.
33    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/// Represents the contract specifications.
57#[derive(Debug, Clone)]
58#[cfg_attr(
59    feature = "use-serde",
60    derive(Serialize, Deserialize),
61    serde(rename_all = "camelCase")
62)]
63pub struct ContractInputInfo {
64    /// The contract conditions.
65    pub contract_descriptor: ContractDescriptor,
66    /// The oracle information.
67    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)]
76/// Contains all the information necessary for the initialization of a DLC.
77pub struct ContractInput {
78    /// The collateral for the offering party.
79    pub offer_collateral: Amount,
80    /// The collateral for the accepting party.
81    pub accept_collateral: Amount,
82    /// The fee rate used to construct the transactions.
83    pub fee_rate: u64,
84    /// The set of contract that make up the DLC (a single DLC can be based
85    /// on multiple contracts).
86    pub contract_infos: Vec<ContractInputInfo>,
87}
88
89impl ContractInput {
90    /// Validate the contract input parameters
91    pub fn validate(&self) -> Result<(), Error> {
92        if self.offer_collateral < DUST_LIMIT {
93            return Err(Error::InvalidParameters(
94                "Offer collateral must be greater than dust limit.".to_string(),
95            ));
96        }
97
98        let total_collateral = self.offer_collateral + self.accept_collateral;
99        if total_collateral < DUST_LIMIT {
100            return Err(Error::InvalidParameters(
101                "Total collateral must be greater than dust limit.".to_string(),
102            ));
103        }
104
105        if self.contract_infos.is_empty() {
106            return Err(Error::InvalidParameters(
107                "Need at least one contract info".to_string(),
108            ));
109        }
110
111        for contract_info in &self.contract_infos {
112            contract_info.oracles.validate()?;
113        }
114
115        ddk_dlc::util::validate_fee_rate(self.fee_rate)
116            .map_err(|_| Error::InvalidParameters("Fee rate too high.".to_string()))
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use ddk_dlc::{EnumerationPayout, Payout};
123    use secp256k1_zkp::{Keypair, SecretKey, SECP256K1};
124
125    use crate::contract::enum_descriptor::EnumDescriptor;
126
127    use super::*;
128
129    fn get_base_input() -> ContractInput {
130        ContractInput {
131            offer_collateral: Amount::from_sat(1000000),
132            accept_collateral: Amount::from_sat(2000000),
133            fee_rate: 1234,
134            contract_infos: vec![ContractInputInfo {
135                contract_descriptor: ContractDescriptor::Enum(EnumDescriptor {
136                    outcome_payouts: vec![
137                        EnumerationPayout {
138                            outcome: "A".to_string(),
139                            payout: Payout {
140                                offer: Amount::from_sat(3000000),
141                                accept: Amount::ZERO,
142                            },
143                        },
144                        EnumerationPayout {
145                            outcome: "B".to_string(),
146                            payout: Payout {
147                                offer: Amount::ZERO,
148                                accept: Amount::from_sat(3000000),
149                            },
150                        },
151                    ],
152                }),
153                oracles: OracleInput {
154                    public_keys: vec![
155                        XOnlyPublicKey::from_keypair(&Keypair::from_secret_key(
156                            SECP256K1,
157                            &SecretKey::from_slice(&secp256k1_zkp::constants::ONE).unwrap(),
158                        ))
159                        .0,
160                    ],
161                    event_id: "1234".to_string(),
162                    threshold: 1,
163                },
164            }],
165        }
166    }
167
168    #[test]
169    fn valid_contract_input_is_valid() {
170        let input = get_base_input();
171        input.validate().expect("the contract input to be valid.");
172    }
173
174    #[test]
175    fn no_contract_info_contract_input_is_not_valid() {
176        let mut input = get_base_input();
177        input.contract_infos.clear();
178        input
179            .validate()
180            .expect_err("the contract input to be invalid.");
181    }
182
183    #[test]
184    fn invalid_fee_rate_contract_input_is_not_valid() {
185        let mut input = get_base_input();
186        input.fee_rate = 251 * 25;
187        input
188            .validate()
189            .expect_err("the contract input to be invalid.");
190    }
191
192    #[test]
193    fn no_public_keys_oracle_input_contract_input_is_not_valid() {
194        let mut input = get_base_input();
195        input.contract_infos[0].oracles.public_keys.clear();
196        input
197            .validate()
198            .expect_err("the contract input to be invalid.");
199    }
200
201    #[test]
202    fn invalid_oracle_info_threshold_oracle_input_contract_input_is_not_valid() {
203        let mut input = get_base_input();
204        input.contract_infos[0].oracles.threshold = 2;
205        input
206            .validate()
207            .expect_err("the contract input to be invalid.");
208    }
209
210    #[test]
211    fn invalid_oracle_info_threshold_zero() {
212        let mut input = get_base_input();
213        input.contract_infos[0].oracles.threshold = 0;
214        input
215            .validate()
216            .expect_err("the contract input to be invalid.");
217    }
218}