1use std::collections::HashMap;
18
19use serde::{Deserialize, Serialize};
20
21use crate::payments::{PaymentError, WebLedger};
22
23impl WebLedger {
33 pub fn get_currency_balance(&self, did: &str, currency: &str) -> u64 {
35 self.entries
36 .iter()
37 .find(|e| e.url == did)
38 .map(|e| e.amount.chain_balance(currency))
39 .unwrap_or(0)
40 }
41
42 pub fn credit_currency(&mut self, did: &str, currency: &str, amount: u64) {
44 use crate::payments::{CurrencyAmount, LedgerAmount, LedgerEntry};
45
46 self.updated = now_secs();
47 if let Some(entry) = self.entries.iter_mut().find(|e| e.url == did) {
48 match &mut entry.amount {
49 LedgerAmount::Simple(s) => {
50 let sat_val = s.parse::<u64>().unwrap_or(0);
52 let mut currencies = vec![CurrencyAmount {
53 currency: "satoshi".into(),
54 value: sat_val.to_string(),
55 }];
56 currencies.push(CurrencyAmount {
57 currency: currency.into(),
58 value: amount.to_string(),
59 });
60 entry.amount = LedgerAmount::Multi(currencies);
61 }
62 LedgerAmount::Multi(v) => {
63 if let Some(ca) = v.iter_mut().find(|a| a.currency == currency) {
64 let current: u64 = ca.value.parse().unwrap_or(0);
65 ca.value = current.saturating_add(amount).to_string();
66 } else {
67 v.push(CurrencyAmount {
68 currency: currency.into(),
69 value: amount.to_string(),
70 });
71 }
72 }
73 }
74 } else {
75 self.entries.push(LedgerEntry {
76 entry_type: "Entry".into(),
77 url: did.into(),
78 amount: LedgerAmount::Multi(vec![CurrencyAmount {
79 currency: currency.into(),
80 value: amount.to_string(),
81 }]),
82 });
83 }
84 }
85
86 pub fn debit_currency(
88 &mut self,
89 did: &str,
90 currency: &str,
91 amount: u64,
92 ) -> Result<u64, PaymentError> {
93 let current = self.get_currency_balance(did, currency);
94 if current < amount {
95 return Err(PaymentError::InsufficientBalance {
96 balance: current,
97 cost: amount,
98 });
99 }
100 self.updated = now_secs();
101 let entry = self.entries.iter_mut().find(|e| e.url == did).unwrap();
103 match &mut entry.amount {
104 crate::payments::LedgerAmount::Multi(v) => {
105 if let Some(ca) = v.iter_mut().find(|a| a.currency == currency) {
106 let cur: u64 = ca.value.parse().unwrap_or(0);
107 let new_val = cur - amount;
108 ca.value = new_val.to_string();
109 Ok(new_val)
110 } else {
111 Err(PaymentError::InsufficientBalance {
112 balance: 0,
113 cost: amount,
114 })
115 }
116 }
117 crate::payments::LedgerAmount::Simple(_) => {
118 Err(PaymentError::InsufficientBalance {
119 balance: 0,
120 cost: amount,
121 })
122 }
123 }
124 }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct SwapResult {
134 pub amount_in: u64,
135 pub amount_out: u64,
136 pub fee: u64,
137 pub new_balance_in: u64,
138 pub new_balance_out: u64,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct SellOrder {
148 pub id: String,
149 pub seller: String,
151 pub sell_currency: String,
153 pub sell_amount: u64,
155 pub buy_currency: String,
157 pub price: u64,
159 pub created_at: u64,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct OrderBook {
166 orders: Vec<SellOrder>,
167 next_id: u64,
168}
169
170impl OrderBook {
171 pub fn new() -> Self {
172 Self {
173 orders: Vec::new(),
174 next_id: 1,
175 }
176 }
177
178 pub fn create_order(
183 &mut self,
184 seller: &str,
185 sell_currency: &str,
186 sell_amount: u64,
187 buy_currency: &str,
188 price: u64,
189 ) -> SellOrder {
190 let order = SellOrder {
191 id: self.next_id.to_string(),
192 seller: seller.into(),
193 sell_currency: sell_currency.into(),
194 sell_amount,
195 buy_currency: buy_currency.into(),
196 price,
197 created_at: now_secs(),
198 };
199 self.next_id += 1;
200 self.orders.push(order.clone());
201 order
202 }
203
204 pub fn list_offers(&self, currency_pair: Option<(&str, &str)>) -> Vec<&SellOrder> {
209 match currency_pair {
210 None => self.orders.iter().collect(),
211 Some((sell, buy)) => self
212 .orders
213 .iter()
214 .filter(|o| o.sell_currency == sell && o.buy_currency == buy)
215 .collect(),
216 }
217 }
218
219 pub fn cancel_order(
221 &mut self,
222 id: &str,
223 seller: &str,
224 ) -> Result<SellOrder, PaymentError> {
225 let idx = self
226 .orders
227 .iter()
228 .position(|o| o.id == id)
229 .ok_or_else(|| {
230 PaymentError::InvalidTxo(format!("order {id} not found"))
231 })?;
232
233 if self.orders[idx].seller != seller {
234 return Err(PaymentError::InvalidTxo(format!(
235 "order {id} belongs to {}, not {seller}",
236 self.orders[idx].seller
237 )));
238 }
239
240 Ok(self.orders.remove(idx))
241 }
242
243 pub fn execute_swap(
249 &mut self,
250 id: &str,
251 buyer: &str,
252 ledger: &mut WebLedger,
253 ) -> Result<SwapResult, PaymentError> {
254 let idx = self
255 .orders
256 .iter()
257 .position(|o| o.id == id)
258 .ok_or_else(|| {
259 PaymentError::InvalidTxo(format!("order {id} not found"))
260 })?;
261
262 let order = &self.orders[idx];
263
264 let total_cost = order
266 .sell_amount
267 .checked_mul(order.price)
268 .ok_or_else(|| {
269 PaymentError::InvalidTxo("price overflow".into())
270 })?;
271
272 let buyer_balance = ledger.get_currency_balance(buyer, &order.buy_currency);
274 if buyer_balance < total_cost {
275 return Err(PaymentError::InsufficientBalance {
276 balance: buyer_balance,
277 cost: total_cost,
278 });
279 }
280
281 let seller_balance =
283 ledger.get_currency_balance(&order.seller, &order.sell_currency);
284 if seller_balance < order.sell_amount {
285 return Err(PaymentError::InsufficientBalance {
286 balance: seller_balance,
287 cost: order.sell_amount,
288 });
289 }
290
291 let sell_amount = order.sell_amount;
293 let sell_currency = order.sell_currency.clone();
294 let buy_currency = order.buy_currency.clone();
295 let seller = order.seller.clone();
296
297 ledger.debit_currency(buyer, &buy_currency, total_cost)?;
300 ledger.credit_currency(&seller, &buy_currency, total_cost);
302 ledger.debit_currency(&seller, &sell_currency, sell_amount)?;
304 ledger.credit_currency(buyer, &sell_currency, sell_amount);
306
307 self.orders.remove(idx);
309
310 let new_balance_in = ledger.get_currency_balance(buyer, &buy_currency);
311 let new_balance_out = ledger.get_currency_balance(buyer, &sell_currency);
312
313 Ok(SwapResult {
314 amount_in: total_cost,
315 amount_out: sell_amount,
316 fee: 0,
317 new_balance_in,
318 new_balance_out,
319 })
320 }
321}
322
323impl Default for OrderBook {
324 fn default() -> Self {
325 Self::new()
326 }
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct AmmPool {
340 pub currency_a: String,
341 pub currency_b: String,
342 pub reserve_a: u64,
343 pub reserve_b: u64,
344 pub total_shares: u64,
345 pub fee_bps: u64,
347 shares: HashMap<String, u64>,
349}
350
351impl AmmPool {
352 pub const DEFAULT_FEE_BPS: u64 = 30;
354
355 pub fn new(currency_a: &str, currency_b: &str, fee_bps: u64) -> Self {
356 Self {
357 currency_a: currency_a.into(),
358 currency_b: currency_b.into(),
359 reserve_a: 0,
360 reserve_b: 0,
361 total_shares: 0,
362 fee_bps,
363 shares: HashMap::new(),
364 }
365 }
366
367 pub fn add_liquidity(
373 &mut self,
374 provider: &str,
375 amount_a: u64,
376 amount_b: u64,
377 ledger: &mut WebLedger,
378 ) -> Result<u64, PaymentError> {
379 if amount_a == 0 || amount_b == 0 {
380 return Err(PaymentError::InvalidTxo(
381 "liquidity amounts must be non-zero".into(),
382 ));
383 }
384
385 ledger.debit_currency(provider, &self.currency_a, amount_a)?;
387 ledger.debit_currency(provider, &self.currency_b, amount_b)?;
388
389 let shares = if self.total_shares == 0 {
390 let product = (amount_a as u128) * (amount_b as u128);
393 isqrt_u128(product) as u64
394 } else {
395 let share_a = (amount_a as u128) * (self.total_shares as u128)
397 / (self.reserve_a as u128);
398 let share_b = (amount_b as u128) * (self.total_shares as u128)
399 / (self.reserve_b as u128);
400 share_a.min(share_b) as u64
401 };
402
403 if shares == 0 {
404 return Err(PaymentError::InvalidTxo(
405 "liquidity too small to issue shares".into(),
406 ));
407 }
408
409 self.reserve_a = self.reserve_a.saturating_add(amount_a);
410 self.reserve_b = self.reserve_b.saturating_add(amount_b);
411 self.total_shares = self.total_shares.saturating_add(shares);
412 *self.shares.entry(provider.into()).or_insert(0) += shares;
413
414 Ok(shares)
415 }
416
417 pub fn remove_liquidity(
422 &mut self,
423 provider: &str,
424 shares: u64,
425 ledger: &mut WebLedger,
426 ) -> Result<(u64, u64), PaymentError> {
427 let provider_shares = self
428 .shares
429 .get(provider)
430 .copied()
431 .unwrap_or(0);
432
433 if provider_shares < shares {
434 return Err(PaymentError::InsufficientBalance {
435 balance: provider_shares,
436 cost: shares,
437 });
438 }
439
440 if self.total_shares == 0 {
441 return Err(PaymentError::InvalidTxo("pool has no shares".into()));
442 }
443
444 let amount_a =
446 ((self.reserve_a as u128) * (shares as u128) / (self.total_shares as u128))
447 as u64;
448 let amount_b =
449 ((self.reserve_b as u128) * (shares as u128) / (self.total_shares as u128))
450 as u64;
451
452 self.reserve_a = self.reserve_a.saturating_sub(amount_a);
453 self.reserve_b = self.reserve_b.saturating_sub(amount_b);
454 self.total_shares = self.total_shares.saturating_sub(shares);
455
456 let entry = self.shares.get_mut(provider).unwrap();
457 *entry -= shares;
458 if *entry == 0 {
459 self.shares.remove(provider);
460 }
461
462 ledger.credit_currency(provider, &self.currency_a, amount_a);
464 ledger.credit_currency(provider, &self.currency_b, amount_b);
465
466 Ok((amount_a, amount_b))
467 }
468
469 pub fn swap(
479 &mut self,
480 trader: &str,
481 from_currency: &str,
482 amount_in: u64,
483 ledger: &mut WebLedger,
484 ) -> Result<SwapResult, PaymentError> {
485 if amount_in == 0 {
486 return Err(PaymentError::InvalidTxo(
487 "swap amount must be non-zero".into(),
488 ));
489 }
490
491 let (reserve_in, reserve_out, to_currency) =
492 if from_currency == self.currency_a {
493 (self.reserve_a, self.reserve_b, self.currency_b.clone())
494 } else if from_currency == self.currency_b {
495 (self.reserve_b, self.reserve_a, self.currency_a.clone())
496 } else {
497 return Err(PaymentError::InvalidTxo(format!(
498 "currency {from_currency} not in pool ({}/{})",
499 self.currency_a, self.currency_b
500 )));
501 };
502
503 if reserve_in == 0 || reserve_out == 0 {
504 return Err(PaymentError::InvalidTxo("pool is empty".into()));
505 }
506
507 let fee_factor = 10_000u128 - (self.fee_bps as u128);
509 let numerator = (reserve_out as u128) * (amount_in as u128) * fee_factor;
510 let denominator =
511 (reserve_in as u128) * 10_000u128 + (amount_in as u128) * fee_factor;
512
513 let amount_out = (numerator / denominator) as u64;
514
515 if amount_out == 0 {
516 return Err(PaymentError::InvalidTxo(
517 "swap output rounds to zero".into(),
518 ));
519 }
520
521 let effective_input =
524 ((amount_in as u128) * fee_factor / 10_000u128) as u64;
525 let fee = amount_in - effective_input;
526
527 ledger.debit_currency(trader, from_currency, amount_in)?;
529 ledger.credit_currency(trader, &to_currency, amount_out);
530
531 if from_currency == self.currency_a {
533 self.reserve_a = self.reserve_a.saturating_add(amount_in);
534 self.reserve_b = self.reserve_b.saturating_sub(amount_out);
535 } else {
536 self.reserve_b = self.reserve_b.saturating_add(amount_in);
537 self.reserve_a = self.reserve_a.saturating_sub(amount_out);
538 }
539
540 let new_balance_in = ledger.get_currency_balance(trader, from_currency);
541 let new_balance_out = ledger.get_currency_balance(trader, &to_currency);
542
543 Ok(SwapResult {
544 amount_in,
545 amount_out,
546 fee,
547 new_balance_in,
548 new_balance_out,
549 })
550 }
551
552 pub fn pool_info(&self) -> serde_json::Value {
554 serde_json::json!({
555 "currency_a": self.currency_a,
556 "currency_b": self.currency_b,
557 "reserve_a": self.reserve_a,
558 "reserve_b": self.reserve_b,
559 "total_shares": self.total_shares,
560 "fee_bps": self.fee_bps,
561 "invariant_k": (self.reserve_a as u128) * (self.reserve_b as u128),
562 "providers": self.shares.len(),
563 })
564 }
565}
566
567#[derive(Debug, Clone, Serialize, Deserialize)]
573pub struct Exchange {
574 pub order_book: OrderBook,
575 pub pools: HashMap<String, AmmPool>,
576}
577
578impl Exchange {
579 pub fn new() -> Self {
580 Self {
581 order_book: OrderBook::new(),
582 pools: HashMap::new(),
583 }
584 }
585
586 pub fn get_or_create_pool(
591 &mut self,
592 currency_a: &str,
593 currency_b: &str,
594 fee_bps: u64,
595 ) -> &mut AmmPool {
596 let key = pool_key(currency_a, currency_b);
597 self.pools
598 .entry(key)
599 .or_insert_with(|| AmmPool::new(currency_a, currency_b, fee_bps))
600 }
601
602 pub fn get_pool(&self, currency_a: &str, currency_b: &str) -> Option<&AmmPool> {
604 let key = pool_key(currency_a, currency_b);
605 self.pools.get(&key)
606 }
607}
608
609impl Default for Exchange {
610 fn default() -> Self {
611 Self::new()
612 }
613}
614
615fn pool_key(a: &str, b: &str) -> String {
621 if a <= b {
622 format!("{a}/{b}")
623 } else {
624 format!("{b}/{a}")
625 }
626}
627
628fn isqrt_u128(n: u128) -> u128 {
630 if n == 0 {
631 return 0;
632 }
633 let mut x = n;
634 let mut y = (x + 1) / 2;
635 while y < x {
636 x = y;
637 y = (x + n / x) / 2;
638 }
639 x
640}
641
642fn now_secs() -> u64 {
644 #[cfg(target_arch = "wasm32")]
645 {
646 (js_sys::Date::now() / 1000.0) as u64
647 }
648 #[cfg(not(target_arch = "wasm32"))]
649 {
650 std::time::SystemTime::now()
651 .duration_since(std::time::UNIX_EPOCH)
652 .unwrap_or_default()
653 .as_secs()
654 }
655}
656
657#[cfg(test)]
662mod tests {
663 use super::*;
664 use crate::payments::WebLedger;
665
666 fn setup_ledger() -> WebLedger {
668 let mut ledger = WebLedger::new("Test Exchange");
669 ledger.credit_currency("did:nostr:alice", "tbtc4", 10_000);
670 ledger.credit_currency("did:nostr:alice", "tbtc3", 5_000);
671 ledger.credit_currency("did:nostr:bob", "tbtc4", 8_000);
672 ledger.credit_currency("did:nostr:bob", "tbtc3", 12_000);
673 ledger
674 }
675
676 #[test]
679 fn test_order_create_and_list() {
680 let mut book = OrderBook::new();
681 book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
682 book.create_order("did:nostr:bob", "tbtc3", 50, "tbtc4", 1);
683 book.create_order("did:nostr:alice", "tbtc4", 200, "tbtc3", 3);
684
685 assert_eq!(book.list_offers(None).len(), 3);
687
688 let filtered = book.list_offers(Some(("tbtc4", "tbtc3")));
690 assert_eq!(filtered.len(), 2);
691 assert!(filtered.iter().all(|o| o.sell_currency == "tbtc4"));
692
693 let filtered2 = book.list_offers(Some(("tbtc3", "tbtc4")));
695 assert_eq!(filtered2.len(), 1);
696 assert_eq!(filtered2[0].seller, "did:nostr:bob");
697 }
698
699 #[test]
700 fn test_order_cancel_by_seller() {
701 let mut book = OrderBook::new();
702 let order = book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
703
704 let err = book.cancel_order(&order.id, "did:nostr:bob").unwrap_err();
706 assert!(
707 format!("{err}").contains("belongs to"),
708 "Expected ownership error, got: {err}"
709 );
710
711 let cancelled = book.cancel_order(&order.id, "did:nostr:alice").unwrap();
713 assert_eq!(cancelled.id, order.id);
714 assert_eq!(book.list_offers(None).len(), 0);
715 }
716
717 #[test]
718 fn test_order_execute_swap() {
719 let mut ledger = setup_ledger();
720 let mut book = OrderBook::new();
721
722 let order =
724 book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
725
726 let result =
728 book.execute_swap(&order.id, "did:nostr:bob", &mut ledger).unwrap();
729
730 assert_eq!(result.amount_in, 200); assert_eq!(result.amount_out, 100); assert_eq!(result.fee, 0); assert_eq!(
736 ledger.get_currency_balance("did:nostr:bob", "tbtc3"),
737 12_000 - 200
738 );
739 assert_eq!(
740 ledger.get_currency_balance("did:nostr:bob", "tbtc4"),
741 8_000 + 100
742 );
743 assert_eq!(
744 ledger.get_currency_balance("did:nostr:alice", "tbtc3"),
745 5_000 + 200
746 );
747 assert_eq!(
748 ledger.get_currency_balance("did:nostr:alice", "tbtc4"),
749 10_000 - 100
750 );
751
752 assert_eq!(book.list_offers(None).len(), 0);
754 }
755
756 #[test]
757 fn test_order_swap_insufficient_balance() {
758 let mut ledger = setup_ledger();
759 let mut book = OrderBook::new();
760
761 let order =
763 book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 200);
764
765 let err =
767 book.execute_swap(&order.id, "did:nostr:bob", &mut ledger).unwrap_err();
768 assert!(matches!(err, PaymentError::InsufficientBalance { .. }));
769
770 assert_eq!(book.list_offers(None).len(), 1);
772 }
773
774 #[test]
777 fn test_amm_add_liquidity_first() {
778 let mut ledger = setup_ledger();
779 let mut pool = AmmPool::new("tbtc4", "tbtc3", AmmPool::DEFAULT_FEE_BPS);
780
781 let shares = pool
782 .add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
783 .unwrap();
784
785 assert_eq!(shares, isqrt_u128(2_000_000) as u64);
787 assert_eq!(pool.reserve_a, 1_000);
788 assert_eq!(pool.reserve_b, 2_000);
789 assert_eq!(pool.total_shares, shares);
790
791 assert_eq!(
793 ledger.get_currency_balance("did:nostr:alice", "tbtc4"),
794 10_000 - 1_000
795 );
796 assert_eq!(
797 ledger.get_currency_balance("did:nostr:alice", "tbtc3"),
798 5_000 - 2_000
799 );
800 }
801
802 #[test]
803 fn test_amm_add_liquidity_subsequent() {
804 let mut ledger = setup_ledger();
805 let mut pool = AmmPool::new("tbtc4", "tbtc3", AmmPool::DEFAULT_FEE_BPS);
806
807 let shares_alice = pool
808 .add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
809 .unwrap();
810
811 let shares_bob = pool
813 .add_liquidity("did:nostr:bob", 500, 1_000, &mut ledger)
814 .unwrap();
815
816 assert_eq!(shares_bob, shares_alice / 2);
819 assert_eq!(pool.reserve_a, 1_500);
820 assert_eq!(pool.reserve_b, 3_000);
821 assert_eq!(pool.total_shares, shares_alice + shares_bob);
822 }
823
824 #[test]
825 fn test_amm_swap_constant_product() {
826 let mut ledger = setup_ledger();
827 let mut pool = AmmPool::new("tbtc4", "tbtc3", 0); pool.add_liquidity("did:nostr:alice", 5_000, 5_000, &mut ledger)
830 .unwrap();
831
832 let k_before = (pool.reserve_a as u128) * (pool.reserve_b as u128);
833
834 let result = pool
836 .swap("did:nostr:bob", "tbtc4", 1_000, &mut ledger)
837 .unwrap();
838
839 let k_after = (pool.reserve_a as u128) * (pool.reserve_b as u128);
840
841 assert!(k_after >= k_before, "k decreased: {k_before} → {k_after}");
843
844 assert_eq!(result.amount_out, 833);
847 assert_eq!(result.amount_in, 1_000);
848
849 assert_eq!(pool.reserve_a, 6_000);
851 assert_eq!(pool.reserve_b, 5_000 - 833);
852 }
853
854 #[test]
855 fn test_amm_swap_fee_collection() {
856 let mut ledger = setup_ledger();
857 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30); pool.add_liquidity("did:nostr:alice", 5_000, 5_000, &mut ledger)
860 .unwrap();
861
862 let k_before = (pool.reserve_a as u128) * (pool.reserve_b as u128);
863
864 let result = pool
866 .swap("did:nostr:bob", "tbtc4", 1_000, &mut ledger)
867 .unwrap();
868
869 let k_after = (pool.reserve_a as u128) * (pool.reserve_b as u128);
870
871 assert!(k_after > k_before, "k should increase with fee: {k_before} → {k_after}");
873
874 assert_eq!(result.fee, 3);
876
877 assert_eq!(result.amount_out, 831);
881 }
882
883 #[test]
884 fn test_amm_remove_liquidity() {
885 let mut ledger = setup_ledger();
886 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
887
888 let shares = pool
889 .add_liquidity("did:nostr:alice", 2_000, 4_000, &mut ledger)
890 .unwrap();
891
892 pool.swap("did:nostr:bob", "tbtc4", 500, &mut ledger).unwrap();
894
895 let (got_a, got_b) = pool
897 .remove_liquidity("did:nostr:alice", shares, &mut ledger)
898 .unwrap();
899
900 assert!(
904 got_a > 2_000 || got_b > 4_000 || (got_a >= 2_000 && got_b >= 3_500),
905 "Expected fee accrual: got ({got_a}, {got_b}) vs deposited (2000, 4000)"
906 );
907
908 assert_eq!(pool.reserve_a, 0);
910 assert_eq!(pool.reserve_b, 0);
911 assert_eq!(pool.total_shares, 0);
912 }
913
914 #[test]
915 fn test_amm_swap_empty_pool() {
916 let mut ledger = setup_ledger();
917 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
918
919 let err = pool
920 .swap("did:nostr:bob", "tbtc4", 100, &mut ledger)
921 .unwrap_err();
922 assert!(
923 format!("{err}").contains("empty"),
924 "Expected empty pool error, got: {err}"
925 );
926 }
927
928 #[test]
929 fn test_exchange_multi_pool() {
930 let mut ledger = setup_ledger();
931 ledger.credit_currency("did:nostr:alice", "signet", 10_000);
933
934 let mut exchange = Exchange::new();
935
936 let pool1 =
937 exchange.get_or_create_pool("tbtc4", "tbtc3", AmmPool::DEFAULT_FEE_BPS);
938 pool1
939 .add_liquidity("did:nostr:alice", 1_000, 1_000, &mut ledger)
940 .unwrap();
941
942 let pool2 =
943 exchange.get_or_create_pool("tbtc4", "signet", AmmPool::DEFAULT_FEE_BPS);
944 pool2
945 .add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
946 .unwrap();
947
948 assert_eq!(exchange.pools.len(), 2);
949
950 let p1 = exchange.get_pool("tbtc4", "tbtc3").unwrap();
952 assert_eq!(p1.reserve_a, 1_000);
953
954 let p2 = exchange.get_pool("tbtc4", "signet").unwrap();
955 assert_eq!(p2.reserve_b, 2_000);
956
957 let p2_alt = exchange.get_pool("signet", "tbtc4").unwrap();
959 assert_eq!(p2_alt.reserve_b, 2_000);
960 }
961
962 #[test]
963 fn test_integer_overflow_safety() {
964 let mut ledger = WebLedger::new("Overflow Test");
965 let large = u64::MAX / 2;
966 ledger.credit_currency("did:nostr:whale", "tbtc4", large);
967 ledger.credit_currency("did:nostr:whale", "tbtc3", large);
968
969 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
970
971 let shares = pool
973 .add_liquidity("did:nostr:whale", large, large, &mut ledger)
974 .unwrap();
975 assert!(shares > 0);
976 assert_eq!(pool.reserve_a, large);
977 assert_eq!(pool.reserve_b, large);
978
979 ledger.credit_currency("did:nostr:trader", "tbtc4", 1_000_000);
981
982 let result = pool
984 .swap("did:nostr:trader", "tbtc4", 1_000_000, &mut ledger)
985 .unwrap();
986 assert!(result.amount_out > 0);
987 assert!(result.amount_out < 1_000_000); let k = (pool.reserve_a as u128) * (pool.reserve_b as u128);
991 let k_original = (large as u128) * (large as u128);
992 assert!(k >= k_original);
993 }
994
995 #[test]
998 fn test_exchange_serialization_roundtrip() {
999 let mut exchange = Exchange::new();
1000 exchange
1001 .order_book
1002 .create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
1003 exchange.get_or_create_pool("tbtc4", "tbtc3", 30);
1004
1005 let json = serde_json::to_string(&exchange).unwrap();
1006 let parsed: Exchange = serde_json::from_str(&json).unwrap();
1007
1008 assert_eq!(parsed.order_book.list_offers(None).len(), 1);
1009 assert_eq!(parsed.pools.len(), 1);
1010 }
1011
1012 #[test]
1015 fn test_pool_info() {
1016 let mut ledger = setup_ledger();
1017 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
1018 pool.add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
1019 .unwrap();
1020
1021 let info = pool.pool_info();
1022 assert_eq!(info["currency_a"], "tbtc4");
1023 assert_eq!(info["reserve_a"], 1_000);
1024 assert_eq!(info["reserve_b"], 2_000);
1025 assert_eq!(info["fee_bps"], 30);
1026 assert_eq!(info["invariant_k"], 2_000_000u64);
1027 assert_eq!(info["providers"], 1);
1028 }
1029
1030 #[test]
1033 fn test_isqrt() {
1034 assert_eq!(isqrt_u128(0), 0);
1035 assert_eq!(isqrt_u128(1), 1);
1036 assert_eq!(isqrt_u128(4), 2);
1037 assert_eq!(isqrt_u128(9), 3);
1038 assert_eq!(isqrt_u128(10), 3);
1039 assert_eq!(isqrt_u128(2_000_000), 1414);
1040 let max = u64::MAX as u128;
1042 assert_eq!(isqrt_u128(max * max), max);
1043 }
1044}