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#[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#[derive(CandidType, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, Default)]
34pub struct CoinBalances(Vec<CoinBalance>);
35
36#[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 pub fn iter(&self) -> impl Iterator<Item = &CoinBalance> {
89 self.0.iter()
90 }
91 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 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; }
118 }
119 }
120 false }
122 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 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 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 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 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 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}