1use std::sync::Arc;
2
3use bsv::auth::utils::create_nonce;
4use bsv::primitives::public_key::PublicKey;
5use bsv::primitives::utils::from_hex;
6use bsv::remittance::types::PeerMessage;
7use bsv::script::templates::{ScriptTemplateLock, P2PKH};
8use bsv::wallet::interfaces::{
9 CreateActionArgs, CreateActionOptions, CreateActionOutput, GetPublicKeyArgs,
10 InternalizeActionArgs, InternalizeOutput, Payment, SignActionArgs, WalletInterface,
11};
12use bsv::wallet::types::{BooleanDefaultTrue, Counterparty, CounterpartyType, Protocol};
13
14use crate::client::MessageBoxClient;
15use crate::error::MessageBoxError;
16use crate::types::{IncomingPayment, PaymentCustomInstructions, PaymentToken};
17
18impl<W: WalletInterface + Clone + 'static + Send + Sync> MessageBoxClient<W> {
19 pub async fn create_payment_token(
29 &self,
30 recipient: &str,
31 amount: u64,
32 ) -> Result<PaymentToken, MessageBoxError> {
33 let prefix = create_nonce(self.wallet())
35 .await
36 .map_err(|e| MessageBoxError::Auth(format!("create_nonce prefix: {e}")))?;
37 let suffix = create_nonce(self.wallet())
38 .await
39 .map_err(|e| MessageBoxError::Auth(format!("create_nonce suffix: {e}")))?;
40
41 let pk_result = self
43 .wallet()
44 .get_public_key(
45 GetPublicKeyArgs {
46 identity_key: false,
47 protocol_id: Some(Protocol {
48 security_level: 2,
49 protocol: "3241645161d8".to_string(),
50 }),
51 key_id: Some(format!("{prefix} {suffix}")),
52 counterparty: Some(Counterparty {
53 counterparty_type: CounterpartyType::Other,
54 public_key: Some(
55 PublicKey::from_string(recipient)
56 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?,
57 ),
58 }),
59 privileged: false,
60 privileged_reason: None,
61 for_self: None,
62 seek_permission: None,
63 },
64 self.originator(),
65 )
66 .await
67 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
68
69 let hash_vec = pk_result.public_key.to_hash();
71 let mut hash = [0u8; 20];
72 hash.copy_from_slice(&hash_vec);
73 let lock_script = P2PKH::from_public_key_hash(hash)
74 .lock()
75 .map_err(|e| MessageBoxError::Wallet(format!("P2PKH lock error: {e}")))?;
76 let locking_script_bytes = from_hex(&lock_script.to_hex())
78 .map_err(|e| MessageBoxError::Wallet(format!("hex decode locking script: {e}")))?;
79
80 let custom_instructions = PaymentCustomInstructions {
82 derivation_prefix: prefix.clone(),
83 derivation_suffix: suffix.clone(),
84 payee: Some(recipient.to_string()),
85 };
86
87 let create_result = self
90 .wallet()
91 .create_action(
92 CreateActionArgs {
93 description: "PeerPay payment".to_string(),
94 input_beef: None,
95 inputs: None,
96 outputs: Some(vec![CreateActionOutput {
97 locking_script: Some(locking_script_bytes),
98 satoshis: amount,
99 output_description: "Payment for PeerPay transaction".to_string(),
100 basket: None,
101 custom_instructions: Some(
102 serde_json::to_string(&custom_instructions)
103 .map_err(MessageBoxError::Json)?,
104 ),
105 tags: None,
106 }]),
107 lock_time: None,
108 version: None,
109 labels: Some(vec!["peerpay".to_string()]),
110 options: Some(CreateActionOptions {
111 randomize_outputs: BooleanDefaultTrue(Some(false)),
112 sign_and_process: BooleanDefaultTrue(None),
113 accept_delayed_broadcast: BooleanDefaultTrue(None),
114 ..Default::default()
115 }),
116 reference: None,
117 },
118 self.originator(),
119 )
120 .await
121 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
122
123 let tx = if let Some(tx_bytes) = create_result.tx {
126 tx_bytes
127 } else if let Some(signable) = create_result.signable_transaction {
128 let sign_result = self
129 .wallet()
130 .sign_action(
131 SignActionArgs {
132 reference: signable.reference,
133 spends: std::collections::HashMap::new(),
134 options: None,
135 },
136 self.originator(),
137 )
138 .await
139 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
140 sign_result
141 .tx
142 .ok_or_else(|| MessageBoxError::Wallet("sign_action returned no tx".to_string()))?
143 } else {
144 return Err(MessageBoxError::Wallet(
145 "create_action returned neither tx nor signable_transaction".to_string(),
146 ));
147 };
148
149 Ok(PaymentToken {
152 custom_instructions,
153 transaction: tx,
154 amount,
155 output_index: None,
156 })
157 }
158
159 pub async fn send_payment(
163 &self,
164 recipient: &str,
165 amount: u64,
166 ) -> Result<String, MessageBoxError> {
167 let token = self.create_payment_token(recipient, amount).await?;
168 let token_json = serde_json::to_string(&token)?;
169 self.send_message(
170 recipient,
171 "payment_inbox",
172 &token_json,
173 false,
174 false,
175 None,
176 None,
177 )
178 .await
179 }
180
181 pub async fn send_live_payment(
191 &self,
192 recipient: &str,
193 amount: u64,
194 ) -> Result<String, MessageBoxError> {
195 let token = self.create_payment_token(recipient, amount).await?;
196 let token_json = serde_json::to_string(&token)?;
197 let delivery = self
198 .send_live_message(
199 recipient,
200 "payment_inbox",
201 &token_json,
202 false,
203 false,
204 None,
205 None,
206 )
207 .await?;
208 Ok(delivery.message_id().to_string())
209 }
210
211 pub async fn listen_for_live_payments(
218 &self,
219 on_payment: Arc<dyn Fn(IncomingPayment) + Send + Sync>,
220 ) -> Result<(), MessageBoxError> {
221 let wrapper: Arc<dyn Fn(PeerMessage) + Send + Sync> = Arc::new(move |msg: PeerMessage| {
222 if let Ok(token) = serde_json::from_str::<PaymentToken>(&msg.body) {
223 let incoming = IncomingPayment {
224 token,
225 sender: msg.sender,
226 message_id: msg.message_id,
227 };
228 on_payment(incoming);
229 }
230 });
232
233 self.listen_for_live_messages("payment_inbox", wrapper, None)
234 .await
235 }
236
237 pub async fn accept_payment(&self, payment: &IncomingPayment) -> Result<(), MessageBoxError> {
243 use base64::{engine::general_purpose::STANDARD, Engine};
244
245 let sender_pk = PublicKey::from_string(&payment.sender)
246 .map_err(|e| MessageBoxError::Wallet(format!("invalid sender key: {e}")))?;
247
248 let prefix_bytes = STANDARD
249 .decode(&payment.token.custom_instructions.derivation_prefix)
250 .map_err(|e| MessageBoxError::Wallet(format!("base64 decode prefix: {e}")))?;
251 let suffix_bytes = STANDARD
252 .decode(&payment.token.custom_instructions.derivation_suffix)
253 .map_err(|e| MessageBoxError::Wallet(format!("base64 decode suffix: {e}")))?;
254
255 self.wallet()
256 .internalize_action(
257 InternalizeActionArgs {
258 tx: payment.token.transaction.clone(),
259 description: "PeerPay Payment".to_string(),
260 labels: Some(vec!["peerpay".to_string()]),
261 seek_permission: BooleanDefaultTrue(Some(true)),
262 outputs: vec![InternalizeOutput::WalletPayment {
263 output_index: payment.token.output_index.unwrap_or(0),
264 payment: Payment {
265 derivation_prefix: prefix_bytes,
266 derivation_suffix: suffix_bytes,
267 sender_identity_key: sender_pk,
268 },
269 }],
270 },
271 self.originator(),
272 )
273 .await
274 .map_err(|e| MessageBoxError::Wallet(e.to_string()))?;
275
276 self.acknowledge_message(vec![payment.message_id.clone()], None)
277 .await?;
278 Ok(())
279 }
280
281 pub async fn reject_payment(&self, payment: &IncomingPayment) -> Result<(), MessageBoxError> {
290 if payment.token.amount < 2000 {
291 return self
292 .acknowledge_message(vec![payment.message_id.clone()], None)
293 .await;
294 }
295
296 self.accept_payment(payment).await?;
297
298 if let Err(e) = self
299 .send_payment(&payment.sender, payment.token.amount - 1000)
300 .await
301 {
302 if Self::is_401_error(&e) {
303 return Ok(());
304 }
305 return Err(e);
306 }
307
308 if let Err(e) = self
309 .acknowledge_message(vec![payment.message_id.clone()], None)
310 .await
311 {
312 if Self::is_401_error(&e) {
313 return Ok(());
314 }
315 return Err(e);
316 }
317
318 Ok(())
319 }
320
321 fn is_401_error(e: &MessageBoxError) -> bool {
323 matches!(e, MessageBoxError::Http(401, _))
324 || matches!(e, MessageBoxError::Auth(msg) if msg.contains("401"))
325 }
326
327 pub async fn list_incoming_payments(&self) -> Result<Vec<IncomingPayment>, MessageBoxError> {
334 let messages = self.list_messages("payment_inbox", false, None).await?;
335
336 let payments = messages
337 .into_iter()
338 .filter_map(|msg| {
339 serde_json::from_str::<PaymentToken>(&msg.body)
340 .ok()
341 .map(|token| IncomingPayment {
342 token,
343 sender: msg.sender,
344 message_id: msg.message_id,
345 })
346 })
347 .collect();
348
349 Ok(payments)
350 }
351
352 pub async fn acknowledge_notification(
360 &self,
361 message: &PeerMessage,
362 ) -> Result<bool, MessageBoxError> {
363 self.acknowledge_message(vec![message.message_id.clone()], None)
365 .await?;
366
367 let parsed = serde_json::from_str::<crate::http_ops::WrappedMessageBody>(&message.body);
369 let payment_data = parsed.ok().and_then(|w| w.payment);
370
371 if let Some(payment) = payment_data {
373 if let (Some(tx_bytes), Some(outputs)) = (&payment.tx, &payment.outputs) {
374 let description = payment
375 .description
376 .clone()
377 .unwrap_or_else(|| "MessageBox recipient payment".to_string());
378
379 let internalize_outputs: Vec<InternalizeOutput> = outputs
381 .iter()
382 .filter_map(|o| {
383 let sender_pk = o.sender_identity_key.as_deref().and_then(|k| {
384 bsv::primitives::public_key::PublicKey::from_string(k).ok()
385 })?;
386 Some(InternalizeOutput::WalletPayment {
387 output_index: o.output_index.unwrap_or(0),
388 payment: Payment {
389 derivation_prefix: o.derivation_prefix.clone().unwrap_or_default(),
390 derivation_suffix: o.derivation_suffix.clone().unwrap_or_default(),
391 sender_identity_key: sender_pk,
392 },
393 })
394 })
395 .collect();
396
397 if internalize_outputs.is_empty() {
398 return Ok(false);
399 }
400
401 let args = InternalizeActionArgs {
402 tx: tx_bytes.clone(),
403 description,
404 labels: Some(vec!["notification-payment".to_string()]),
405 seek_permission: bsv::wallet::types::BooleanDefaultTrue(Some(false)),
406 outputs: internalize_outputs,
407 };
408
409 match self
410 .wallet()
411 .internalize_action(args, self.originator())
412 .await
413 {
414 Ok(_) => return Ok(true),
415 Err(_) => return Ok(false),
416 }
417 }
418 }
419
420 Ok(false)
421 }
422}
423
424#[cfg(test)]
429mod tests {
430 use super::*;
431 use crate::types::{
432 IncomingPayment, PaymentCustomInstructions, PaymentToken, ServerPeerMessage,
433 };
434 use bsv::primitives::private_key::PrivateKey;
435 use bsv::remittance::types::PeerMessage;
436 use bsv::wallet::error::WalletError;
437 use bsv::wallet::interfaces::*;
438 use bsv::wallet::proto_wallet::ProtoWallet;
439 use std::sync::Arc;
440
441 #[derive(Clone)]
443 struct ArcWallet(Arc<ProtoWallet>);
444
445 impl ArcWallet {
446 fn new() -> Self {
447 let key = PrivateKey::from_random().expect("random key");
448 ArcWallet(Arc::new(ProtoWallet::new(key)))
449 }
450
451 async fn identity_hex(&self) -> String {
452 self.get_public_key(
453 GetPublicKeyArgs {
454 identity_key: true,
455 protocol_id: None,
456 key_id: None,
457 counterparty: None,
458 privileged: false,
459 privileged_reason: None,
460 for_self: None,
461 seek_permission: None,
462 },
463 None,
464 )
465 .await
466 .expect("get_public_key")
467 .public_key
468 .to_der_hex()
469 }
470 }
471
472 #[async_trait::async_trait]
473 impl WalletInterface for ArcWallet {
474 async fn create_action(
475 &self,
476 args: CreateActionArgs,
477 orig: Option<&str>,
478 ) -> Result<CreateActionResult, WalletError> {
479 self.0.create_action(args, orig).await
480 }
481 async fn sign_action(
482 &self,
483 args: SignActionArgs,
484 orig: Option<&str>,
485 ) -> Result<SignActionResult, WalletError> {
486 self.0.sign_action(args, orig).await
487 }
488 async fn abort_action(
489 &self,
490 args: AbortActionArgs,
491 orig: Option<&str>,
492 ) -> Result<AbortActionResult, WalletError> {
493 self.0.abort_action(args, orig).await
494 }
495 async fn list_actions(
496 &self,
497 args: ListActionsArgs,
498 orig: Option<&str>,
499 ) -> Result<ListActionsResult, WalletError> {
500 self.0.list_actions(args, orig).await
501 }
502 async fn internalize_action(
503 &self,
504 args: InternalizeActionArgs,
505 orig: Option<&str>,
506 ) -> Result<InternalizeActionResult, WalletError> {
507 self.0.internalize_action(args, orig).await
508 }
509 async fn list_outputs(
510 &self,
511 args: ListOutputsArgs,
512 orig: Option<&str>,
513 ) -> Result<ListOutputsResult, WalletError> {
514 self.0.list_outputs(args, orig).await
515 }
516 async fn relinquish_output(
517 &self,
518 args: RelinquishOutputArgs,
519 orig: Option<&str>,
520 ) -> Result<RelinquishOutputResult, WalletError> {
521 self.0.relinquish_output(args, orig).await
522 }
523 async fn get_public_key(
524 &self,
525 args: GetPublicKeyArgs,
526 orig: Option<&str>,
527 ) -> Result<GetPublicKeyResult, WalletError> {
528 self.0.get_public_key(args, orig).await
529 }
530 async fn reveal_counterparty_key_linkage(
531 &self,
532 args: RevealCounterpartyKeyLinkageArgs,
533 orig: Option<&str>,
534 ) -> Result<RevealCounterpartyKeyLinkageResult, WalletError> {
535 self.0.reveal_counterparty_key_linkage(args, orig).await
536 }
537 async fn reveal_specific_key_linkage(
538 &self,
539 args: RevealSpecificKeyLinkageArgs,
540 orig: Option<&str>,
541 ) -> Result<RevealSpecificKeyLinkageResult, WalletError> {
542 self.0.reveal_specific_key_linkage(args, orig).await
543 }
544 async fn encrypt(
545 &self,
546 args: EncryptArgs,
547 orig: Option<&str>,
548 ) -> Result<EncryptResult, WalletError> {
549 self.0.encrypt(args, orig).await
550 }
551 async fn decrypt(
552 &self,
553 args: DecryptArgs,
554 orig: Option<&str>,
555 ) -> Result<DecryptResult, WalletError> {
556 self.0.decrypt(args, orig).await
557 }
558 async fn create_hmac(
559 &self,
560 args: CreateHmacArgs,
561 orig: Option<&str>,
562 ) -> Result<CreateHmacResult, WalletError> {
563 self.0.create_hmac(args, orig).await
564 }
565 async fn verify_hmac(
566 &self,
567 args: VerifyHmacArgs,
568 orig: Option<&str>,
569 ) -> Result<VerifyHmacResult, WalletError> {
570 self.0.verify_hmac(args, orig).await
571 }
572 async fn create_signature(
573 &self,
574 args: CreateSignatureArgs,
575 orig: Option<&str>,
576 ) -> Result<CreateSignatureResult, WalletError> {
577 self.0.create_signature(args, orig).await
578 }
579 async fn verify_signature(
580 &self,
581 args: VerifySignatureArgs,
582 orig: Option<&str>,
583 ) -> Result<VerifySignatureResult, WalletError> {
584 self.0.verify_signature(args, orig).await
585 }
586 async fn acquire_certificate(
587 &self,
588 args: AcquireCertificateArgs,
589 orig: Option<&str>,
590 ) -> Result<Certificate, WalletError> {
591 self.0.acquire_certificate(args, orig).await
592 }
593 async fn list_certificates(
594 &self,
595 args: ListCertificatesArgs,
596 orig: Option<&str>,
597 ) -> Result<ListCertificatesResult, WalletError> {
598 self.0.list_certificates(args, orig).await
599 }
600 async fn prove_certificate(
601 &self,
602 args: ProveCertificateArgs,
603 orig: Option<&str>,
604 ) -> Result<ProveCertificateResult, WalletError> {
605 self.0.prove_certificate(args, orig).await
606 }
607 async fn relinquish_certificate(
608 &self,
609 args: RelinquishCertificateArgs,
610 orig: Option<&str>,
611 ) -> Result<RelinquishCertificateResult, WalletError> {
612 self.0.relinquish_certificate(args, orig).await
613 }
614 async fn discover_by_identity_key(
615 &self,
616 args: DiscoverByIdentityKeyArgs,
617 orig: Option<&str>,
618 ) -> Result<DiscoverCertificatesResult, WalletError> {
619 self.0.discover_by_identity_key(args, orig).await
620 }
621 async fn discover_by_attributes(
622 &self,
623 args: DiscoverByAttributesArgs,
624 orig: Option<&str>,
625 ) -> Result<DiscoverCertificatesResult, WalletError> {
626 self.0.discover_by_attributes(args, orig).await
627 }
628 async fn is_authenticated(
629 &self,
630 orig: Option<&str>,
631 ) -> Result<AuthenticatedResult, WalletError> {
632 self.0.is_authenticated(orig).await
633 }
634 async fn wait_for_authentication(
635 &self,
636 orig: Option<&str>,
637 ) -> Result<AuthenticatedResult, WalletError> {
638 self.0.wait_for_authentication(orig).await
639 }
640 async fn get_height(&self, orig: Option<&str>) -> Result<GetHeightResult, WalletError> {
641 self.0.get_height(orig).await
642 }
643 async fn get_header_for_height(
644 &self,
645 args: GetHeaderArgs,
646 orig: Option<&str>,
647 ) -> Result<GetHeaderResult, WalletError> {
648 self.0.get_header_for_height(args, orig).await
649 }
650 async fn get_network(&self, orig: Option<&str>) -> Result<GetNetworkResult, WalletError> {
651 self.0.get_network(orig).await
652 }
653 async fn get_version(&self, orig: Option<&str>) -> Result<GetVersionResult, WalletError> {
654 self.0.get_version(orig).await
655 }
656 }
657
658 #[tokio::test]
669 async fn create_payment_token_uses_create_nonce() {
670 let sender = ArcWallet::new();
671 let recipient = ArcWallet::new();
672 let recipient_pk = recipient.identity_hex().await;
673
674 let client = crate::client::MessageBoxClient::new(
675 "https://example.com".to_string(),
676 sender,
677 None,
678 bsv::services::overlay_tools::Network::Mainnet,
679 );
680
681 let result = client.create_payment_token(&recipient_pk, 1000).await;
685 match &result {
689 Err(e) => {
690 let msg = e.to_string();
691 assert!(
693 !msg.contains("create_nonce prefix:"),
694 "should not fail at nonce step"
695 );
696 println!("create_payment_token expected error: {msg}");
698 }
699 Ok(_) => {
700 println!("create_payment_token succeeded unexpectedly — wallet may have funds");
702 }
703 }
704 }
705
706 #[allow(dead_code)]
709 fn send_payment_compile_check(client: &crate::client::MessageBoxClient<ArcWallet>) {
710 drop(client.send_payment("03abc", 1000));
712 }
713
714 #[test]
723 fn reject_payment_threshold_below_2000() {
724 const THRESHOLD: u64 = 2000;
728 assert_eq!(THRESHOLD, 2000, "threshold must be 2000 sats");
729
730 let amount: u64 = 3000;
732 let refund = amount - 1000;
733 assert_eq!(refund, 2000, "refund is amount minus 1000 sat fee");
734 }
735
736 #[test]
740 fn list_incoming_payments_skips_unparseable() {
741 let messages = vec![
744 ServerPeerMessage {
745 message_id: "msg1".to_string(),
746 body: "not valid json".to_string(),
747 sender: "03sender1".to_string(),
748 created_at: "2024-01-01T00:00:00Z".to_string(),
749 updated_at: "2024-01-01T00:00:00Z".to_string(),
750 acknowledged: None,
751 authenticated_decrypt: false,
752 },
753 ServerPeerMessage {
754 message_id: "msg2".to_string(),
755 body: r#"{"customInstructions":{"derivationPrefix":"p","derivationSuffix":"s"},"transaction":[1,2,3],"amount":1000}"#.to_string(),
756 sender: "03sender2".to_string(),
757 created_at: "2024-01-01T00:00:00Z".to_string(),
758 updated_at: "2024-01-01T00:00:00Z".to_string(),
759 acknowledged: None,
760 authenticated_decrypt: false,
761 },
762 ServerPeerMessage {
763 message_id: "msg3".to_string(),
764 body: r#"{"foo":"bar"}"#.to_string(),
765 sender: "03sender3".to_string(),
766 created_at: "2024-01-01T00:00:00Z".to_string(),
767 updated_at: "2024-01-01T00:00:00Z".to_string(),
768 acknowledged: None,
769 authenticated_decrypt: false,
770 },
771 ];
772
773 let payments: Vec<IncomingPayment> = messages
775 .into_iter()
776 .filter_map(|msg| {
777 serde_json::from_str::<PaymentToken>(&msg.body)
778 .ok()
779 .map(|token| IncomingPayment {
780 token,
781 sender: msg.sender,
782 message_id: msg.message_id,
783 })
784 })
785 .collect();
786
787 assert_eq!(
789 payments.len(),
790 1,
791 "only valid payment token should be included"
792 );
793 assert_eq!(payments[0].message_id, "msg2");
794 assert_eq!(payments[0].sender, "03sender2");
795 assert_eq!(payments[0].token.amount, 1000);
796 }
797
798 #[test]
802 fn accept_payment_base64_round_trip() {
803 use base64::{engine::general_purpose::STANDARD, Engine};
804
805 let prefix = "dGVzdC1wcmVmaXg="; let suffix = "dGVzdC1zdWZmaXg="; let prefix_bytes = STANDARD.decode(prefix).unwrap();
811 let suffix_bytes = STANDARD.decode(suffix).unwrap();
812 assert_eq!(prefix_bytes, b"test-prefix");
813 assert_eq!(suffix_bytes, b"test-suffix");
814
815 let re_encoded = STANDARD.encode(&prefix_bytes);
817 assert_eq!(
818 re_encoded, prefix,
819 "round-trip must produce original base64"
820 );
821 }
822
823 #[test]
825 fn incoming_payment_round_trip() {
826 let token = PaymentToken {
827 custom_instructions: PaymentCustomInstructions {
828 derivation_prefix: "pfx".to_string(),
829 derivation_suffix: "sfx".to_string(),
830 payee: Some("03recipient".to_string()),
831 },
832 transaction: vec![0xde, 0xad, 0xbe, 0xef],
833 amount: 5000,
834 output_index: None,
835 };
836
837 let incoming = IncomingPayment {
838 token: token.clone(),
839 sender: "03sender_key".to_string(),
840 message_id: "abc123".to_string(),
841 };
842
843 assert_eq!(incoming.sender, "03sender_key");
844 assert_eq!(incoming.message_id, "abc123");
845 assert_eq!(incoming.token.amount, 5000);
846 assert_eq!(incoming.token.transaction, vec![0xde, 0xad, 0xbe, 0xef]);
847 assert_eq!(incoming.token.custom_instructions.derivation_prefix, "pfx");
848 assert_eq!(incoming.token.custom_instructions.derivation_suffix, "sfx");
849 assert_eq!(incoming.token.output_index, None);
850 }
851
852 #[allow(dead_code)]
858 fn send_live_payment_compile_check(client: &crate::client::MessageBoxClient<ArcWallet>) {
859 let _fut = client.send_live_payment("03abc", 1000);
860 }
861
862 #[test]
866 fn listen_for_live_payments_callback_parses_token() {
867 use std::sync::Mutex as StdMutex;
868
869 let received = Arc::new(StdMutex::new(Vec::<IncomingPayment>::new()));
870 let received_clone = received.clone();
871
872 let msg = PeerMessage {
874 message_id: "msg-live-1".to_string(),
875 sender: "03sender".to_string(),
876 recipient: "03recipient".to_string(),
877 message_box: "payment_inbox".to_string(),
878 body: r#"{"customInstructions":{"derivationPrefix":"p","derivationSuffix":"s"},"transaction":[1,2],"amount":500}"#.to_string(),
879 };
880
881 if let Ok(token) = serde_json::from_str::<PaymentToken>(&msg.body) {
883 let incoming = IncomingPayment {
884 token,
885 sender: msg.sender.clone(),
886 message_id: msg.message_id.clone(),
887 };
888 received_clone.lock().unwrap().push(incoming);
889 }
890
891 let payments = received.lock().unwrap();
892 assert_eq!(payments.len(), 1, "one valid payment should be parsed");
893 assert_eq!(payments[0].token.amount, 500);
894 assert_eq!(payments[0].sender, "03sender");
895 assert_eq!(payments[0].message_id, "msg-live-1");
896 }
897
898 #[test]
902 fn listen_for_live_payments_callback_skips_non_payment() {
903 use std::sync::Mutex as StdMutex;
904
905 let received = Arc::new(StdMutex::new(Vec::<IncomingPayment>::new()));
906 let received_clone = received.clone();
907
908 let msg = PeerMessage {
910 message_id: "msg-bad-1".to_string(),
911 sender: "03sender".to_string(),
912 recipient: "03recipient".to_string(),
913 message_box: "payment_inbox".to_string(),
914 body: r#"{"not":"a payment token"}"#.to_string(),
915 };
916
917 if let Ok(token) = serde_json::from_str::<PaymentToken>(&msg.body) {
919 let incoming = IncomingPayment {
920 token,
921 sender: msg.sender.clone(),
922 message_id: msg.message_id.clone(),
923 };
924 received_clone.lock().unwrap().push(incoming);
925 }
926
927 let payments = received.lock().unwrap();
928 assert_eq!(
929 payments.len(),
930 0,
931 "non-payment message must be silently skipped"
932 );
933 }
934}