1use std::collections::HashMap;
18
19use serde::{Deserialize, Serialize};
20
21use crate::payments::{PaymentError, WebLedger};
22
23impl WebLedger {
36 pub fn get_currency_balance(&self, did: &str, currency: &str) -> u64 {
38 self.entries
39 .iter()
40 .find(|e| e.url == did)
41 .map(|e| e.amount.chain_balance(currency))
42 .unwrap_or(0)
43 }
44
45 pub(crate) fn credit_currency(&mut self, did: &str, currency: &str, amount: u64) {
47 use crate::payments::{CurrencyAmount, LedgerAmount, LedgerEntry};
48
49 self.updated = now_secs();
50 if let Some(entry) = self.entries.iter_mut().find(|e| e.url == did) {
51 match &mut entry.amount {
52 LedgerAmount::Simple(s) => {
53 let sat_val = s.parse::<u64>().unwrap_or(0);
55 let mut currencies = vec![CurrencyAmount {
56 currency: "satoshi".into(),
57 value: sat_val.to_string(),
58 }];
59 currencies.push(CurrencyAmount {
60 currency: currency.into(),
61 value: amount.to_string(),
62 });
63 entry.amount = LedgerAmount::Multi(currencies);
64 }
65 LedgerAmount::Multi(v) => {
66 if let Some(ca) = v.iter_mut().find(|a| a.currency == currency) {
67 let current: u64 = ca.value.parse().unwrap_or(0);
68 ca.value = current.saturating_add(amount).to_string();
69 } else {
70 v.push(CurrencyAmount {
71 currency: currency.into(),
72 value: amount.to_string(),
73 });
74 }
75 }
76 }
77 } else {
78 self.entries.push(LedgerEntry {
79 entry_type: "Entry".into(),
80 url: did.into(),
81 amount: LedgerAmount::Multi(vec![CurrencyAmount {
82 currency: currency.into(),
83 value: amount.to_string(),
84 }]),
85 });
86 }
87 }
88
89 pub(crate) fn debit_currency(
91 &mut self,
92 did: &str,
93 currency: &str,
94 amount: u64,
95 ) -> Result<u64, PaymentError> {
96 let current = self.get_currency_balance(did, currency);
97 if current < amount {
98 return Err(PaymentError::InsufficientBalance {
99 balance: current,
100 cost: amount,
101 });
102 }
103 self.updated = now_secs();
104 let entry = self.entries.iter_mut().find(|e| e.url == did).unwrap();
106 match &mut entry.amount {
107 crate::payments::LedgerAmount::Multi(v) => {
108 if let Some(ca) = v.iter_mut().find(|a| a.currency == currency) {
109 let cur: u64 = ca.value.parse().unwrap_or(0);
110 let new_val = cur - amount;
111 ca.value = new_val.to_string();
112 Ok(new_val)
113 } else {
114 Err(PaymentError::InsufficientBalance {
115 balance: 0,
116 cost: amount,
117 })
118 }
119 }
120 crate::payments::LedgerAmount::Simple(_) => Err(PaymentError::InsufficientBalance {
121 balance: 0,
122 cost: amount,
123 }),
124 }
125 }
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct SwapResult {
135 pub amount_in: u64,
136 pub amount_out: u64,
137 pub fee: u64,
138 pub new_balance_in: u64,
139 pub new_balance_out: u64,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct SellOrder {
149 pub id: String,
150 pub seller: String,
152 pub sell_currency: String,
154 pub sell_amount: u64,
156 pub buy_currency: String,
158 pub price: u64,
160 pub created_at: u64,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct OrderBook {
167 orders: Vec<SellOrder>,
168 next_id: u64,
169}
170
171impl OrderBook {
172 pub fn new() -> Self {
173 Self {
174 orders: Vec::new(),
175 next_id: 1,
176 }
177 }
178
179 pub fn create_order(
184 &mut self,
185 seller: &str,
186 sell_currency: &str,
187 sell_amount: u64,
188 buy_currency: &str,
189 price: u64,
190 ) -> SellOrder {
191 let order = SellOrder {
192 id: self.next_id.to_string(),
193 seller: seller.into(),
194 sell_currency: sell_currency.into(),
195 sell_amount,
196 buy_currency: buy_currency.into(),
197 price,
198 created_at: now_secs(),
199 };
200 self.next_id += 1;
201 self.orders.push(order.clone());
202 order
203 }
204
205 pub fn list_offers(&self, currency_pair: Option<(&str, &str)>) -> Vec<&SellOrder> {
210 match currency_pair {
211 None => self.orders.iter().collect(),
212 Some((sell, buy)) => self
213 .orders
214 .iter()
215 .filter(|o| o.sell_currency == sell && o.buy_currency == buy)
216 .collect(),
217 }
218 }
219
220 pub fn cancel_order(&mut self, id: &str, seller: &str) -> Result<SellOrder, PaymentError> {
222 let idx = self
223 .orders
224 .iter()
225 .position(|o| o.id == id)
226 .ok_or_else(|| PaymentError::InvalidTxo(format!("order {id} not found")))?;
227
228 if self.orders[idx].seller != seller {
229 return Err(PaymentError::InvalidTxo(format!(
230 "order {id} belongs to {}, not {seller}",
231 self.orders[idx].seller
232 )));
233 }
234
235 Ok(self.orders.remove(idx))
236 }
237
238 pub fn execute_swap(
244 &mut self,
245 id: &str,
246 buyer: &str,
247 ledger: &mut WebLedger,
248 ) -> Result<SwapResult, PaymentError> {
249 let idx = self
250 .orders
251 .iter()
252 .position(|o| o.id == id)
253 .ok_or_else(|| PaymentError::InvalidTxo(format!("order {id} not found")))?;
254
255 let order = &self.orders[idx];
256
257 let total_cost = order
259 .sell_amount
260 .checked_mul(order.price)
261 .ok_or_else(|| PaymentError::InvalidTxo("price overflow".into()))?;
262
263 let buyer_balance = ledger.get_currency_balance(buyer, &order.buy_currency);
265 if buyer_balance < total_cost {
266 return Err(PaymentError::InsufficientBalance {
267 balance: buyer_balance,
268 cost: total_cost,
269 });
270 }
271
272 let seller_balance = ledger.get_currency_balance(&order.seller, &order.sell_currency);
274 if seller_balance < order.sell_amount {
275 return Err(PaymentError::InsufficientBalance {
276 balance: seller_balance,
277 cost: order.sell_amount,
278 });
279 }
280
281 let sell_amount = order.sell_amount;
283 let sell_currency = order.sell_currency.clone();
284 let buy_currency = order.buy_currency.clone();
285 let seller = order.seller.clone();
286
287 ledger.debit_currency(buyer, &buy_currency, total_cost)?;
290 ledger.credit_currency(&seller, &buy_currency, total_cost);
292 ledger.debit_currency(&seller, &sell_currency, sell_amount)?;
294 ledger.credit_currency(buyer, &sell_currency, sell_amount);
296
297 self.orders.remove(idx);
299
300 let new_balance_in = ledger.get_currency_balance(buyer, &buy_currency);
301 let new_balance_out = ledger.get_currency_balance(buyer, &sell_currency);
302
303 Ok(SwapResult {
304 amount_in: total_cost,
305 amount_out: sell_amount,
306 fee: 0,
307 new_balance_in,
308 new_balance_out,
309 })
310 }
311}
312
313impl Default for OrderBook {
314 fn default() -> Self {
315 Self::new()
316 }
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct AmmPool {
330 pub currency_a: String,
331 pub currency_b: String,
332 pub reserve_a: u64,
333 pub reserve_b: u64,
334 pub total_shares: u64,
335 pub fee_bps: u64,
337 shares: HashMap<String, u64>,
339}
340
341impl AmmPool {
342 pub const DEFAULT_FEE_BPS: u64 = 30;
344
345 pub fn new(currency_a: &str, currency_b: &str, fee_bps: u64) -> Self {
346 Self {
347 currency_a: currency_a.into(),
348 currency_b: currency_b.into(),
349 reserve_a: 0,
350 reserve_b: 0,
351 total_shares: 0,
352 fee_bps,
353 shares: HashMap::new(),
354 }
355 }
356
357 pub fn add_liquidity(
363 &mut self,
364 provider: &str,
365 amount_a: u64,
366 amount_b: u64,
367 ledger: &mut WebLedger,
368 ) -> Result<u64, PaymentError> {
369 if amount_a == 0 || amount_b == 0 {
370 return Err(PaymentError::InvalidTxo(
371 "liquidity amounts must be non-zero".into(),
372 ));
373 }
374
375 ledger.debit_currency(provider, &self.currency_a, amount_a)?;
377 ledger.debit_currency(provider, &self.currency_b, amount_b)?;
378
379 let shares = if self.total_shares == 0 {
380 let product = (amount_a as u128) * (amount_b as u128);
383 isqrt_u128(product) as u64
384 } else {
385 let share_a =
387 (amount_a as u128) * (self.total_shares as u128) / (self.reserve_a as u128);
388 let share_b =
389 (amount_b as u128) * (self.total_shares as u128) / (self.reserve_b as u128);
390 share_a.min(share_b) as u64
391 };
392
393 if shares == 0 {
394 return Err(PaymentError::InvalidTxo(
395 "liquidity too small to issue shares".into(),
396 ));
397 }
398
399 self.reserve_a = self.reserve_a.saturating_add(amount_a);
400 self.reserve_b = self.reserve_b.saturating_add(amount_b);
401 self.total_shares = self.total_shares.saturating_add(shares);
402 *self.shares.entry(provider.into()).or_insert(0) += shares;
403
404 Ok(shares)
405 }
406
407 pub fn remove_liquidity(
412 &mut self,
413 provider: &str,
414 shares: u64,
415 ledger: &mut WebLedger,
416 ) -> Result<(u64, u64), PaymentError> {
417 let provider_shares = self.shares.get(provider).copied().unwrap_or(0);
418
419 if provider_shares < shares {
420 return Err(PaymentError::InsufficientBalance {
421 balance: provider_shares,
422 cost: shares,
423 });
424 }
425
426 if self.total_shares == 0 {
427 return Err(PaymentError::InvalidTxo("pool has no shares".into()));
428 }
429
430 let amount_a =
432 ((self.reserve_a as u128) * (shares as u128) / (self.total_shares as u128)) as u64;
433 let amount_b =
434 ((self.reserve_b as u128) * (shares as u128) / (self.total_shares as u128)) as u64;
435
436 self.reserve_a = self.reserve_a.saturating_sub(amount_a);
437 self.reserve_b = self.reserve_b.saturating_sub(amount_b);
438 self.total_shares = self.total_shares.saturating_sub(shares);
439
440 let entry = self.shares.get_mut(provider).unwrap();
441 *entry -= shares;
442 if *entry == 0 {
443 self.shares.remove(provider);
444 }
445
446 ledger.credit_currency(provider, &self.currency_a, amount_a);
448 ledger.credit_currency(provider, &self.currency_b, amount_b);
449
450 Ok((amount_a, amount_b))
451 }
452
453 pub fn swap(
463 &mut self,
464 trader: &str,
465 from_currency: &str,
466 amount_in: u64,
467 ledger: &mut WebLedger,
468 ) -> Result<SwapResult, PaymentError> {
469 if amount_in == 0 {
470 return Err(PaymentError::InvalidTxo(
471 "swap amount must be non-zero".into(),
472 ));
473 }
474
475 let (reserve_in, reserve_out, to_currency) = if from_currency == self.currency_a {
476 (self.reserve_a, self.reserve_b, self.currency_b.clone())
477 } else if from_currency == self.currency_b {
478 (self.reserve_b, self.reserve_a, self.currency_a.clone())
479 } else {
480 return Err(PaymentError::InvalidTxo(format!(
481 "currency {from_currency} not in pool ({}/{})",
482 self.currency_a, self.currency_b
483 )));
484 };
485
486 if reserve_in == 0 || reserve_out == 0 {
487 return Err(PaymentError::InvalidTxo("pool is empty".into()));
488 }
489
490 let fee_factor = 10_000u128 - (self.fee_bps as u128);
492 let numerator = (reserve_out as u128) * (amount_in as u128) * fee_factor;
493 let denominator = (reserve_in as u128) * 10_000u128 + (amount_in as u128) * fee_factor;
494
495 let amount_out = (numerator / denominator) as u64;
496
497 if amount_out == 0 {
498 return Err(PaymentError::InvalidTxo(
499 "swap output rounds to zero".into(),
500 ));
501 }
502
503 let effective_input = ((amount_in as u128) * fee_factor / 10_000u128) as u64;
506 let fee = amount_in - effective_input;
507
508 ledger.debit_currency(trader, from_currency, amount_in)?;
510 ledger.credit_currency(trader, &to_currency, amount_out);
511
512 if from_currency == self.currency_a {
514 self.reserve_a = self.reserve_a.saturating_add(amount_in);
515 self.reserve_b = self.reserve_b.saturating_sub(amount_out);
516 } else {
517 self.reserve_b = self.reserve_b.saturating_add(amount_in);
518 self.reserve_a = self.reserve_a.saturating_sub(amount_out);
519 }
520
521 let new_balance_in = ledger.get_currency_balance(trader, from_currency);
522 let new_balance_out = ledger.get_currency_balance(trader, &to_currency);
523
524 Ok(SwapResult {
525 amount_in,
526 amount_out,
527 fee,
528 new_balance_in,
529 new_balance_out,
530 })
531 }
532
533 pub fn pool_info(&self) -> serde_json::Value {
535 serde_json::json!({
536 "currency_a": self.currency_a,
537 "currency_b": self.currency_b,
538 "reserve_a": self.reserve_a,
539 "reserve_b": self.reserve_b,
540 "total_shares": self.total_shares,
541 "fee_bps": self.fee_bps,
542 "invariant_k": (self.reserve_a as u128) * (self.reserve_b as u128),
543 "providers": self.shares.len(),
544 })
545 }
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
554pub struct Exchange {
555 pub order_book: OrderBook,
556 pub pools: HashMap<String, AmmPool>,
557}
558
559impl Exchange {
560 pub fn new() -> Self {
561 Self {
562 order_book: OrderBook::new(),
563 pools: HashMap::new(),
564 }
565 }
566
567 pub fn get_or_create_pool(
572 &mut self,
573 currency_a: &str,
574 currency_b: &str,
575 fee_bps: u64,
576 ) -> &mut AmmPool {
577 let key = pool_key(currency_a, currency_b);
578 self.pools
579 .entry(key)
580 .or_insert_with(|| AmmPool::new(currency_a, currency_b, fee_bps))
581 }
582
583 pub fn get_pool(&self, currency_a: &str, currency_b: &str) -> Option<&AmmPool> {
585 let key = pool_key(currency_a, currency_b);
586 self.pools.get(&key)
587 }
588}
589
590impl Default for Exchange {
591 fn default() -> Self {
592 Self::new()
593 }
594}
595
596fn pool_key(a: &str, b: &str) -> String {
602 if a <= b {
603 format!("{a}/{b}")
604 } else {
605 format!("{b}/{a}")
606 }
607}
608
609fn isqrt_u128(n: u128) -> u128 {
611 if n == 0 {
612 return 0;
613 }
614 let mut x = n;
615 let mut y = x.div_ceil(2);
616 while y < x {
617 x = y;
618 y = (x + n / x) / 2;
619 }
620 x
621}
622
623fn now_secs() -> u64 {
625 #[cfg(target_arch = "wasm32")]
626 {
627 (js_sys::Date::now() / 1000.0) as u64
628 }
629 #[cfg(not(target_arch = "wasm32"))]
630 {
631 std::time::SystemTime::now()
632 .duration_since(std::time::UNIX_EPOCH)
633 .unwrap_or_default()
634 .as_secs()
635 }
636}
637
638#[cfg(test)]
643mod tests {
644 use super::*;
645 use crate::payments::WebLedger;
646
647 fn setup_ledger() -> WebLedger {
649 let mut ledger = WebLedger::new("Test Exchange");
650 ledger.credit_currency("did:nostr:alice", "tbtc4", 10_000);
651 ledger.credit_currency("did:nostr:alice", "tbtc3", 5_000);
652 ledger.credit_currency("did:nostr:bob", "tbtc4", 8_000);
653 ledger.credit_currency("did:nostr:bob", "tbtc3", 12_000);
654 ledger
655 }
656
657 #[test]
660 fn test_order_create_and_list() {
661 let mut book = OrderBook::new();
662 book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
663 book.create_order("did:nostr:bob", "tbtc3", 50, "tbtc4", 1);
664 book.create_order("did:nostr:alice", "tbtc4", 200, "tbtc3", 3);
665
666 assert_eq!(book.list_offers(None).len(), 3);
668
669 let filtered = book.list_offers(Some(("tbtc4", "tbtc3")));
671 assert_eq!(filtered.len(), 2);
672 assert!(filtered.iter().all(|o| o.sell_currency == "tbtc4"));
673
674 let filtered2 = book.list_offers(Some(("tbtc3", "tbtc4")));
676 assert_eq!(filtered2.len(), 1);
677 assert_eq!(filtered2[0].seller, "did:nostr:bob");
678 }
679
680 #[test]
681 fn test_order_cancel_by_seller() {
682 let mut book = OrderBook::new();
683 let order = book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
684
685 let err = book.cancel_order(&order.id, "did:nostr:bob").unwrap_err();
687 assert!(
688 format!("{err}").contains("belongs to"),
689 "Expected ownership error, got: {err}"
690 );
691
692 let cancelled = book.cancel_order(&order.id, "did:nostr:alice").unwrap();
694 assert_eq!(cancelled.id, order.id);
695 assert_eq!(book.list_offers(None).len(), 0);
696 }
697
698 #[test]
699 fn test_order_execute_swap() {
700 let mut ledger = setup_ledger();
701 let mut book = OrderBook::new();
702
703 let order = book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
705
706 let result = book
708 .execute_swap(&order.id, "did:nostr:bob", &mut ledger)
709 .unwrap();
710
711 assert_eq!(result.amount_in, 200); assert_eq!(result.amount_out, 100); assert_eq!(result.fee, 0); assert_eq!(
717 ledger.get_currency_balance("did:nostr:bob", "tbtc3"),
718 12_000 - 200
719 );
720 assert_eq!(
721 ledger.get_currency_balance("did:nostr:bob", "tbtc4"),
722 8_000 + 100
723 );
724 assert_eq!(
725 ledger.get_currency_balance("did:nostr:alice", "tbtc3"),
726 5_000 + 200
727 );
728 assert_eq!(
729 ledger.get_currency_balance("did:nostr:alice", "tbtc4"),
730 10_000 - 100
731 );
732
733 assert_eq!(book.list_offers(None).len(), 0);
735 }
736
737 #[test]
738 fn test_order_swap_insufficient_balance() {
739 let mut ledger = setup_ledger();
740 let mut book = OrderBook::new();
741
742 let order = book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 200);
744
745 let err = book
747 .execute_swap(&order.id, "did:nostr:bob", &mut ledger)
748 .unwrap_err();
749 assert!(matches!(err, PaymentError::InsufficientBalance { .. }));
750
751 assert_eq!(book.list_offers(None).len(), 1);
753 }
754
755 #[test]
758 fn test_amm_add_liquidity_first() {
759 let mut ledger = setup_ledger();
760 let mut pool = AmmPool::new("tbtc4", "tbtc3", AmmPool::DEFAULT_FEE_BPS);
761
762 let shares = pool
763 .add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
764 .unwrap();
765
766 assert_eq!(shares, isqrt_u128(2_000_000) as u64);
768 assert_eq!(pool.reserve_a, 1_000);
769 assert_eq!(pool.reserve_b, 2_000);
770 assert_eq!(pool.total_shares, shares);
771
772 assert_eq!(
774 ledger.get_currency_balance("did:nostr:alice", "tbtc4"),
775 10_000 - 1_000
776 );
777 assert_eq!(
778 ledger.get_currency_balance("did:nostr:alice", "tbtc3"),
779 5_000 - 2_000
780 );
781 }
782
783 #[test]
784 fn test_amm_add_liquidity_subsequent() {
785 let mut ledger = setup_ledger();
786 let mut pool = AmmPool::new("tbtc4", "tbtc3", AmmPool::DEFAULT_FEE_BPS);
787
788 let shares_alice = pool
789 .add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
790 .unwrap();
791
792 let shares_bob = pool
794 .add_liquidity("did:nostr:bob", 500, 1_000, &mut ledger)
795 .unwrap();
796
797 assert_eq!(shares_bob, shares_alice / 2);
800 assert_eq!(pool.reserve_a, 1_500);
801 assert_eq!(pool.reserve_b, 3_000);
802 assert_eq!(pool.total_shares, shares_alice + shares_bob);
803 }
804
805 #[test]
806 fn test_amm_swap_constant_product() {
807 let mut ledger = setup_ledger();
808 let mut pool = AmmPool::new("tbtc4", "tbtc3", 0); pool.add_liquidity("did:nostr:alice", 5_000, 5_000, &mut ledger)
811 .unwrap();
812
813 let k_before = (pool.reserve_a as u128) * (pool.reserve_b as u128);
814
815 let result = pool
817 .swap("did:nostr:bob", "tbtc4", 1_000, &mut ledger)
818 .unwrap();
819
820 let k_after = (pool.reserve_a as u128) * (pool.reserve_b as u128);
821
822 assert!(k_after >= k_before, "k decreased: {k_before} → {k_after}");
824
825 assert_eq!(result.amount_out, 833);
828 assert_eq!(result.amount_in, 1_000);
829
830 assert_eq!(pool.reserve_a, 6_000);
832 assert_eq!(pool.reserve_b, 5_000 - 833);
833 }
834
835 #[test]
836 fn test_amm_swap_fee_collection() {
837 let mut ledger = setup_ledger();
838 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30); pool.add_liquidity("did:nostr:alice", 5_000, 5_000, &mut ledger)
841 .unwrap();
842
843 let k_before = (pool.reserve_a as u128) * (pool.reserve_b as u128);
844
845 let result = pool
847 .swap("did:nostr:bob", "tbtc4", 1_000, &mut ledger)
848 .unwrap();
849
850 let k_after = (pool.reserve_a as u128) * (pool.reserve_b as u128);
851
852 assert!(
854 k_after > k_before,
855 "k should increase with fee: {k_before} → {k_after}"
856 );
857
858 assert_eq!(result.fee, 3);
860
861 assert_eq!(result.amount_out, 831);
865 }
866
867 #[test]
868 fn test_amm_remove_liquidity() {
869 let mut ledger = setup_ledger();
870 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
871
872 let shares = pool
873 .add_liquidity("did:nostr:alice", 2_000, 4_000, &mut ledger)
874 .unwrap();
875
876 pool.swap("did:nostr:bob", "tbtc4", 500, &mut ledger)
878 .unwrap();
879
880 let (got_a, got_b) = pool
882 .remove_liquidity("did:nostr:alice", shares, &mut ledger)
883 .unwrap();
884
885 assert!(
889 got_a > 2_000 || got_b > 4_000 || (got_a >= 2_000 && got_b >= 3_500),
890 "Expected fee accrual: got ({got_a}, {got_b}) vs deposited (2000, 4000)"
891 );
892
893 assert_eq!(pool.reserve_a, 0);
895 assert_eq!(pool.reserve_b, 0);
896 assert_eq!(pool.total_shares, 0);
897 }
898
899 #[test]
900 fn test_amm_swap_empty_pool() {
901 let mut ledger = setup_ledger();
902 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
903
904 let err = pool
905 .swap("did:nostr:bob", "tbtc4", 100, &mut ledger)
906 .unwrap_err();
907 assert!(
908 format!("{err}").contains("empty"),
909 "Expected empty pool error, got: {err}"
910 );
911 }
912
913 #[test]
914 fn test_exchange_multi_pool() {
915 let mut ledger = setup_ledger();
916 ledger.credit_currency("did:nostr:alice", "signet", 10_000);
918
919 let mut exchange = Exchange::new();
920
921 let pool1 = exchange.get_or_create_pool("tbtc4", "tbtc3", AmmPool::DEFAULT_FEE_BPS);
922 pool1
923 .add_liquidity("did:nostr:alice", 1_000, 1_000, &mut ledger)
924 .unwrap();
925
926 let pool2 = exchange.get_or_create_pool("tbtc4", "signet", AmmPool::DEFAULT_FEE_BPS);
927 pool2
928 .add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
929 .unwrap();
930
931 assert_eq!(exchange.pools.len(), 2);
932
933 let p1 = exchange.get_pool("tbtc4", "tbtc3").unwrap();
935 assert_eq!(p1.reserve_a, 1_000);
936
937 let p2 = exchange.get_pool("tbtc4", "signet").unwrap();
938 assert_eq!(p2.reserve_b, 2_000);
939
940 let p2_alt = exchange.get_pool("signet", "tbtc4").unwrap();
942 assert_eq!(p2_alt.reserve_b, 2_000);
943 }
944
945 #[test]
946 fn test_integer_overflow_safety() {
947 let mut ledger = WebLedger::new("Overflow Test");
948 let large = u64::MAX / 2;
949 ledger.credit_currency("did:nostr:whale", "tbtc4", large);
950 ledger.credit_currency("did:nostr:whale", "tbtc3", large);
951
952 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
953
954 let shares = pool
956 .add_liquidity("did:nostr:whale", large, large, &mut ledger)
957 .unwrap();
958 assert!(shares > 0);
959 assert_eq!(pool.reserve_a, large);
960 assert_eq!(pool.reserve_b, large);
961
962 ledger.credit_currency("did:nostr:trader", "tbtc4", 1_000_000);
964
965 let result = pool
967 .swap("did:nostr:trader", "tbtc4", 1_000_000, &mut ledger)
968 .unwrap();
969 assert!(result.amount_out > 0);
970 assert!(result.amount_out < 1_000_000); let k = (pool.reserve_a as u128) * (pool.reserve_b as u128);
974 let k_original = (large as u128) * (large as u128);
975 assert!(k >= k_original);
976 }
977
978 #[test]
981 fn test_exchange_serialization_roundtrip() {
982 let mut exchange = Exchange::new();
983 exchange
984 .order_book
985 .create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
986 exchange.get_or_create_pool("tbtc4", "tbtc3", 30);
987
988 let json = serde_json::to_string(&exchange).unwrap();
989 let parsed: Exchange = serde_json::from_str(&json).unwrap();
990
991 assert_eq!(parsed.order_book.list_offers(None).len(), 1);
992 assert_eq!(parsed.pools.len(), 1);
993 }
994
995 #[test]
998 fn test_pool_info() {
999 let mut ledger = setup_ledger();
1000 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
1001 pool.add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
1002 .unwrap();
1003
1004 let info = pool.pool_info();
1005 assert_eq!(info["currency_a"], "tbtc4");
1006 assert_eq!(info["reserve_a"], 1_000);
1007 assert_eq!(info["reserve_b"], 2_000);
1008 assert_eq!(info["fee_bps"], 30);
1009 assert_eq!(info["invariant_k"], 2_000_000u64);
1010 assert_eq!(info["providers"], 1);
1011 }
1012
1013 #[test]
1016 fn test_isqrt() {
1017 assert_eq!(isqrt_u128(0), 0);
1018 assert_eq!(isqrt_u128(1), 1);
1019 assert_eq!(isqrt_u128(4), 2);
1020 assert_eq!(isqrt_u128(9), 3);
1021 assert_eq!(isqrt_u128(10), 3);
1022 assert_eq!(isqrt_u128(2_000_000), 1414);
1023 let max = u64::MAX as u128;
1025 assert_eq!(isqrt_u128(max * max), max);
1026 }
1027}