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)]
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 iter(&self) -> impl Iterator<Item = &CoinBalance> {
81        self.0.iter()
82    }
83    //
84    pub fn add_coin(&mut self, coin: &CoinBalance) {
85        let mut found = false;
86        for existing_coin in &mut self.0 {
87            if existing_coin.id == coin.id {
88                existing_coin.value += coin.value;
89                found = true;
90                break;
91            }
92        }
93        if !found {
94            self.0.push(coin.clone());
95        }
96    }
97    //
98    pub fn subtract_coin(&mut self, coin: &CoinBalance) -> bool {
99        for i in 0..self.0.len() {
100            if self.0[i].id == coin.id {
101                if self.0[i].value >= coin.value {
102                    self.0[i].value -= coin.value;
103                    if self.0[i].value == 0 {
104                        self.0.remove(i);
105                    }
106                    return true;
107                } else {
108                    return false; // Not enough value to remove
109                }
110            }
111        }
112        false // Coin not found
113    }
114    //
115    pub fn value_of(&self, coin_id: &CoinId) -> u128 {
116        for coin in &self.0 {
117            if coin.id == *coin_id {
118                return coin.value;
119            }
120        }
121        0
122    }
123    //
124    pub fn add_coins(&mut self, coins: &CoinBalances) {
125        for coin in &coins.0 {
126            self.add_coin(coin);
127        }
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use core::str::FromStr;
134
135    use super::*;
136
137    #[test]
138    fn test_ree_instruction_json() {
139        let instruction_set_1 = IntentionSet {
140            initiator_address: "bc1q8anrrgczju8zn02ww06slsfh9grm07de7r9e3k".to_string(),
141            tx_fee_in_sats: 360,
142            intentions: vec![Intention {
143                exchange_id: "RICH_SWAP".to_string(),
144                action: "add_liquidity".to_string(),
145                action_params: String::new(),
146                pool_address: "bc1pxtmh763568jd8pz9m8wekt2yrqyntqv2wk465mgpzlr9f2aq2vqs52l0hq"
147                    .to_string(),
148                nonce: 1,
149                pool_utxo_spent: vec![],
150                pool_utxo_received: vec![],
151                input_coins: vec![
152                    InputCoin {
153                        from: "bc1q8anrrgczju8zn02ww06slsfh9grm07de7r9e3k".to_string(),
154                        coin: CoinBalance {
155                            id: CoinId::btc(),
156                            value: 23_000,
157                        },
158                    },
159                    InputCoin {
160                        from: "bc1q8anrrgczju8zn02ww06slsfh9grm07de7r9e3k".to_string(),
161                        coin: CoinBalance {
162                            id: CoinId::from_str("868703:142").unwrap(),
163                            value: 959_000_000,
164                        },
165                    },
166                ],
167                output_coins: vec![],
168            }],
169        };
170        println!(
171            "Add liquidity sample instruction: {}\n",
172            serde_json::to_string(&instruction_set_1).unwrap()
173        );
174        //
175        //
176        //
177        let instruction_set_2 = IntentionSet {
178            initiator_address: "bc1qvwvcttn5dtxleu73uuyh8w759gukjr22l7z503".to_string(),
179            tx_fee_in_sats: 330,
180            intentions: vec![Intention {
181                exchange_id: "RICH_SWAP".to_string(),
182                action: "withdraw_liquidity".to_string(),
183                action_params: String::new(),
184                pool_address: "bc1pu3pv54uxfps00a8ydle67fd3rktz090l07lyg7wadurq4h0lpjhqnet990"
185                    .to_string(),
186                nonce: 11,
187                pool_utxo_spent: vec![
188                    "71c9aa9a015e0fcd5cbd6354fbd61c290f9c0a77cecb920df1f0917e7ddc75b7:0"
189                        .to_string(),
190                ],
191                pool_utxo_received: vec![],
192                input_coins: vec![],
193                output_coins: vec![
194                    OutputCoin {
195                        to: "bc1qvwvcttn5dtxleu73uuyh8w759gukjr22l7z503".to_string(),
196                        coin: CoinBalance {
197                            id: CoinId::btc(),
198                            value: 10_124,
199                        },
200                    },
201                    OutputCoin {
202                        to: "bc1qvwvcttn5dtxleu73uuyh8w759gukjr22l7z503".to_string(),
203                        coin: CoinBalance {
204                            id: CoinId::from_str("840106:129").unwrap(),
205                            value: 7_072_563,
206                        },
207                    },
208                ],
209            }],
210        };
211        println!(
212            "Withdraw liquidity sample instruction: {}\n",
213            serde_json::to_string(&instruction_set_2).unwrap()
214        );
215        //
216        //
217        //
218        let instruction_set_3 = IntentionSet {
219            initiator_address: "bc1plvgrpk6mxwyppvqa5j275ujatn8qgs2dcm8m3r2w7sfkn395x6us9l5qdj"
220                .to_string(),
221            tx_fee_in_sats: 340,
222            intentions: vec![Intention {
223                exchange_id: "RICH_SWAP".to_string(),
224                action: "swap".to_string(),
225                action_params: String::new(),
226                pool_address: "bc1ptnxf8aal3apeg8r4zysr6k2mhadg833se2dm4nssl7drjlqdh2jqa4tk3p"
227                    .to_string(),
228                nonce: 5,
229                pool_utxo_spent: vec![
230                    "17616a9d2258c41bea2175e64ecc2e5fc45ae18be5c9003e058cb0bb85301fd8:0"
231                        .to_string(),
232                ],
233                pool_utxo_received: vec![],
234                input_coins: vec![InputCoin {
235                    from: "bc1plvgrpk6mxwyppvqa5j275ujatn8qgs2dcm8m3r2w7sfkn395x6us9l5qdj"
236                        .to_string(),
237                    coin: CoinBalance {
238                        id: CoinId::from_str("840000:846").unwrap(),
239                        value: 10_000_000,
240                    },
241                }],
242                output_coins: vec![OutputCoin {
243                    to: "bc1plvgrpk6mxwyppvqa5j275ujatn8qgs2dcm8m3r2w7sfkn395x6us9l5qdj"
244                        .to_string(),
245                    coin: CoinBalance {
246                        id: CoinId::btc(),
247                        value: 25_523,
248                    },
249                }],
250            }],
251        };
252        println!(
253            "Runes swap btc sample instruction: {}\n",
254            serde_json::to_string(&instruction_set_3).unwrap()
255        );
256        //
257        //
258        //
259        let instruction_set_4 = IntentionSet {
260            initiator_address: "bc1plvgrpk6mxwyppvqa5j275ujatn8qgs2dcm8m3r2w7sfkn395x6us9l5qdj"
261                .to_string(),
262            tx_fee_in_sats: 410,
263            intentions: vec![
264                Intention {
265                    exchange_id: "RICH_SWAP".to_string(),
266                    action: "swap".to_string(),
267                    action_params: String::new(),
268                    pool_address: "bc1ptnxf8aal3apeg8r4zysr6k2mhadg833se2dm4nssl7drjlqdh2jqa4tk3p"
269                        .to_string(),
270                    nonce: 5,
271                    pool_utxo_spent: vec![
272                        "17616a9d2258c41bea2175e64ecc2e5fc45ae18be5c9003e058cb0bb85301fd8:0"
273                            .to_string(),
274                    ],
275                    pool_utxo_received: vec![],
276                    input_coins: vec![InputCoin {
277                        from: "bc1plvgrpk6mxwyppvqa5j275ujatn8qgs2dcm8m3r2w7sfkn395x6us9l5qdj"
278                            .to_string(),
279                        coin: CoinBalance {
280                            id: CoinId::from_str("840000:846").unwrap(),
281                            value: 10_000_000,
282                        },
283                    }],
284                    output_coins: vec![OutputCoin {
285                        to: "bc1pu3pv54uxfps00a8ydle67fd3rktz090l07lyg7wadurq4h0lpjhqnet990"
286                            .to_string(),
287                        coin: CoinBalance {
288                            id: CoinId::btc(),
289                            value: 25_523,
290                        },
291                    }],
292                },
293                Intention {
294                    exchange_id: "RICH_SWAP".to_string(),
295                    action: "swap".to_string(),
296                    action_params: String::new(),
297                    pool_address: "bc1pu3pv54uxfps00a8ydle67fd3rktz090l07lyg7wadurq4h0lpjhqnet990"
298                        .to_string(),
299                    nonce: 9,
300                    pool_utxo_spent: vec![
301                        "9c3590a30d7b5d27f264a295aec6ed15c83618c152c89b28b81a460fcbb66514:1"
302                            .to_string(),
303                    ],
304                    pool_utxo_received: vec![],
305                    input_coins: vec![InputCoin {
306                        from: "bc1pu3pv54uxfps00a8ydle67fd3rktz090l07lyg7wadurq4h0lpjhqnet990"
307                            .to_string(),
308                        coin: CoinBalance {
309                            id: CoinId::btc(),
310                            value: 25_523,
311                        },
312                    }],
313                    output_coins: vec![OutputCoin {
314                        to: "bc1plvgrpk6mxwyppvqa5j275ujatn8qgs2dcm8m3r2w7sfkn395x6us9l5qdj"
315                            .to_string(),
316                        coin: CoinBalance {
317                            id: CoinId::from_str("840106:129").unwrap(),
318                            value: 672_563,
319                        },
320                    }],
321                },
322            ],
323        };
324        println!(
325            "Runes swap runes sample instruction: {}\n",
326            serde_json::to_string(&instruction_set_4).unwrap()
327        );
328    }
329
330    #[test]
331    /// Test the CoinBalances struct
332    fn test_coin_balances() {
333        let mut balances = CoinBalances::new();
334        let coin1 = CoinBalance {
335            id: CoinId::btc(),
336            value: 1000,
337        };
338        let coin2 = CoinBalance {
339            id: CoinId::from_str("840106:129").unwrap(),
340            value: 500,
341        };
342
343        balances.add_coin(&coin1);
344        balances.add_coin(&coin2);
345
346        assert_eq!(balances.value_of(&CoinId::btc()), 1000);
347        assert_eq!(
348            balances.value_of(&CoinId::from_str("840106:129").unwrap()),
349            500
350        );
351
352        let coin3 = CoinBalance {
353            id: CoinId::btc(),
354            value: 200,
355        };
356        balances.add_coin(&coin3);
357        assert_eq!(balances.value_of(&CoinId::btc()), 1200);
358
359        let coin4 = CoinBalance {
360            id: CoinId::from_str("840106:129").unwrap(),
361            value: 600,
362        };
363        assert!(!balances.subtract_coin(&coin4));
364
365        let coin4 = CoinBalance {
366            id: CoinId::from_str("840106:129").unwrap(),
367            value: 500,
368        };
369        assert!(balances.subtract_coin(&coin4));
370        assert_eq!(
371            balances.value_of(&CoinId::from_str("840106:129").unwrap()),
372            0
373        );
374
375        println!("Coin Balances: {:?}", balances);
376    }
377}