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)]
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 pub fn iter(&self) -> impl Iterator<Item = &CoinBalance> {
81 self.0.iter()
82 }
83 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 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; }
110 }
111 }
112 false }
114 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 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 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 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 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 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}