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 self.fee_bps >= 10_000 {
370 return Err(PaymentError::InvalidTxo(
371 "pool fee must be below 10000 bps".into(),
372 ));
373 }
374 if amount_a == 0 || amount_b == 0 {
375 return Err(PaymentError::InvalidTxo(
376 "liquidity amounts must be non-zero".into(),
377 ));
378 }
379
380 ledger.debit_currency(provider, &self.currency_a, amount_a)?;
382 ledger.debit_currency(provider, &self.currency_b, amount_b)?;
383
384 let shares = if self.total_shares == 0 {
385 let product = (amount_a as u128) * (amount_b as u128);
388 isqrt_u128(product) as u64
389 } else {
390 let share_a =
392 (amount_a as u128) * (self.total_shares as u128) / (self.reserve_a as u128);
393 let share_b =
394 (amount_b as u128) * (self.total_shares as u128) / (self.reserve_b as u128);
395 share_a.min(share_b) as u64
396 };
397
398 if shares == 0 {
399 return Err(PaymentError::InvalidTxo(
400 "liquidity too small to issue shares".into(),
401 ));
402 }
403
404 self.reserve_a = self.reserve_a.saturating_add(amount_a);
405 self.reserve_b = self.reserve_b.saturating_add(amount_b);
406 self.total_shares = self.total_shares.saturating_add(shares);
407 *self.shares.entry(provider.into()).or_insert(0) += shares;
408
409 Ok(shares)
410 }
411
412 pub fn remove_liquidity(
417 &mut self,
418 provider: &str,
419 shares: u64,
420 ledger: &mut WebLedger,
421 ) -> Result<(u64, u64), PaymentError> {
422 let provider_shares = self.shares.get(provider).copied().unwrap_or(0);
423
424 if provider_shares < shares {
425 return Err(PaymentError::InsufficientBalance {
426 balance: provider_shares,
427 cost: shares,
428 });
429 }
430
431 if self.total_shares == 0 {
432 return Err(PaymentError::InvalidTxo("pool has no shares".into()));
433 }
434
435 let amount_a =
437 ((self.reserve_a as u128) * (shares as u128) / (self.total_shares as u128)) as u64;
438 let amount_b =
439 ((self.reserve_b as u128) * (shares as u128) / (self.total_shares as u128)) as u64;
440
441 self.reserve_a = self.reserve_a.saturating_sub(amount_a);
442 self.reserve_b = self.reserve_b.saturating_sub(amount_b);
443 self.total_shares = self.total_shares.saturating_sub(shares);
444
445 let entry = self.shares.get_mut(provider).unwrap();
446 *entry -= shares;
447 if *entry == 0 {
448 self.shares.remove(provider);
449 }
450
451 ledger.credit_currency(provider, &self.currency_a, amount_a);
453 ledger.credit_currency(provider, &self.currency_b, amount_b);
454
455 Ok((amount_a, amount_b))
456 }
457
458 pub fn swap(
468 &mut self,
469 trader: &str,
470 from_currency: &str,
471 amount_in: u64,
472 ledger: &mut WebLedger,
473 ) -> Result<SwapResult, PaymentError> {
474 if amount_in == 0 {
475 return Err(PaymentError::InvalidTxo(
476 "swap amount must be non-zero".into(),
477 ));
478 }
479
480 let (reserve_in, reserve_out, to_currency) = if from_currency == self.currency_a {
481 (self.reserve_a, self.reserve_b, self.currency_b.clone())
482 } else if from_currency == self.currency_b {
483 (self.reserve_b, self.reserve_a, self.currency_a.clone())
484 } else {
485 return Err(PaymentError::InvalidTxo(format!(
486 "currency {from_currency} not in pool ({}/{})",
487 self.currency_a, self.currency_b
488 )));
489 };
490
491 if reserve_in == 0 || reserve_out == 0 {
492 return Err(PaymentError::InvalidTxo("pool is empty".into()));
493 }
494
495 let fee_factor = 10_000u128
497 .checked_sub(self.fee_bps as u128)
498 .filter(|factor| *factor > 0)
499 .ok_or_else(|| PaymentError::InvalidTxo("pool fee must be below 10000 bps".into()))?;
500 let numerator = (reserve_out as u128)
501 .checked_mul(amount_in as u128)
502 .and_then(|product| product.checked_mul(fee_factor))
503 .ok_or_else(|| PaymentError::InvalidTxo("swap arithmetic overflow".into()))?;
504 let denominator = (reserve_in as u128) * 10_000u128 + (amount_in as u128) * fee_factor;
505
506 let amount_out = (numerator / denominator) as u64;
507
508 if amount_out == 0 {
509 return Err(PaymentError::InvalidTxo(
510 "swap output rounds to zero".into(),
511 ));
512 }
513
514 let effective_input = ((amount_in as u128) * fee_factor / 10_000u128) as u64;
517 let fee = amount_in - effective_input;
518
519 ledger.debit_currency(trader, from_currency, amount_in)?;
521 ledger.credit_currency(trader, &to_currency, amount_out);
522
523 if from_currency == self.currency_a {
525 self.reserve_a = self.reserve_a.saturating_add(amount_in);
526 self.reserve_b = self.reserve_b.saturating_sub(amount_out);
527 } else {
528 self.reserve_b = self.reserve_b.saturating_add(amount_in);
529 self.reserve_a = self.reserve_a.saturating_sub(amount_out);
530 }
531
532 let new_balance_in = ledger.get_currency_balance(trader, from_currency);
533 let new_balance_out = ledger.get_currency_balance(trader, &to_currency);
534
535 Ok(SwapResult {
536 amount_in,
537 amount_out,
538 fee,
539 new_balance_in,
540 new_balance_out,
541 })
542 }
543
544 pub fn pool_info(&self) -> serde_json::Value {
546 serde_json::json!({
547 "currency_a": self.currency_a,
548 "currency_b": self.currency_b,
549 "reserve_a": self.reserve_a,
550 "reserve_b": self.reserve_b,
551 "total_shares": self.total_shares,
552 "fee_bps": self.fee_bps,
553 "invariant_k": (self.reserve_a as u128) * (self.reserve_b as u128),
554 "providers": self.shares.len(),
555 })
556 }
557}
558
559#[derive(Debug, Clone, Serialize, Deserialize)]
565pub struct Exchange {
566 pub order_book: OrderBook,
567 pub pools: HashMap<String, AmmPool>,
568}
569
570impl Exchange {
571 pub fn new() -> Self {
572 Self {
573 order_book: OrderBook::new(),
574 pools: HashMap::new(),
575 }
576 }
577
578 pub fn get_or_create_pool(
583 &mut self,
584 currency_a: &str,
585 currency_b: &str,
586 fee_bps: u64,
587 ) -> &mut AmmPool {
588 let key = pool_key(currency_a, currency_b);
589 self.pools
590 .entry(key)
591 .or_insert_with(|| AmmPool::new(currency_a, currency_b, fee_bps))
592 }
593
594 pub fn get_pool(&self, currency_a: &str, currency_b: &str) -> Option<&AmmPool> {
596 let key = pool_key(currency_a, currency_b);
597 self.pools.get(&key)
598 }
599}
600
601impl Default for Exchange {
602 fn default() -> Self {
603 Self::new()
604 }
605}
606
607fn pool_key(a: &str, b: &str) -> String {
613 if a <= b {
614 format!("{a}/{b}")
615 } else {
616 format!("{b}/{a}")
617 }
618}
619
620fn isqrt_u128(n: u128) -> u128 {
622 if n == 0 {
623 return 0;
624 }
625 let mut x = n;
626 let mut y = x.div_ceil(2);
627 while y < x {
628 x = y;
629 y = (x + n / x) / 2;
630 }
631 x
632}
633
634fn now_secs() -> u64 {
636 #[cfg(target_arch = "wasm32")]
637 {
638 (js_sys::Date::now() / 1000.0) as u64
639 }
640 #[cfg(not(target_arch = "wasm32"))]
641 {
642 std::time::SystemTime::now()
643 .duration_since(std::time::UNIX_EPOCH)
644 .unwrap_or_default()
645 .as_secs()
646 }
647}
648
649#[cfg(test)]
654mod tests {
655 use super::*;
656 use crate::payments::WebLedger;
657
658 fn setup_ledger() -> WebLedger {
660 let mut ledger = WebLedger::new("Test Exchange");
661 ledger.credit_currency("did:nostr:alice", "tbtc4", 10_000);
662 ledger.credit_currency("did:nostr:alice", "tbtc3", 5_000);
663 ledger.credit_currency("did:nostr:bob", "tbtc4", 8_000);
664 ledger.credit_currency("did:nostr:bob", "tbtc3", 12_000);
665 ledger
666 }
667
668 #[test]
671 fn test_order_create_and_list() {
672 let mut book = OrderBook::new();
673 book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
674 book.create_order("did:nostr:bob", "tbtc3", 50, "tbtc4", 1);
675 book.create_order("did:nostr:alice", "tbtc4", 200, "tbtc3", 3);
676
677 assert_eq!(book.list_offers(None).len(), 3);
679
680 let filtered = book.list_offers(Some(("tbtc4", "tbtc3")));
682 assert_eq!(filtered.len(), 2);
683 assert!(filtered.iter().all(|o| o.sell_currency == "tbtc4"));
684
685 let filtered2 = book.list_offers(Some(("tbtc3", "tbtc4")));
687 assert_eq!(filtered2.len(), 1);
688 assert_eq!(filtered2[0].seller, "did:nostr:bob");
689 }
690
691 #[test]
692 fn test_order_cancel_by_seller() {
693 let mut book = OrderBook::new();
694 let order = book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
695
696 let err = book.cancel_order(&order.id, "did:nostr:bob").unwrap_err();
698 assert!(
699 format!("{err}").contains("belongs to"),
700 "Expected ownership error, got: {err}"
701 );
702
703 let cancelled = book.cancel_order(&order.id, "did:nostr:alice").unwrap();
705 assert_eq!(cancelled.id, order.id);
706 assert_eq!(book.list_offers(None).len(), 0);
707 }
708
709 #[test]
710 fn test_order_execute_swap() {
711 let mut ledger = setup_ledger();
712 let mut book = OrderBook::new();
713
714 let order = book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
716
717 let result = book
719 .execute_swap(&order.id, "did:nostr:bob", &mut ledger)
720 .unwrap();
721
722 assert_eq!(result.amount_in, 200); assert_eq!(result.amount_out, 100); assert_eq!(result.fee, 0); assert_eq!(
728 ledger.get_currency_balance("did:nostr:bob", "tbtc3"),
729 12_000 - 200
730 );
731 assert_eq!(
732 ledger.get_currency_balance("did:nostr:bob", "tbtc4"),
733 8_000 + 100
734 );
735 assert_eq!(
736 ledger.get_currency_balance("did:nostr:alice", "tbtc3"),
737 5_000 + 200
738 );
739 assert_eq!(
740 ledger.get_currency_balance("did:nostr:alice", "tbtc4"),
741 10_000 - 100
742 );
743
744 assert_eq!(book.list_offers(None).len(), 0);
746 }
747
748 #[test]
749 fn test_order_swap_insufficient_balance() {
750 let mut ledger = setup_ledger();
751 let mut book = OrderBook::new();
752
753 let order = book.create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 200);
755
756 let err = book
758 .execute_swap(&order.id, "did:nostr:bob", &mut ledger)
759 .unwrap_err();
760 assert!(matches!(err, PaymentError::InsufficientBalance { .. }));
761
762 assert_eq!(book.list_offers(None).len(), 1);
764 }
765
766 #[test]
769 fn test_amm_add_liquidity_first() {
770 let mut ledger = setup_ledger();
771 let mut pool = AmmPool::new("tbtc4", "tbtc3", AmmPool::DEFAULT_FEE_BPS);
772
773 let shares = pool
774 .add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
775 .unwrap();
776
777 assert_eq!(shares, isqrt_u128(2_000_000) as u64);
779 assert_eq!(pool.reserve_a, 1_000);
780 assert_eq!(pool.reserve_b, 2_000);
781 assert_eq!(pool.total_shares, shares);
782
783 assert_eq!(
785 ledger.get_currency_balance("did:nostr:alice", "tbtc4"),
786 10_000 - 1_000
787 );
788 assert_eq!(
789 ledger.get_currency_balance("did:nostr:alice", "tbtc3"),
790 5_000 - 2_000
791 );
792 }
793
794 #[test]
795 fn test_amm_add_liquidity_subsequent() {
796 let mut ledger = setup_ledger();
797 let mut pool = AmmPool::new("tbtc4", "tbtc3", AmmPool::DEFAULT_FEE_BPS);
798
799 let shares_alice = pool
800 .add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
801 .unwrap();
802
803 let shares_bob = pool
805 .add_liquidity("did:nostr:bob", 500, 1_000, &mut ledger)
806 .unwrap();
807
808 assert_eq!(shares_bob, shares_alice / 2);
811 assert_eq!(pool.reserve_a, 1_500);
812 assert_eq!(pool.reserve_b, 3_000);
813 assert_eq!(pool.total_shares, shares_alice + shares_bob);
814 }
815
816 #[test]
817 fn test_amm_swap_constant_product() {
818 let mut ledger = setup_ledger();
819 let mut pool = AmmPool::new("tbtc4", "tbtc3", 0); pool.add_liquidity("did:nostr:alice", 5_000, 5_000, &mut ledger)
822 .unwrap();
823
824 let k_before = (pool.reserve_a as u128) * (pool.reserve_b as u128);
825
826 let result = pool
828 .swap("did:nostr:bob", "tbtc4", 1_000, &mut ledger)
829 .unwrap();
830
831 let k_after = (pool.reserve_a as u128) * (pool.reserve_b as u128);
832
833 assert!(k_after >= k_before, "k decreased: {k_before} → {k_after}");
835
836 assert_eq!(result.amount_out, 833);
839 assert_eq!(result.amount_in, 1_000);
840
841 assert_eq!(pool.reserve_a, 6_000);
843 assert_eq!(pool.reserve_b, 5_000 - 833);
844 }
845
846 #[test]
847 fn test_amm_swap_fee_collection() {
848 let mut ledger = setup_ledger();
849 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30); pool.add_liquidity("did:nostr:alice", 5_000, 5_000, &mut ledger)
852 .unwrap();
853
854 let k_before = (pool.reserve_a as u128) * (pool.reserve_b as u128);
855
856 let result = pool
858 .swap("did:nostr:bob", "tbtc4", 1_000, &mut ledger)
859 .unwrap();
860
861 let k_after = (pool.reserve_a as u128) * (pool.reserve_b as u128);
862
863 assert!(
865 k_after > k_before,
866 "k should increase with fee: {k_before} → {k_after}"
867 );
868
869 assert_eq!(result.fee, 3);
871
872 assert_eq!(result.amount_out, 831);
876 }
877
878 #[test]
879 fn test_amm_remove_liquidity() {
880 let mut ledger = setup_ledger();
881 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
882
883 let shares = pool
884 .add_liquidity("did:nostr:alice", 2_000, 4_000, &mut ledger)
885 .unwrap();
886
887 pool.swap("did:nostr:bob", "tbtc4", 500, &mut ledger)
889 .unwrap();
890
891 let (got_a, got_b) = pool
893 .remove_liquidity("did:nostr:alice", shares, &mut ledger)
894 .unwrap();
895
896 assert!(
900 got_a > 2_000 || got_b > 4_000 || (got_a >= 2_000 && got_b >= 3_500),
901 "Expected fee accrual: got ({got_a}, {got_b}) vs deposited (2000, 4000)"
902 );
903
904 assert_eq!(pool.reserve_a, 0);
906 assert_eq!(pool.reserve_b, 0);
907 assert_eq!(pool.total_shares, 0);
908 }
909
910 #[test]
911 fn test_amm_swap_empty_pool() {
912 let mut ledger = setup_ledger();
913 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
914
915 let err = pool
916 .swap("did:nostr:bob", "tbtc4", 100, &mut ledger)
917 .unwrap_err();
918 assert!(
919 format!("{err}").contains("empty"),
920 "Expected empty pool error, got: {err}"
921 );
922 }
923
924 #[test]
925 fn test_exchange_multi_pool() {
926 let mut ledger = setup_ledger();
927 ledger.credit_currency("did:nostr:alice", "signet", 10_000);
929
930 let mut exchange = Exchange::new();
931
932 let pool1 = exchange.get_or_create_pool("tbtc4", "tbtc3", AmmPool::DEFAULT_FEE_BPS);
933 pool1
934 .add_liquidity("did:nostr:alice", 1_000, 1_000, &mut ledger)
935 .unwrap();
936
937 let pool2 = exchange.get_or_create_pool("tbtc4", "signet", AmmPool::DEFAULT_FEE_BPS);
938 pool2
939 .add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
940 .unwrap();
941
942 assert_eq!(exchange.pools.len(), 2);
943
944 let p1 = exchange.get_pool("tbtc4", "tbtc3").unwrap();
946 assert_eq!(p1.reserve_a, 1_000);
947
948 let p2 = exchange.get_pool("tbtc4", "signet").unwrap();
949 assert_eq!(p2.reserve_b, 2_000);
950
951 let p2_alt = exchange.get_pool("signet", "tbtc4").unwrap();
953 assert_eq!(p2_alt.reserve_b, 2_000);
954 }
955
956 #[test]
957 fn test_integer_overflow_safety() {
958 let mut ledger = WebLedger::new("Overflow Test");
959 let large = u64::MAX / 2;
960 ledger.credit_currency("did:nostr:whale", "tbtc4", large);
961 ledger.credit_currency("did:nostr:whale", "tbtc3", large);
962
963 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
964
965 let shares = pool
967 .add_liquidity("did:nostr:whale", large, large, &mut ledger)
968 .unwrap();
969 assert!(shares > 0);
970 assert_eq!(pool.reserve_a, large);
971 assert_eq!(pool.reserve_b, large);
972
973 ledger.credit_currency("did:nostr:trader", "tbtc4", 1_000_000);
975
976 let result = pool
978 .swap("did:nostr:trader", "tbtc4", 1_000_000, &mut ledger)
979 .unwrap();
980 assert!(result.amount_out > 0);
981 assert!(result.amount_out < 1_000_000); let k = (pool.reserve_a as u128) * (pool.reserve_b as u128);
985 let k_original = (large as u128) * (large as u128);
986 assert!(k >= k_original);
987 }
988
989 #[test]
992 fn test_exchange_serialization_roundtrip() {
993 let mut exchange = Exchange::new();
994 exchange
995 .order_book
996 .create_order("did:nostr:alice", "tbtc4", 100, "tbtc3", 2);
997 exchange.get_or_create_pool("tbtc4", "tbtc3", 30);
998
999 let json = serde_json::to_string(&exchange).unwrap();
1000 let parsed: Exchange = serde_json::from_str(&json).unwrap();
1001
1002 assert_eq!(parsed.order_book.list_offers(None).len(), 1);
1003 assert_eq!(parsed.pools.len(), 1);
1004 }
1005
1006 #[test]
1009 fn test_pool_info() {
1010 let mut ledger = setup_ledger();
1011 let mut pool = AmmPool::new("tbtc4", "tbtc3", 30);
1012 pool.add_liquidity("did:nostr:alice", 1_000, 2_000, &mut ledger)
1013 .unwrap();
1014
1015 let info = pool.pool_info();
1016 assert_eq!(info["currency_a"], "tbtc4");
1017 assert_eq!(info["reserve_a"], 1_000);
1018 assert_eq!(info["reserve_b"], 2_000);
1019 assert_eq!(info["fee_bps"], 30);
1020 assert_eq!(info["invariant_k"], 2_000_000u64);
1021 assert_eq!(info["providers"], 1);
1022 }
1023
1024 #[test]
1025 fn untrusted_pool_fees_and_large_arithmetic_fail_without_mutation() {
1026 let mut ledger = setup_ledger();
1027 for fee in [10_000, 10_001, u64::MAX] {
1028 let mut pool = AmmPool::new("tbtc4", "tbtc3", fee);
1029 assert!(pool
1030 .add_liquidity("did:nostr:alice", 1, 1, &mut ledger)
1031 .is_err());
1032 pool.reserve_a = 100;
1033 pool.reserve_b = 100;
1034 assert!(pool
1035 .swap("did:nostr:alice", "tbtc4", 1, &mut ledger)
1036 .is_err());
1037 assert_eq!((pool.reserve_a, pool.reserve_b), (100, 100));
1038 }
1039 let mut pool = AmmPool::new("tbtc4", "tbtc3", 0);
1040 pool.reserve_a = u64::MAX;
1041 pool.reserve_b = u64::MAX;
1042 assert!(pool
1043 .swap("did:nostr:alice", "tbtc4", u64::MAX, &mut ledger)
1044 .is_err());
1045 }
1046
1047 #[test]
1050 fn test_isqrt() {
1051 assert_eq!(isqrt_u128(0), 0);
1052 assert_eq!(isqrt_u128(1), 1);
1053 assert_eq!(isqrt_u128(4), 2);
1054 assert_eq!(isqrt_u128(9), 3);
1055 assert_eq!(isqrt_u128(10), 3);
1056 assert_eq!(isqrt_u128(2_000_000), 1414);
1057 let max = u64::MAX as u128;
1059 assert_eq!(isqrt_u128(max * max), max);
1060 }
1061}