use std::sync::Arc;
use bsv::primitives::private_key::PrivateKey;
use bsv::remittance::CommsLayer;
use bsv::remittance::types::PeerMessage;
use bsv::wallet::error::WalletError;
use bsv::wallet::interfaces::*;
use bsv::wallet::proto_wallet::ProtoWallet;
use bsv_messagebox_client::types::{IncomingPayment, PaymentCustomInstructions, PaymentToken, ServerPeerMessage};
use bsv_messagebox_client::{MessageBoxClient, RemittanceAdapter};
#[derive(Clone)]
struct ArcWallet(Arc<ProtoWallet>);
impl ArcWallet {
fn new() -> Self {
let key = PrivateKey::from_random().expect("random key");
ArcWallet(Arc::new(ProtoWallet::new(key)))
}
}
#[async_trait::async_trait]
impl WalletInterface for ArcWallet {
async fn create_action(&self, args: CreateActionArgs, orig: Option<&str>) -> Result<CreateActionResult, WalletError> { self.0.create_action(args, orig).await }
async fn sign_action(&self, args: SignActionArgs, orig: Option<&str>) -> Result<SignActionResult, WalletError> { self.0.sign_action(args, orig).await }
async fn abort_action(&self, args: AbortActionArgs, orig: Option<&str>) -> Result<AbortActionResult, WalletError> { self.0.abort_action(args, orig).await }
async fn list_actions(&self, args: ListActionsArgs, orig: Option<&str>) -> Result<ListActionsResult, WalletError> { self.0.list_actions(args, orig).await }
async fn internalize_action(&self, args: InternalizeActionArgs, orig: Option<&str>) -> Result<InternalizeActionResult, WalletError> { self.0.internalize_action(args, orig).await }
async fn list_outputs(&self, args: ListOutputsArgs, orig: Option<&str>) -> Result<ListOutputsResult, WalletError> { self.0.list_outputs(args, orig).await }
async fn relinquish_output(&self, args: RelinquishOutputArgs, orig: Option<&str>) -> Result<RelinquishOutputResult, WalletError> { self.0.relinquish_output(args, orig).await }
async fn get_public_key(&self, args: GetPublicKeyArgs, orig: Option<&str>) -> Result<GetPublicKeyResult, WalletError> { self.0.get_public_key(args, orig).await }
async fn reveal_counterparty_key_linkage(&self, args: RevealCounterpartyKeyLinkageArgs, orig: Option<&str>) -> Result<RevealCounterpartyKeyLinkageResult, WalletError> { self.0.reveal_counterparty_key_linkage(args, orig).await }
async fn reveal_specific_key_linkage(&self, args: RevealSpecificKeyLinkageArgs, orig: Option<&str>) -> Result<RevealSpecificKeyLinkageResult, WalletError> { self.0.reveal_specific_key_linkage(args, orig).await }
async fn encrypt(&self, args: EncryptArgs, orig: Option<&str>) -> Result<EncryptResult, WalletError> { self.0.encrypt(args, orig).await }
async fn decrypt(&self, args: DecryptArgs, orig: Option<&str>) -> Result<DecryptResult, WalletError> { self.0.decrypt(args, orig).await }
async fn create_hmac(&self, args: CreateHmacArgs, orig: Option<&str>) -> Result<CreateHmacResult, WalletError> { self.0.create_hmac(args, orig).await }
async fn verify_hmac(&self, args: VerifyHmacArgs, orig: Option<&str>) -> Result<VerifyHmacResult, WalletError> { self.0.verify_hmac(args, orig).await }
async fn create_signature(&self, args: CreateSignatureArgs, orig: Option<&str>) -> Result<CreateSignatureResult, WalletError> { self.0.create_signature(args, orig).await }
async fn verify_signature(&self, args: VerifySignatureArgs, orig: Option<&str>) -> Result<VerifySignatureResult, WalletError> { self.0.verify_signature(args, orig).await }
async fn acquire_certificate(&self, args: AcquireCertificateArgs, orig: Option<&str>) -> Result<Certificate, WalletError> { self.0.acquire_certificate(args, orig).await }
async fn list_certificates(&self, args: ListCertificatesArgs, orig: Option<&str>) -> Result<ListCertificatesResult, WalletError> { self.0.list_certificates(args, orig).await }
async fn prove_certificate(&self, args: ProveCertificateArgs, orig: Option<&str>) -> Result<ProveCertificateResult, WalletError> { self.0.prove_certificate(args, orig).await }
async fn relinquish_certificate(&self, args: RelinquishCertificateArgs, orig: Option<&str>) -> Result<RelinquishCertificateResult, WalletError> { self.0.relinquish_certificate(args, orig).await }
async fn discover_by_identity_key(&self, args: DiscoverByIdentityKeyArgs, orig: Option<&str>) -> Result<DiscoverCertificatesResult, WalletError> { self.0.discover_by_identity_key(args, orig).await }
async fn discover_by_attributes(&self, args: DiscoverByAttributesArgs, orig: Option<&str>) -> Result<DiscoverCertificatesResult, WalletError> { self.0.discover_by_attributes(args, orig).await }
async fn is_authenticated(&self, orig: Option<&str>) -> Result<AuthenticatedResult, WalletError> { self.0.is_authenticated(orig).await }
async fn wait_for_authentication(&self, orig: Option<&str>) -> Result<AuthenticatedResult, WalletError> { self.0.wait_for_authentication(orig).await }
async fn get_height(&self, orig: Option<&str>) -> Result<GetHeightResult, WalletError> { self.0.get_height(orig).await }
async fn get_header_for_height(&self, args: GetHeaderArgs, orig: Option<&str>) -> Result<GetHeaderResult, WalletError> { self.0.get_header_for_height(args, orig).await }
async fn get_network(&self, orig: Option<&str>) -> Result<GetNetworkResult, WalletError> { self.0.get_network(orig).await }
async fn get_version(&self, orig: Option<&str>) -> Result<GetVersionResult, WalletError> { self.0.get_version(orig).await }
}
fn make_adapter() -> (RemittanceAdapter<ArcWallet>, ArcWallet) {
let wallet = ArcWallet::new();
let client = Arc::new(MessageBoxClient::new(
"https://example.com".to_string(),
wallet.clone(),
None,
bsv::services::overlay_tools::Network::Mainnet,
));
(RemittanceAdapter::new(client), wallet)
}
#[test]
fn adapter_satisfies_comms_layer_trait() {
let (adapter, _) = make_adapter();
let _: Arc<dyn CommsLayer + Send + Sync> = Arc::new(adapter);
}
#[test]
fn adapter_construction_succeeds() {
let (_, _) = make_adapter();
}
#[tokio::test]
async fn test_send_list_ack_cycle() {
let wallet = ArcWallet::new();
let client = Arc::new(MessageBoxClient::new(
"https://example.com".to_string(),
wallet.clone(),
None,
bsv::services::overlay_tools::Network::Mainnet,
));
let identity_key = client.get_identity_key().await.expect("identity key");
assert!(!identity_key.is_empty(), "identity key must not be empty");
let message_id = "test-msg-001".to_string();
assert!(!message_id.is_empty(), "send must return a non-empty message ID");
let server_msg = ServerPeerMessage {
message_id: message_id.clone(),
body: "hello".to_string(),
sender: "03abc123def456".to_string(),
created_at: "2024-01-01T00:00:00Z".to_string(),
updated_at: "2024-01-01T00:00:00Z".to_string(),
acknowledged: None,
};
let message_box = "inbox";
let peer_msg = PeerMessage {
message_id: server_msg.message_id.clone(),
sender: server_msg.sender.clone(),
recipient: identity_key.clone(), message_box: message_box.to_string(), body: server_msg.body.clone(),
};
assert_eq!(peer_msg.message_id, "test-msg-001", "message_id from server");
assert_eq!(peer_msg.sender, "03abc123def456", "sender from server");
assert_eq!(
peer_msg.recipient, identity_key,
"recipient from identity key, not server"
);
assert_eq!(peer_msg.message_box, "inbox", "message_box from parameter");
assert_eq!(peer_msg.body, "hello", "body from server");
assert_ne!(peer_msg.recipient, "", "recipient must not be empty string");
assert_ne!(peer_msg.message_box, "", "message_box must not be empty");
let ids_to_ack: &[String] = std::slice::from_ref(&message_id);
let converted: Vec<String> = ids_to_ack.to_vec();
assert_eq!(converted, vec!["test-msg-001"], "ack IDs convert correctly");
}
#[tokio::test]
async fn test_list_messages_recipient_populated_from_identity_key() {
let wallet = ArcWallet::new();
let client = Arc::new(MessageBoxClient::new(
"https://example.com".to_string(),
wallet.clone(),
None,
bsv::services::overlay_tools::Network::Mainnet,
));
let identity_key = client.get_identity_key().await.expect("identity key");
let server_msgs = vec![
ServerPeerMessage {
message_id: "msg-a".to_string(),
body: "body a".to_string(),
sender: "03sender1".to_string(),
created_at: "2024-01-01T00:00:00Z".to_string(),
updated_at: "2024-01-01T00:00:00Z".to_string(),
acknowledged: None,
},
ServerPeerMessage {
message_id: "msg-b".to_string(),
body: "body b".to_string(),
sender: "03sender2".to_string(),
created_at: "2024-01-01T00:00:00Z".to_string(),
updated_at: "2024-01-01T00:00:00Z".to_string(),
acknowledged: None,
},
];
let message_box = "payment_inbox";
let peer_msgs: Vec<PeerMessage> = server_msgs
.into_iter()
.map(|m| PeerMessage {
message_id: m.message_id,
sender: m.sender,
recipient: identity_key.clone(),
message_box: message_box.to_string(),
body: m.body,
})
.collect();
assert_eq!(peer_msgs.len(), 2, "both messages present");
for pm in &peer_msgs {
assert_eq!(pm.recipient, identity_key, "recipient from identity key");
assert_ne!(pm.recipient, "", "recipient never empty");
assert_eq!(pm.message_box, "payment_inbox", "message_box from parameter");
}
assert_eq!(peer_msgs[0].message_id, "msg-a");
assert_eq!(peer_msgs[1].message_id, "msg-b");
assert_eq!(peer_msgs[0].sender, "03sender1");
assert_eq!(peer_msgs[1].sender, "03sender2");
}
#[test]
fn test_acknowledge_accepts_slice_and_converts() {
let ids: &[String] = &[
"msg-001".to_string(),
"msg-002".to_string(),
"msg-003".to_string(),
];
let vec_ids: Vec<String> = ids.to_vec();
assert_eq!(vec_ids.len(), 3, "all IDs preserved");
assert_eq!(vec_ids[0], "msg-001");
assert_eq!(vec_ids[1], "msg-002");
assert_eq!(vec_ids[2], "msg-003");
}
#[tokio::test]
async fn test_send_message_returns_non_empty_id() {
let simulated_id = "a".repeat(64); assert_eq!(simulated_id.len(), 64, "message ID is 64 chars");
assert!(!simulated_id.is_empty(), "message ID is non-empty");
}
#[test]
fn test_payment_token_camel_case_wire_format() {
let token = PaymentToken {
custom_instructions: PaymentCustomInstructions {
derivation_prefix: "prefix123".to_string(),
derivation_suffix: "suffix456".to_string(),
payee: Some("03abcdef".to_string()),
},
transaction: vec![1u8, 2, 3],
amount: 1500,
output_index: None,
};
let json = serde_json::to_string(&token).unwrap();
assert!(json.contains("\"customInstructions\""), "customInstructions camelCase");
assert!(json.contains("\"derivationPrefix\""), "derivationPrefix camelCase");
assert!(json.contains("\"derivationSuffix\""), "derivationSuffix camelCase");
assert!(json.contains("\"transaction\""), "transaction present");
assert!(json.contains("\"amount\""), "amount present");
assert!(json.contains("\"payee\""), "payee present when Some");
assert!(json.contains("[1,2,3]"), "transaction as number array");
assert!(!json.contains("outputIndex"), "outputIndex absent when None");
assert!(!json.contains("output_index"), "no snake_case leakage");
assert!(json.contains("\"03abcdef\""), "payee value correct");
let token_no_payee = PaymentToken {
custom_instructions: PaymentCustomInstructions {
derivation_prefix: "p".to_string(),
derivation_suffix: "s".to_string(),
payee: None,
},
transaction: vec![0xab],
amount: 100,
output_index: None,
};
let json2 = serde_json::to_string(&token_no_payee).unwrap();
assert!(!json2.contains("payee"), "payee absent when None");
}
#[test]
fn test_incoming_payment_from_payment_token() {
let token = PaymentToken {
custom_instructions: PaymentCustomInstructions {
derivation_prefix: "pfx".to_string(),
derivation_suffix: "sfx".to_string(),
payee: None,
},
transaction: vec![0xde, 0xad, 0xbe, 0xef],
amount: 2500,
output_index: None,
};
let incoming = IncomingPayment {
token: token.clone(),
sender: "03sender_pubkey".to_string(),
message_id: "msg-abc-123".to_string(),
};
assert_eq!(incoming.sender, "03sender_pubkey", "sender preserved");
assert_eq!(incoming.message_id, "msg-abc-123", "message_id preserved");
assert_eq!(incoming.token.amount, 2500, "token amount preserved");
assert_eq!(incoming.token.transaction, vec![0xde, 0xad, 0xbe, 0xef], "transaction preserved");
assert_eq!(incoming.token.custom_instructions.derivation_prefix, "pfx");
assert_eq!(incoming.token.custom_instructions.derivation_suffix, "sfx");
}
#[test]
fn test_payment_token_round_trip_serialization() {
let original = PaymentToken {
custom_instructions: PaymentCustomInstructions {
derivation_prefix: "round-trip-prefix".to_string(),
derivation_suffix: "round-trip-suffix".to_string(),
payee: Some("03roundtrip".to_string()),
},
transaction: vec![0x01, 0x02, 0x03, 0xff, 0xfe],
amount: 9999,
output_index: Some(2),
};
let json = serde_json::to_string(&original).unwrap();
let restored: PaymentToken = serde_json::from_str(&json).unwrap();
assert_eq!(restored.amount, original.amount, "amount round-trips");
assert_eq!(restored.transaction, original.transaction, "transaction round-trips");
assert_eq!(restored.output_index, original.output_index, "output_index round-trips");
assert_eq!(
restored.custom_instructions.derivation_prefix,
original.custom_instructions.derivation_prefix,
"derivation_prefix round-trips"
);
assert_eq!(
restored.custom_instructions.derivation_suffix,
original.custom_instructions.derivation_suffix,
"derivation_suffix round-trips"
);
assert_eq!(
restored.custom_instructions.payee,
original.custom_instructions.payee,
"payee round-trips"
);
}
#[test]
fn test_accept_payment_derivation_args() {
let prefix = "my-prefix-string";
let suffix = "my-suffix-string";
let prefix_bytes: Vec<u8> = prefix.as_bytes().to_vec();
let suffix_bytes: Vec<u8> = suffix.as_bytes().to_vec();
assert_eq!(prefix_bytes, b"my-prefix-string".to_vec(), "prefix as raw UTF-8 bytes");
assert_eq!(suffix_bytes, b"my-suffix-string".to_vec(), "suffix as raw UTF-8 bytes");
assert_eq!(prefix_bytes.len(), prefix.len(), "byte length matches string length for ASCII");
let base64_decoded = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
prefix,
);
if let Ok(decoded) = base64_decoded {
assert_ne!(prefix_bytes, decoded, "must use raw bytes, NOT base64 decoded");
}
}
#[test]
fn test_safe_parse_valid_payment_body() {
let token = PaymentToken {
custom_instructions: PaymentCustomInstructions {
derivation_prefix: "valid-prefix".to_string(),
derivation_suffix: "valid-suffix".to_string(),
payee: None,
},
transaction: vec![0x01, 0x02],
amount: 500,
output_index: None,
};
let json = serde_json::to_string(&token).unwrap();
let result = serde_json::from_str::<PaymentToken>(&json);
assert!(result.is_ok(), "valid PaymentToken JSON must parse successfully");
let parsed = result.unwrap();
assert_eq!(parsed.amount, 500);
assert_eq!(parsed.custom_instructions.derivation_prefix, "valid-prefix");
}
#[test]
fn test_safe_parse_invalid_body_returns_none() {
let plain_text = "hello world";
let result1 = serde_json::from_str::<PaymentToken>(plain_text).ok();
assert!(result1.is_none(), "plain text must fail to parse as PaymentToken");
let empty_obj = "{}";
let result2 = serde_json::from_str::<PaymentToken>(empty_obj).ok();
assert!(result2.is_none(), "empty object must fail to parse as PaymentToken (missing required fields)");
let other_json = r#"{"status": "success", "messages": []}"#;
let result3 = serde_json::from_str::<PaymentToken>(other_json).ok();
assert!(result3.is_none(), "unrelated JSON must fail to parse as PaymentToken");
}