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(_) => Err(PaymentError::InsufficientBalance {
118 balance: 0,
119 cost: amount,
120 }),
121 }
122 }
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct SwapResult {
132 pub amount_in: u64,
133 pub amount_out: u64,
134 pub fee: u64,
135 pub new_balance_in: u64,
136 pub new_balance_out: u64,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct SellOrder {
146 pub id: String,
147 pub seller: String,
149 pub sell_currency: String,
151 pub sell_amount: u64,
153 pub buy_currency: String,
155 pub price: u64,
157 pub created_at: u64,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct OrderBook {
164 orders: Vec<SellOrder>,
165 next_id: u64,
166}
167
168impl OrderBook {
169 pub fn new() -> Self {
170 Self {
171 orders: Vec::new(),
172 next_id: 1,
173 }
174 }
175
176 pub fn create_order(
181 &mut self,
182 seller: &str,
183 sell_currency: &str,
184 sell_amount: u64,
185 buy_currency: &str,
186 price: u64,
187 ) -> SellOrder {
188 let order = SellOrder {
189 id: self.next_id.to_string(),
190 seller: seller.into(),
191 sell_currency: sell_currency.into(),
192 sell_amount,
193 buy_currency: buy_currency.into(),
194 price,
195 created_at: now_secs(),
196 };
197 self.next_id += 1;
198 self.orders.push(order.clone());
199 order
200 }
201
202 pub fn list_offers(&self, currency_pair: Option<(&str, &str)>) -> Vec<&SellOrder> {
207 match currency_pair {
208 None => self.orders.iter().collect(),
209 Some((sell, buy)) => self
210 .orders
211 .iter()
212 .filter(|o| o.sell_currency == sell && o.buy_currency == buy)
213 .collect(),
214 }
215 }
216
217 pub fn cancel_order(&mut self, id: &str, seller: &str) -> Result<SellOrder, PaymentError> {
219 let idx = self
220 .orders
221 .iter()
222 .position(|o| o.id == id)
223 .ok_or_else(|| PaymentError::InvalidTxo(format!("order {id} not found")))?;
224
225 if self.orders[idx].seller != seller {
226 return Err(PaymentError::InvalidTxo(format!(
227 "order {id} belongs to {}, not {seller}",
228 self.orders[idx].seller
229 )));
230 }
231
232 Ok(self.orders.remove(idx))
233 }
234
235 pub fn execute_swap(
241 &mut self,
242 id: &str,
243 buyer: &str,
244 ledger: &mut WebLedger,
245 ) -> Result<SwapResult, PaymentError> {
246 let idx = self
247 .orders
248 .iter()
249 .position(|o| o.id == id)
250 .ok_or_else(|| PaymentError::InvalidTxo(format!("order {id} not found")))?;
251
252 let order = &self.orders[idx];
253
254 let total_cost = order
256 .sell_amount
257 .checked_mul(order.price)
258 .ok_or_else(|| PaymentError::InvalidTxo("price overflow".into()))?;
259
260 let buyer_balance = ledger.get_currency_balance(buyer, &order.buy_currency);
262 if buyer_balance < total_cost {
263 return Err(PaymentError::InsufficientBalance {
264 balance: buyer_balance,
265 cost: total_cost,
266 });
267 }
268
269 let seller_balance = ledger.get_currency_balance(&order.seller, &order.sell_currency);
271 if seller_balance < order.sell_amount {
272 return Err(PaymentError::InsufficientBalance {
273 balance: seller_balance,
274 cost: order.sell_amount,
275 });
276 }
277
278 let sell_amount = order.sell_amount;
280 let sell_currency = order.sell_currency.clone();
281 let buy_currency = order.buy_currency.clone();
282 let seller = order.seller.clone();
283
284 ledger.debit_currency(buyer, &buy_currency, total_cost)?;
287 ledger.credit_currency(&seller, &buy_currency, total_cost);
289 ledger.debit_currency(&seller, &sell_currency, sell_amount)?;
291 ledger.credit_currency(buyer, &sell_currency, sell_amount);
293
294 self.orders.remove(idx);
296
297 let new_balance_in = ledger.get_currency_balance(buyer, &buy_currency);
298 let new_balance_out = ledger.get_currency_balance(buyer, &sell_currency);
299
300 Ok(SwapResult {
301 amount_in: total_cost,
302 amount_out: sell_amount,
303 fee: 0,
304 new_balance_in,
305 new_balance_out,
306 })
307 }
308}
309
310impl Default for OrderBook {
311 fn default() -> Self {
312 Self::new()
313 }
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct AmmPool {
327 pub currency_a: String,
328 pub currency_b: String,
329 pub reserve_a: u64,
330 pub reserve_b: u64,
331 pub total_shares: u64,
332 pub fee_bps: u64,
334 shares: HashMap<String, u64>,
336}
337
338impl AmmPool {
339 pub const DEFAULT_FEE_BPS: u64 = 30;
341
342 pub fn new(currency_a: &str, currency_b: &str, fee_bps: u64) -> Self {
343 Self {
344 currency_a: currency_a.into(),
345 currency_b: currency_b.into(),
346 reserve_a: 0,
347 reserve_b: 0,
348 total_shares: 0,
349 fee_bps,
350 shares: HashMap::new(),
351 }
352 }
353
354 pub fn add_liquidity(
360 &mut self,
361 provider: &str,
362 amount_a: u64,
363 amount_b: u64,
364 ledger: &mut WebLedger,
365 ) -> Result<u64, PaymentError> {
366 if amount_a == 0 || amount_b == 0 {
367 return Err(PaymentError::InvalidTxo(
368 "liquidity amounts must be non-zero".into(),
369 ));
370 }
371
372 ledger.debit_currency(provider, &self.currency_a, amount_a)?;
374 ledger.debit_currency(provider, &self.currency_b, amount_b)?;
375
376 let shares = if self.total_shares == 0 {
377 let product = (amount_a as u128) * (amount_b as u128);
380 isqrt_u128(product) as u64
381 } else {
382 let share_a =
384 (amount_a as u128) * (self.total_shares as u128) / (self.reserve_a as u128);
385 let share_b =
386 (amount_b as u128) * (self.total_shares as u128) / (self.reserve_b as u128);
387 share_a.min(share_b) as u64
388 };
389
390 if shares == 0 {
391 return Err(PaymentError::InvalidTxo(
392 "liquidity too small to issue shares".into(),
393 ));
394 }
395
396 self.reserve_a = self.reserve_a.saturating_add(amount_a);
397 self.reserve_b = self.reserve_b.saturating_add(amount_b);
398 self.total_shares = self.total_shares.saturating_add(shares);
399 *self.shares.entry(provider.into()).or_insert(0) += shares;
400
401 Ok(shares)
402 }
403
404 pub fn remove_liquidity(
409 &mut self,
410 provider: &str,
411 shares: u64,
412 ledger: &mut WebLedger,
413 ) -> Result<(u64, u64), PaymentError> {
414 let provider_shares = self.shares.get(provider).copied().unwrap_or(0);
415
416 if provider_shares < shares {
417 return Err(PaymentError::InsufficientBalance {
418 balance: provider_shares,
419 cost: shares,
420 });
421 }
422
423 if self.total_shares == 0 {
424 return Err(PaymentError::InvalidTxo("pool has no shares".into()));
425 }
426
427 let amount_a =
429 ((self.reserve_a as u128) * (shares as u128) / (self.total_shares as u128)) as u64;
430 let amount_b =
431 ((self.reserve_b as u128) * (shares as u128) / (self.total_shares as u128)) as u64;
432
433 self.reserve_a = self.reserve_a.saturating_sub(amount_a);
434 self.reserve_b = self.reserve_b.saturating_sub(amount_b);
435 self.total_shares = self.total_shares.saturating_sub(shares);
436
437 let entry = self.shares.get_mut(provider).unwrap();
438 *entry -= shares;
439 if *entry == 0 {
440 self.shares.remove(provider);
441 }
442
443 ledger.credit_currency(provider, &self.currency_a, amount_a);
445 ledger.credit_currency(provider, &self.currency_b, amount_b);
446
447 Ok((amount_a, amount_b))
448 }
449
450 pub fn swap(
460 &mut self,
461 trader: &str,
462 from_currency: &str,
463 amount_in: u64,
464 ledger: &mut WebLedger,
465 ) -> Result<SwapResult, PaymentError> {
466 if amount_in == 0 {
467 return Err(PaymentError::InvalidTxo(
468 "swap amount must be non-zero".into(),
469 ));
470 }
471
472 let (reserve_in, reserve_out, to_currency) = if from_currency == self.currency_a {
473 (self.reserve_a, self.reserve_b, self.currency_b.clone())
474 } else if from_currency == self.currency_b {
475 (self.reserve_b, self.reserve_a, self.currency_a.clone())
476 } else {
477 return Err(PaymentError::InvalidTxo(format!(
478 "currency {from_currency} not in pool ({}/{})",
479 self.currency_a, self.currency_b
480 )));
481 };
482
483 if reserve_in == 0 || reserve_out == 0 {
484 return Err(PaymentError::InvalidTxo("pool is empty".into()));
485 }
486
487 let fee_factor = 10_000u128 - (self.fee_bps as u128);
489 let numerator = (reserve_out as u128) * (amount_in as u128) * fee_factor;
490 let denominator = (reserve_in as u128) * 10_000u128 + (amount_in as u128) * fee_factor;
491
492 let amount_out = (numerator / denominator) as u64;
493
494 if amount_out == 0 {
495 return Err(PaymentError::InvalidTxo(
496 "swap output rounds to zero".into(),
497 ));
498 }
499
500 let effective_input = ((amount_in as u128) * fee_factor / 10_000u128) as u64;
503 let fee = amount_in - effective_input;
504
505 ledger.debit_currency(trader, from_currency, amount_in)?;
507 ledger.credit_currency(trader, &to_currency, amount_out);
508
509 if from_currency == self.currency_a {
511 self.reserve_a = self.reserve_a.saturating_add(amount_in);
512 self.reserve_b = self.reserve_b.saturating_sub(amount_out);
513 } else {
514 self.reserve_b = self.reserve_b.saturating_add(amount_in);
515 self.reserve_a = self.reserve_a.saturating_sub(amount_out);
516 }
517
518 let new_balance_in = ledger.get_currency_balance(trader, from_currency);
519 let new_balance_out = ledger.get_currency_balance(trader, &to_currency);
520
521 Ok(SwapResult {
522 amount_in,
523 amount_out,
524 fee,
525 new_balance_in,
526 new_balance_out,
527 })
528 }
529
530 pub fn pool_info(&self) -> serde_json::Value {
532 serde_json::json!({
533 "currency_a": self.currency_a,
534 "currency_b": self.currency_b,
535 "reserve_a": self.reserve_a,
536 "reserve_b": self.reserve_b,
537 "total_shares": self.total_shares,
538 "fee_bps": self.fee_bps,
539 "invariant_k": (self.reserve_a as u128) * (self.reserve_b as u128),
540 "providers": self.shares.len(),
541 })
542 }
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct Exchange {
552 pub order_book: OrderBook,
553 pub pools: HashMap<String, AmmPool>,
554}
555
556impl Exchange {
557 pub fn new() -> Self {
558 Self {
559 order_book: OrderBook::new(),
560 pools: HashMap::new(),
561 }
562 }
563
564 pub fn get_or_create_pool(
569 &mut self,
570 currency_a: &str,
571 currency_b: &str,
572 fee_bps: u64,
573 ) -> &mut AmmPool {
574 let key = pool_key(currency_a, currency_b);
575 self.pools
576 .entry(key)
577 .or_insert_with(|| AmmPool::new(currency_a, currency_b, fee_bps))
578 }
579
580 pub fn get_pool(&self, currency_a: &str, currency_b: &str) -> Option<&AmmPool> {
582 let key = pool_key(currency_a, currency_b);
583 self.pools.get(&key)
584 }
585}
586
587impl Default for Exchange {
588 fn default() -> Self {
589 Self::new()
590 }
591}
592
593fn pool_key(a: &str, b: &str) -> String {
599 if a <= b {
600 format!("{a}/{b}")
601 } else {
602 format!("{b}/{a}")
603 }
604}
605
606fn isqrt_u128(n: u128) -> u128 {
608 if n == 0 {
609 return 0;
610 }
611 let mut x = n;
612 let mut y = (x + 1) / 2;
613 while y < x {
614 x = y;
615 y = (x + n / x) / 2;
616 }
617 x
618}
619
620fn now_secs() -> u64 {
622 #[cfg(target_arch = "wasm32")]
623 {
624 (js_sys::Date::now() / 1000.0) as u64
625 }
626 #[cfg(not(target_arch = "wasm32"))]
627 {
628 std::time::SystemTime::now()
629 .duration_since(std::time::UNIX_EPOCH)
630 .unwrap_or_default()
631 .as_secs()
632 }
633}
634
635#[cfg(test)]
640mod tests {
641 use super::*;
642 use crate::payments::WebLedger;
643
644 fn setup_ledger() -> WebLedger {
646 let mut ledger = WebLedger::new("Test Exchange");
647 ledger.credit_currency("did:nostr:alice", "tbtc4", 10_000);
648 ledger.credit_currency("did:nostr:alice", "tbtc3", 5_000);
649 ledger.credit_currency("did:nostr:bob", "tbtc4", 8_000);
650 ledger.credit_currency("did:nostr:bob", "tbtc3", 12_000);
651 ledger
652 }
653
654 #[test]
657 fn test_order_create_and_list() {
658 let mut book = OrderBook::new();
659 book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
660 book.create_order("did:nostr:bob", "tbtc3", 50, "tbtc4", 1);
661 book.create_order("did:nostr:alice", "tbtc4", 200, "tbtc3", 3);
662
663 assert_eq!(book.list_offers(None).len(), 3);
665
666 let filtered = book.list_offers(Some(("tbtc4", "tbtc3")));
668 assert_eq!(filtered.len(), 2);
669 assert!(filtered.iter().all(|o| o.sell_currency == "tbtc4"));
670
671 let filtered2 = book.list_offers(Some(("tbtc3", "tbtc4")));
673 assert_eq!(filtered2.len(), 1);
674 assert_eq!(filtered2[0].seller, "did:nostr:bob");
675 }
676
677 #[test]
678 fn test_order_cancel_by_seller() {
679 let mut book = OrderBook::new();
680 let order = book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
681
682 let err = book.cancel_order(&order.id, "did:nostr:bob").unwrap_err();
684 assert!(
685 format!("{err}").contains("belongs to"),
686 "Expected ownership error, got: {err}"
687 );
688
689 let cancelled = book.cancel_order(&order.id, "did:nostr:alice").unwrap();
691 assert_eq!(cancelled.id, order.id);
692 assert_eq!(book.list_offers(None).len(), 0);
693 }
694
695 #[test]
696 fn test_order_execute_swap() {
697 let mut ledger = setup_ledger();
698 let mut book = OrderBook::new();
699
700 let order = book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
702
703 let result = book
705 .execute_swap(&order.id, "did:nostr:bob", &mut ledger)
706 .unwrap();
707
708 assert_eq!(result.amount_in, 200); assert_eq!(result.amount_out, 100); assert_eq!(result.fee, 0); assert_eq!(
714 ledger.get_currency_balance("did:nostr:bob", "tbtc3"),
715 12_000 - 200
716 );
717 assert_eq!(
718 ledger.get_currency_balance("did:nostr:bob", "tbtc4"),
719 8_000 + 100
720 );
721 assert_eq!(
722 ledger.get_currency_balance("did:nostr:alice", "tbtc3"),
723 5_000 + 200
724 );
725 assert_eq!(
726 ledger.get_currency_balance("did:nostr:alice", "tbtc4"),
727 10_000 - 100
728 );
729
730 assert_eq!(book.list_offers(None).len(), 0);
732 }
733
734 #[test]
735 fn test_order_swap_insufficient_balance() {
736 let mut ledger = setup_ledger();
737 let mut book = OrderBook::new();
738
739 let order = book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 200);
741
742 let err = book
744 .execute_swap(&order.id, "did:nostr:bob", &mut ledger)
745 .unwrap_err();
746 assert!(matches!(err, PaymentError::InsufficientBalance { .. }));
747
748 assert_eq!(book.list_offers(None).len(), 1);
750 }
751
752 #[test]
755 fn test_amm_add_liquidity_first() {
756 let mut ledger = setup_ledger();
757 let mut pool = AmmPool::new("tbtc4", "tbtc3", AmmPool::DEFAULT_FEE_BPS);
758
759 let shares = pool
760 .add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
761 .unwrap();
762
763 assert_eq!(shares, isqrt_u128(2_000_000) as u64);
765 assert_eq!(pool.reserve_a, 1_000);
766 assert_eq!(pool.reserve_b, 2_000);
767 assert_eq!(pool.total_shares, shares);
768
769 assert_eq!(
771 ledger.get_currency_balance("did:nostr:alice", "tbtc4"),
772 10_000 - 1_000
773 );
774 assert_eq!(
775 ledger.get_currency_balance("did:nostr:alice", "tbtc3"),
776 5_000 - 2_000
777 );
778 }
779
780 #[test]
781 fn test_amm_add_liquidity_subsequent() {
782 let mut ledger = setup_ledger();
783 let mut pool = AmmPool::new("tbtc4", "tbtc3", AmmPool::DEFAULT_FEE_BPS);
784
785 let shares_alice = pool
786 .add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
787 .unwrap();
788
789 let shares_bob = pool
791 .add_liquidity("did:nostr:bob", 500, 1_000, &mut ledger)
792 .unwrap();
793
794 assert_eq!(shares_bob, shares_alice / 2);
797 assert_eq!(pool.reserve_a, 1_500);
798 assert_eq!(pool.reserve_b, 3_000);
799 assert_eq!(pool.total_shares, shares_alice + shares_bob);
800 }
801
802 #[test]
803 fn test_amm_swap_constant_product() {
804 let mut ledger = setup_ledger();
805 let mut pool = AmmPool::new("tbtc4", "tbtc3", 0); pool.add_liquidity("did:nostr:alice", 5_000, 5_000, &mut ledger)
808 .unwrap();
809
810 let k_before = (pool.reserve_a as u128) * (pool.reserve_b as u128);
811
812 let result = pool
814 .swap("did:nostr:bob", "tbtc4", 1_000, &mut ledger)
815 .unwrap();
816
817 let k_after = (pool.reserve_a as u128) * (pool.reserve_b as u128);
818
819 assert!(k_after >= k_before, "k decreased: {k_before} → {k_after}");
821
822 assert_eq!(result.amount_out, 833);
825 assert_eq!(result.amount_in, 1_000);
826
827 assert_eq!(pool.reserve_a, 6_000);
829 assert_eq!(pool.reserve_b, 5_000 - 833);
830 }
831
832 #[test]
833 fn test_amm_swap_fee_collection() {
834 let mut ledger = setup_ledger();
835 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30); pool.add_liquidity("did:nostr:alice", 5_000, 5_000, &mut ledger)
838 .unwrap();
839
840 let k_before = (pool.reserve_a as u128) * (pool.reserve_b as u128);
841
842 let result = pool
844 .swap("did:nostr:bob", "tbtc4", 1_000, &mut ledger)
845 .unwrap();
846
847 let k_after = (pool.reserve_a as u128) * (pool.reserve_b as u128);
848
849 assert!(
851 k_after > k_before,
852 "k should increase with fee: {k_before} → {k_after}"
853 );
854
855 assert_eq!(result.fee, 3);
857
858 assert_eq!(result.amount_out, 831);
862 }
863
864 #[test]
865 fn test_amm_remove_liquidity() {
866 let mut ledger = setup_ledger();
867 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
868
869 let shares = pool
870 .add_liquidity("did:nostr:alice", 2_000, 4_000, &mut ledger)
871 .unwrap();
872
873 pool.swap("did:nostr:bob", "tbtc4", 500, &mut ledger)
875 .unwrap();
876
877 let (got_a, got_b) = pool
879 .remove_liquidity("did:nostr:alice", shares, &mut ledger)
880 .unwrap();
881
882 assert!(
886 got_a > 2_000 || got_b > 4_000 || (got_a >= 2_000 && got_b >= 3_500),
887 "Expected fee accrual: got ({got_a}, {got_b}) vs deposited (2000, 4000)"
888 );
889
890 assert_eq!(pool.reserve_a, 0);
892 assert_eq!(pool.reserve_b, 0);
893 assert_eq!(pool.total_shares, 0);
894 }
895
896 #[test]
897 fn test_amm_swap_empty_pool() {
898 let mut ledger = setup_ledger();
899 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
900
901 let err = pool
902 .swap("did:nostr:bob", "tbtc4", 100, &mut ledger)
903 .unwrap_err();
904 assert!(
905 format!("{err}").contains("empty"),
906 "Expected empty pool error, got: {err}"
907 );
908 }
909
910 #[test]
911 fn test_exchange_multi_pool() {
912 let mut ledger = setup_ledger();
913 ledger.credit_currency("did:nostr:alice", "signet", 10_000);
915
916 let mut exchange = Exchange::new();
917
918 let pool1 = exchange.get_or_create_pool("tbtc4", "tbtc3", AmmPool::DEFAULT_FEE_BPS);
919 pool1
920 .add_liquidity("did:nostr:alice", 1_000, 1_000, &mut ledger)
921 .unwrap();
922
923 let pool2 = exchange.get_or_create_pool("tbtc4", "signet", AmmPool::DEFAULT_FEE_BPS);
924 pool2
925 .add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
926 .unwrap();
927
928 assert_eq!(exchange.pools.len(), 2);
929
930 let p1 = exchange.get_pool("tbtc4", "tbtc3").unwrap();
932 assert_eq!(p1.reserve_a, 1_000);
933
934 let p2 = exchange.get_pool("tbtc4", "signet").unwrap();
935 assert_eq!(p2.reserve_b, 2_000);
936
937 let p2_alt = exchange.get_pool("signet", "tbtc4").unwrap();
939 assert_eq!(p2_alt.reserve_b, 2_000);
940 }
941
942 #[test]
943 fn test_integer_overflow_safety() {
944 let mut ledger = WebLedger::new("Overflow Test");
945 let large = u64::MAX / 2;
946 ledger.credit_currency("did:nostr:whale", "tbtc4", large);
947 ledger.credit_currency("did:nostr:whale", "tbtc3", large);
948
949 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
950
951 let shares = pool
953 .add_liquidity("did:nostr:whale", large, large, &mut ledger)
954 .unwrap();
955 assert!(shares > 0);
956 assert_eq!(pool.reserve_a, large);
957 assert_eq!(pool.reserve_b, large);
958
959 ledger.credit_currency("did:nostr:trader", "tbtc4", 1_000_000);
961
962 let result = pool
964 .swap("did:nostr:trader", "tbtc4", 1_000_000, &mut ledger)
965 .unwrap();
966 assert!(result.amount_out > 0);
967 assert!(result.amount_out < 1_000_000); let k = (pool.reserve_a as u128) * (pool.reserve_b as u128);
971 let k_original = (large as u128) * (large as u128);
972 assert!(k >= k_original);
973 }
974
975 #[test]
978 fn test_exchange_serialization_roundtrip() {
979 let mut exchange = Exchange::new();
980 exchange
981 .order_book
982 .create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
983 exchange.get_or_create_pool("tbtc4", "tbtc3", 30);
984
985 let json = serde_json::to_string(&exchange).unwrap();
986 let parsed: Exchange = serde_json::from_str(&json).unwrap();
987
988 assert_eq!(parsed.order_book.list_offers(None).len(), 1);
989 assert_eq!(parsed.pools.len(), 1);
990 }
991
992 #[test]
995 fn test_pool_info() {
996 let mut ledger = setup_ledger();
997 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
998 pool.add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
999 .unwrap();
1000
1001 let info = pool.pool_info();
1002 assert_eq!(info["currency_a"], "tbtc4");
1003 assert_eq!(info["reserve_a"], 1_000);
1004 assert_eq!(info["reserve_b"], 2_000);
1005 assert_eq!(info["fee_bps"], 30);
1006 assert_eq!(info["invariant_k"], 2_000_000u64);
1007 assert_eq!(info["providers"], 1);
1008 }
1009
1010 #[test]
1013 fn test_isqrt() {
1014 assert_eq!(isqrt_u128(0), 0);
1015 assert_eq!(isqrt_u128(1), 1);
1016 assert_eq!(isqrt_u128(4), 2);
1017 assert_eq!(isqrt_u128(9), 3);
1018 assert_eq!(isqrt_u128(10), 3);
1019 assert_eq!(isqrt_u128(2_000_000), 1414);
1020 let max = u64::MAX as u128;
1022 assert_eq!(isqrt_u128(max * max), max);
1023 }
1024}