ree_types/
lib.rs

1extern crate alloc;
2
3use alloc::str::FromStr;
4use candid::CandidType;
5use serde::{Deserialize, Serialize};
6
7mod coin_id;
8mod intention;
9mod pubkey;
10mod txid;
11
12pub mod exchange_interfaces;
13pub mod orchestrator_interfaces;
14
15pub use bitcoin;
16pub use coin_id::CoinId;
17pub use exchange_interfaces::NewBlockInfo;
18pub use ic_cdk;
19pub use intention::*;
20pub use pubkey::Pubkey;
21pub use txid::{TxRecord, Txid};
22
23/// The CoinBalance struct represents a balance of a specific coin type.
24#[derive(
25    CandidType, Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord,
26)]
27pub struct CoinBalance {
28    pub id: CoinId,
29    pub value: u128,
30}
31
32/// The CoinBalances struct is a collection of CoinBalance objects.
33#[derive(CandidType, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, Default)]
34pub struct CoinBalances(Vec<CoinBalance>);
35
36/// The Bitcoin UTXO with Runes coin balances.
37#[derive(CandidType, Eq, PartialEq, Clone, Debug, Deserialize, Serialize)]
38pub struct Utxo {
39    pub txid: Txid,
40    pub vout: u32,
41    pub coins: CoinBalances,
42    pub sats: u64,
43}
44
45impl Utxo {
46    pub fn try_from(
47        outpoint: impl AsRef<str>,
48        coins: CoinBalances,
49        sats: u64,
50    ) -> Result<Self, String> {
51        let parts = outpoint.as_ref().split(':').collect::<Vec<_>>();
52        let txid = parts
53            .get(0)
54            .map(|s| Txid::from_str(s).map_err(|_| "Invalid txid in outpoint."))
55            .transpose()?
56            .ok_or("Invalid txid in outpoint.")?;
57        let vout = parts
58            .get(1)
59            .map(|s| s.parse::<u32>().map_err(|_| "Invalid vout in outpoint."))
60            .transpose()?
61            .ok_or("Invalid vout in outpoint")?;
62        Ok(Utxo {
63            txid,
64            vout,
65            coins,
66            sats,
67        })
68    }
69
70    pub fn outpoint(&self) -> String {
71        format!("{}:{}", self.txid, self.vout)
72    }
73}
74
75impl CoinBalances {
76    pub fn new() -> Self {
77        Self(vec![])
78    }
79
80    pub fn single(coin: CoinBalance) -> Self {
81        Self(vec![coin])
82    }
83
84    pub fn is_empty(&self) -> bool {
85        self.0.is_empty()
86    }
87    //
88    pub fn iter(&self) -> impl Iterator<Item = &CoinBalance> {
89        self.0.iter()
90    }
91    //
92    pub fn add_coin(&mut self, coin: &CoinBalance) {
93        let mut found = false;
94        for existing_coin in &mut self.0 {
95            if existing_coin.id == coin.id {
96                existing_coin.value += coin.value;
97                found = true;
98                break;
99            }
100        }
101        if !found {
102            self.0.push(coin.clone());
103        }
104    }
105    //
106    pub fn subtract_coin(&mut self, coin: &CoinBalance) -> bool {
107        for i in 0..self.0.len() {
108            if self.0[i].id == coin.id {
109                if self.0[i].value >= coin.value {
110                    self.0[i].value -= coin.value;
111                    if self.0[i].value == 0 {
112                        self.0.remove(i);
113                    }
114                    return true;
115                } else {
116                    return false; // Not enough value to remove
117                }
118            }
119        }
120        false // Coin not found
121    }
122    //
123    pub fn value_of(&self, coin_id: &CoinId) -> u128 {
124        for coin in &self.0 {
125            if coin.id == *coin_id {
126                return coin.value;
127            }
128        }
129        0
130    }
131    //
132    pub fn add_coins(&mut self, coins: &CoinBalances) {
133        for coin in &coins.0 {
134            self.add_coin(coin);
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use core::str::FromStr;
142
143    use super::*;
144
145    #[test]
146    fn test_ree_instruction_json() {
147        let instruction_set_1 = IntentionSet {
148            initiator_address: "bc1q8anrrgczju8zn02ww06slsfh9grm07de7r9e3k".to_string(),
149            tx_fee_in_sats: 360,
150            intentions: vec![Intention {
151                exchange_id: "RICH_SWAP".to_string(),
152                action: "add_liquidity".to_string(),
153                action_params: String::new(),
154                pool_address: "bc1pxtmh763568jd8pz9m8wekt2yrqyntqv2wk465mgpzlr9f2aq2vqs52l0hq"
155                    .to_string(),
156                nonce: 1,
157                pool_utxo_spent: vec![],
158                pool_utxo_received: vec![],
159                input_coins: vec![
160                    InputCoin {
161                        from: "bc1q8anrrgczju8zn02ww06slsfh9grm07de7r9e3k".to_string(),
162                        coin: CoinBalance {
163                            id: CoinId::btc(),
164                            value: 23_000,
165                        },
166                    },
167                    InputCoin {
168                        from: "bc1q8anrrgczju8zn02ww06slsfh9grm07de7r9e3k".to_string(),
169                        coin: CoinBalance {
170                            id: CoinId::from_str("868703:142").unwrap(),
171                            value: 959_000_000,
172                        },
173                    },
174                ],
175                output_coins: vec![],
176            }],
177        };
178        println!(
179            "Add liquidity sample instruction: {}\n",
180            serde_json::to_string(&instruction_set_1).unwrap()
181        );
182        //
183        //
184        //
185        let instruction_set_2 = IntentionSet {
186            initiator_address: "bc1qvwvcttn5dtxleu73uuyh8w759gukjr22l7z503".to_string(),
187            tx_fee_in_sats: 330,
188            intentions: vec![Intention {
189                exchange_id: "RICH_SWAP".to_string(),
190                action: "withdraw_liquidity".to_string(),
191                action_params: String::new(),
192                pool_address: "bc1pu3pv54uxfps00a8ydle67fd3rktz090l07lyg7wadurq4h0lpjhqnet990"
193                    .to_string(),
194                nonce: 11,
195                pool_utxo_spent: vec![
196                    "71c9aa9a015e0fcd5cbd6354fbd61c290f9c0a77cecb920df1f0917e7ddc75b7:0"
197                        .to_string(),
198                ],
199                pool_utxo_received: vec![],
200                input_coins: vec![],
201                output_coins: vec![
202                    OutputCoin {
203                        to: "bc1qvwvcttn5dtxleu73uuyh8w759gukjr22l7z503".to_string(),
204                        coin: CoinBalance {
205                            id: CoinId::btc(),
206                            value: 10_124,
207                        },
208                    },
209                    OutputCoin {
210                        to: "bc1qvwvcttn5dtxleu73uuyh8w759gukjr22l7z503".to_string(),
211                        coin: CoinBalance {
212                            id: CoinId::from_str("840106:129").unwrap(),
213                            value: 7_072_563,
214                        },
215                    },
216                ],
217            }],
218        };
219        println!(
220            "Withdraw liquidity sample instruction: {}\n",
221            serde_json::to_string(&instruction_set_2).unwrap()
222        );
223        //
224        //
225        //
226        let instruction_set_3 = IntentionSet {
227            initiator_address: "bc1plvgrpk6mxwyppvqa5j275ujatn8qgs2dcm8m3r2w7sfkn395x6us9l5qdj"
228                .to_string(),
229            tx_fee_in_sats: 340,
230            intentions: vec![Intention {
231                exchange_id: "RICH_SWAP".to_string(),
232                action: "swap".to_string(),
233                action_params: String::new(),
234                pool_address: "bc1ptnxf8aal3apeg8r4zysr6k2mhadg833se2dm4nssl7drjlqdh2jqa4tk3p"
235                    .to_string(),
236                nonce: 5,
237                pool_utxo_spent: vec![
238                    "17616a9d2258c41bea2175e64ecc2e5fc45ae18be5c9003e058cb0bb85301fd8:0"
239                        .to_string(),
240                ],
241                pool_utxo_received: vec![],
242                input_coins: vec![InputCoin {
243                    from: "bc1plvgrpk6mxwyppvqa5j275ujatn8qgs2dcm8m3r2w7sfkn395x6us9l5qdj"
244                        .to_string(),
245                    coin: CoinBalance {
246                        id: CoinId::from_str("840000:846").unwrap(),
247                        value: 10_000_000,
248                    },
249                }],
250                output_coins: vec![OutputCoin {
251                    to: "bc1plvgrpk6mxwyppvqa5j275ujatn8qgs2dcm8m3r2w7sfkn395x6us9l5qdj"
252                        .to_string(),
253                    coin: CoinBalance {
254                        id: CoinId::btc(),
255                        value: 25_523,
256                    },
257                }],
258            }],
259        };
260        println!(
261            "Runes swap btc sample instruction: {}\n",
262            serde_json::to_string(&instruction_set_3).unwrap()
263        );
264        //
265        //
266        //
267        let instruction_set_4 = IntentionSet {
268            initiator_address: "bc1plvgrpk6mxwyppvqa5j275ujatn8qgs2dcm8m3r2w7sfkn395x6us9l5qdj"
269                .to_string(),
270            tx_fee_in_sats: 410,
271            intentions: vec![
272                Intention {
273                    exchange_id: "RICH_SWAP".to_string(),
274                    action: "swap".to_string(),
275                    action_params: String::new(),
276                    pool_address: "bc1ptnxf8aal3apeg8r4zysr6k2mhadg833se2dm4nssl7drjlqdh2jqa4tk3p"
277                        .to_string(),
278                    nonce: 5,
279                    pool_utxo_spent: vec![
280                        "17616a9d2258c41bea2175e64ecc2e5fc45ae18be5c9003e058cb0bb85301fd8:0"
281                            .to_string(),
282                    ],
283                    pool_utxo_received: vec![],
284                    input_coins: vec![InputCoin {
285                        from: "bc1plvgrpk6mxwyppvqa5j275ujatn8qgs2dcm8m3r2w7sfkn395x6us9l5qdj"
286                            .to_string(),
287                        coin: CoinBalance {
288                            id: CoinId::from_str("840000:846").unwrap(),
289                            value: 10_000_000,
290                        },
291                    }],
292                    output_coins: vec![OutputCoin {
293                        to: "bc1pu3pv54uxfps00a8ydle67fd3rktz090l07lyg7wadurq4h0lpjhqnet990"
294                            .to_string(),
295                        coin: CoinBalance {
296                            id: CoinId::btc(),
297                            value: 25_523,
298                        },
299                    }],
300                },
301                Intention {
302                    exchange_id: "RICH_SWAP".to_string(),
303                    action: "swap".to_string(),
304                    action_params: String::new(),
305                    pool_address: "bc1pu3pv54uxfps00a8ydle67fd3rktz090l07lyg7wadurq4h0lpjhqnet990"
306                        .to_string(),
307                    nonce: 9,
308                    pool_utxo_spent: vec![
309                        "9c3590a30d7b5d27f264a295aec6ed15c83618c152c89b28b81a460fcbb66514:1"
310                            .to_string(),
311                    ],
312                    pool_utxo_received: vec![],
313                    input_coins: vec![InputCoin {
314                        from: "bc1pu3pv54uxfps00a8ydle67fd3rktz090l07lyg7wadurq4h0lpjhqnet990"
315                            .to_string(),
316                        coin: CoinBalance {
317                            id: CoinId::btc(),
318                            value: 25_523,
319                        },
320                    }],
321                    output_coins: vec![OutputCoin {
322                        to: "bc1plvgrpk6mxwyppvqa5j275ujatn8qgs2dcm8m3r2w7sfkn395x6us9l5qdj"
323                            .to_string(),
324                        coin: CoinBalance {
325                            id: CoinId::from_str("840106:129").unwrap(),
326                            value: 672_563,
327                        },
328                    }],
329                },
330            ],
331        };
332        println!(
333            "Runes swap runes sample instruction: {}\n",
334            serde_json::to_string(&instruction_set_4).unwrap()
335        );
336    }
337
338    #[test]
339    /// Test the CoinBalances struct
340    fn test_coin_balances() {
341        let mut balances = CoinBalances::new();
342        let coin1 = CoinBalance {
343            id: CoinId::btc(),
344            value: 1000,
345        };
346        let coin2 = CoinBalance {
347            id: CoinId::from_str("840106:129").unwrap(),
348            value: 500,
349        };
350
351        balances.add_coin(&coin1);
352        balances.add_coin(&coin2);
353
354        assert_eq!(balances.value_of(&CoinId::btc()), 1000);
355        assert_eq!(
356            balances.value_of(&CoinId::from_str("840106:129").unwrap()),
357            500
358        );
359
360        let coin3 = CoinBalance {
361            id: CoinId::btc(),
362            value: 200,
363        };
364        balances.add_coin(&coin3);
365        assert_eq!(balances.value_of(&CoinId::btc()), 1200);
366
367        let coin4 = CoinBalance {
368            id: CoinId::from_str("840106:129").unwrap(),
369            value: 600,
370        };
371        assert!(!balances.subtract_coin(&coin4));
372
373        let coin4 = CoinBalance {
374            id: CoinId::from_str("840106:129").unwrap(),
375            value: 500,
376        };
377        assert!(balances.subtract_coin(&coin4));
378        assert_eq!(
379            balances.value_of(&CoinId::from_str("840106:129").unwrap()),
380            0
381        );
382
383        println!("Coin Balances: {:?}", balances);
384    }
385}