use std::sync::Arc;
use bsv::primitives::private_key::PrivateKey;
use bsv::services::overlay_tools::Network;
use bsv::wallet::error::WalletError;
use bsv::wallet::interfaces::*;
use bsv::wallet::proto_wallet::ProtoWallet;
use bsv::wallet::substrates::http_wallet_json::HttpWalletJson;
use bsv_messagebox_client::MessageBoxClient;
const LIVE_HOST: &str = "https://messagebox.babbage.systems";
#[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_live_client() -> Arc<MessageBoxClient<ArcWallet>> {
let wallet = ArcWallet::new();
Arc::new(MessageBoxClient::new(
LIVE_HOST.to_string(),
wallet,
None,
Network::Mainnet,
))
}
#[derive(Clone)]
struct ArcHttpWallet(Arc<HttpWalletJson>);
impl ArcHttpWallet {
fn new() -> Self {
ArcHttpWallet(Arc::new(HttpWalletJson::new("rust-messagebox-tests", "")))
}
}
#[async_trait::async_trait]
impl WalletInterface for ArcHttpWallet {
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_funded_client() -> Arc<MessageBoxClient<ArcHttpWallet>> {
let wallet = ArcHttpWallet::new();
Arc::new(MessageBoxClient::new(
LIVE_HOST.to_string(),
wallet,
None,
Network::Mainnet,
))
}
#[tokio::test]
#[ignore]
async fn test_init_against_live_server() {
let client = make_live_client();
let result = client.init(None).await;
println!("init result: {result:?}");
assert!(result.is_ok(), "init() should not return an error: {result:?}");
}
#[tokio::test]
#[ignore]
async fn test_get_identity_key_live() {
let client = make_live_client();
let key = client.get_identity_key().await.expect("get_identity_key should succeed");
println!("identity key: {key}");
assert_eq!(key.len(), 66, "compressed pubkey is 66 hex chars, got {}", key.len());
assert!(
key.chars().all(|c| c.is_ascii_hexdigit()),
"identity key must be all hex digits"
);
assert!(
key.starts_with("02") || key.starts_with("03"),
"compressed pubkey must start with 02 or 03, got prefix: {}",
&key[..2]
);
}
#[tokio::test]
#[ignore]
async fn test_list_messages_empty_inbox() {
let client = make_live_client();
let result = client.list_messages("test_inbox", false, None).await;
println!("list_messages result: {result:?}");
match result {
Ok(messages) => {
println!("got {} messages (expected 0)", messages.len());
assert!(messages.is_empty(), "fresh wallet should have no messages");
}
Err(e) => {
println!("list_messages returned error (acceptable for fresh wallet): {e}");
}
}
}
#[tokio::test]
#[ignore]
async fn test_send_message_to_self() {
let client = make_live_client();
let identity_key = client
.get_identity_key()
.await
.expect("get_identity_key should succeed");
let result = client
.send_message(
&identity_key,
"test_inbox",
"hello from rust live test",
false, false, None, None, )
.await;
println!("send_message result: {result:?}");
match result {
Ok(msg_id) => {
println!("message ID: {msg_id}");
assert!(!msg_id.is_empty(), "message ID must be non-empty");
}
Err(e) => {
panic!("send_message failed: {e}");
}
}
}
#[tokio::test]
#[ignore]
async fn test_list_and_acknowledge() {
let client = make_live_client();
let identity_key = client
.get_identity_key()
.await
.expect("get_identity_key should succeed");
let send_result = client
.send_message(
&identity_key,
"test_inbox",
"ack test message from rust",
false,
false,
None,
None,
)
.await;
println!("send_message result: {send_result:?}");
let list_result = client.list_messages("test_inbox", false, None).await;
println!("list_messages result: {list_result:?}");
match list_result {
Ok(messages) => {
println!("found {} messages", messages.len());
if !messages.is_empty() {
let ids: Vec<String> = messages.iter().map(|m| m.message_id.clone()).collect();
println!("acknowledging message IDs: {ids:?}");
let ack_result = client.acknowledge_message(ids, None).await;
println!("acknowledge result: {ack_result:?}");
assert!(ack_result.is_ok(), "acknowledge should succeed: {ack_result:?}");
} else {
println!("no messages to acknowledge (send may not have completed yet)");
}
}
Err(e) => {
println!("list_messages failed (acceptable): {e}");
}
}
}
#[tokio::test]
#[ignore]
async fn test_permissions_list() {
let client = make_live_client();
let result = client
.list_message_box_permissions(None, None, None, None)
.await;
println!("list_message_box_permissions result: {result:?}");
match result {
Ok(perms) => {
println!("got {} permissions", perms.len());
}
Err(e) => {
println!("list_message_box_permissions error (acceptable): {e}");
}
}
}
#[tokio::test]
#[ignore]
async fn test_send_message_encrypted() {
let client = make_live_client();
let identity_key = client.get_identity_key().await.expect("get_identity_key");
let result = client
.send_message(
&identity_key,
"test_inbox",
"encrypted live test payload",
false, false,
None,
None,
)
.await;
println!("send_message (encrypted) result: {result:?}");
let msg_id = result.expect("send_message should succeed");
assert!(!msg_id.is_empty(), "message_id must be non-empty");
}
#[tokio::test]
#[ignore]
async fn test_send_message_unencrypted() {
let client = make_live_client();
let identity_key = client.get_identity_key().await.expect("get_identity_key");
let result = client
.send_message(
&identity_key,
"test_inbox",
"unencrypted live test payload",
true, false,
None,
None,
)
.await;
println!("send_message (unencrypted) result: {result:?}");
let msg_id = result.expect("send_message with skip_encryption should succeed");
assert!(!msg_id.is_empty(), "message_id must be non-empty");
}
#[tokio::test]
#[ignore]
async fn test_send_message_with_custom_id() {
let client = make_live_client();
let identity_key = client.get_identity_key().await.expect("get_identity_key");
let custom_id = format!("custom-id-{}", uuid_like_suffix());
let result = client
.send_message(
&identity_key,
"test_inbox",
"message with custom id",
true,
false,
Some(&custom_id),
None,
)
.await;
println!("send_message (custom id) result: {result:?}");
let msg_id = result.expect("send_message with custom id should succeed");
assert!(!msg_id.is_empty(), "message_id must be non-empty");
}
#[tokio::test]
#[ignore]
async fn test_list_messages_lite() {
let client = make_live_client();
let result = client.list_messages_lite("test_inbox", None).await;
println!("list_messages_lite result: {result:?}");
match result {
Ok(messages) => {
println!("list_messages_lite returned {} messages", messages.len());
}
Err(e) => {
println!("list_messages_lite error (acceptable): {e}");
}
}
}
#[tokio::test]
#[ignore]
async fn test_list_messages_with_override_host() {
let client = make_live_client();
let result = client
.list_messages("test_inbox", false, Some(LIVE_HOST))
.await;
println!("list_messages (override_host) result: {result:?}");
match result {
Ok(messages) => {
println!("got {} messages with override_host", messages.len());
}
Err(e) => {
println!("list_messages with override_host error (acceptable): {e}");
}
}
}
#[tokio::test]
#[ignore]
async fn test_send_list_ack_full_cycle() {
let client = make_live_client();
let identity_key = client.get_identity_key().await.expect("get_identity_key");
let unique_body = format!("full-cycle-test-{}", uuid_like_suffix());
let sent_id = client
.send_message(
&identity_key,
"test_inbox",
&unique_body,
true,
false,
None,
None,
)
.await
.expect("send should succeed");
println!("sent message id: {sent_id}");
let messages = client
.list_messages("test_inbox", false, None)
.await
.expect("list should succeed");
println!("listed {} messages", messages.len());
assert!(
messages.iter().any(|m| m.message_id == sent_id),
"sent message must appear in list"
);
client
.acknowledge_message(vec![sent_id.clone()], None)
.await
.expect("acknowledge should succeed");
println!("acknowledged message {sent_id}");
let messages_after = client
.list_messages("test_inbox", false, None)
.await
.expect("second list should succeed");
assert!(
!messages_after.iter().any(|m| m.message_id == sent_id),
"acknowledged message must not appear in second list"
);
}
#[tokio::test]
#[ignore]
async fn test_acknowledge_nonexistent() {
let client = make_live_client();
let result = client
.acknowledge_message(vec!["nonexistent-fake-id-999".to_string()], None)
.await;
println!("acknowledge nonexistent result: {result:?}");
}
#[tokio::test]
#[ignore]
async fn test_set_and_get_permission() {
use bsv_messagebox_client::types::SetPermissionParams;
let client = make_live_client();
let identity_key = client.get_identity_key().await.expect("get_identity_key");
let set_result = client
.set_message_box_permission(
SetPermissionParams {
message_box: "test_inbox".to_string(),
sender: None,
recipient_fee: 0,
},
None,
)
.await;
println!("set_permission result: {set_result:?}");
set_result.expect("set_permission should succeed");
let get_result = client
.get_message_box_permission(&identity_key, "test_inbox", None, None)
.await;
println!("get_permission result: {get_result:?}");
match get_result {
Ok(Some(perm)) => {
assert_eq!(perm.message_box, "test_inbox");
assert_eq!(perm.recipient_fee, 0);
println!("permission fields match: message_box={}, fee={}", perm.message_box, perm.recipient_fee);
}
Ok(None) => {
println!("permission returned None — server may not echo it back for self");
}
Err(e) => {
panic!("get_permission failed: {e}");
}
}
}
#[tokio::test]
#[ignore]
async fn test_list_permissions_filtered() {
let client = make_live_client();
let result = client
.list_message_box_permissions(Some("test_inbox"), None, None, None)
.await;
println!("list_permissions (filtered) result: {result:?}");
match result {
Ok(perms) => {
println!("got {} permissions for test_inbox", perms.len());
}
Err(e) => {
println!("list_permissions filtered error (acceptable): {e}");
}
}
}
#[tokio::test]
#[ignore]
async fn test_get_quote() {
let client = make_live_client();
let identity_key = client.get_identity_key().await.expect("get_identity_key");
let result = client
.get_message_box_quote(&identity_key, "test_inbox", None)
.await;
println!("get_quote result: {result:?}");
match result {
Ok(quote) => {
println!(
"quote: delivery_fee={}, recipient_fee={}, agent={}",
quote.delivery_fee, quote.recipient_fee, quote.delivery_agent_identity_key
);
assert!(
!quote.delivery_agent_identity_key.is_empty(),
"delivery_agent_identity_key must be non-empty"
);
}
Err(e) => {
println!("get_quote error (acceptable): {e}");
}
}
}
#[tokio::test]
#[ignore]
async fn test_deny_and_check_notification() {
let client = make_live_client();
let fake_peer = "0200000000000000000000000000000000000000000000000000000000000000ff";
let deny_result = client
.deny_notifications_from_peer(fake_peer, None)
.await;
println!("deny_notifications result: {deny_result:?}");
let check_result = client
.check_peer_notification_status(fake_peer, None)
.await;
println!("check_peer_notification_status result: {check_result:?}");
}
#[tokio::test]
#[ignore]
async fn test_create_payment_token() {
let client = make_live_client();
let identity_key = client.get_identity_key().await.expect("get_identity_key");
let result = client.create_payment_token(&identity_key, 1000).await;
println!("create_payment_token result: {result:?}");
match result {
Ok(token) => {
assert_eq!(token.amount, 1000);
assert!(!token.transaction.is_empty(), "transaction bytes must be non-empty");
assert!(!token.custom_instructions.derivation_prefix.is_empty());
assert!(!token.custom_instructions.derivation_suffix.is_empty());
println!("token created: amount={}, tx_len={}", token.amount, token.transaction.len());
}
Err(e) => {
println!("create_payment_token error (expected for unfunded wallet): {e}");
}
}
}
#[tokio::test]
#[ignore]
async fn test_send_payment_to_self() {
let client = make_live_client();
let identity_key = client.get_identity_key().await.expect("get_identity_key");
let result = client.send_payment(&identity_key, 1000).await;
println!("send_payment result: {result:?}");
match result {
Ok(msg_id) => {
assert!(!msg_id.is_empty(), "message_id must be non-empty");
println!("payment sent, message_id={msg_id}");
}
Err(e) => {
println!("send_payment error (expected for unfunded wallet): {e}");
}
}
}
#[tokio::test]
#[ignore]
async fn test_list_incoming_payments() {
let client = make_live_client();
let identity_key = client.get_identity_key().await.expect("get_identity_key");
let _ = client.send_payment(&identity_key, 1000).await;
let result = client.list_incoming_payments().await;
println!("list_incoming_payments result: {result:?}");
match result {
Ok(payments) => {
println!("got {} incoming payments", payments.len());
}
Err(e) => {
println!("list_incoming_payments error (acceptable): {e}");
}
}
}
#[tokio::test]
#[ignore]
async fn test_init_idempotent() {
let client = make_live_client();
let r1 = client.init(None).await;
println!("init(None) first call: {r1:?}");
assert!(r1.is_ok(), "first init should succeed: {r1:?}");
let r2 = client.init(None).await;
println!("init(None) second call: {r2:?}");
assert!(r2.is_ok(), "second init should succeed: {r2:?}");
}
#[tokio::test]
#[ignore]
async fn test_init_with_target_host() {
let client = make_live_client();
let result = client.init(Some(LIVE_HOST)).await;
println!("init(Some(LIVE_HOST)) result: {result:?}");
assert!(result.is_ok(), "init with target host should succeed: {result:?}");
}
#[tokio::test]
#[ignore]
async fn test_query_advertisements() {
let client = make_live_client();
let identity_key = client.get_identity_key().await.expect("get_identity_key");
let result = client
.query_advertisements(Some(&identity_key), None)
.await;
println!("query_advertisements result: {result:?}");
let ads = result.expect("query_advertisements always returns Ok");
println!("got {} advertisements", ads.len());
}
#[tokio::test]
#[ignore]
async fn test_resolve_host() {
let client = make_live_client();
let identity_key = client.get_identity_key().await.expect("get_identity_key");
let result = client
.resolve_host_for_recipient(&identity_key)
.await;
println!("resolve_host result: {result:?}");
let host = result.expect("resolve_host should succeed");
assert!(!host.is_empty(), "resolved host must be non-empty");
assert!(
host.starts_with("http://") || host.starts_with("https://"),
"resolved host must be a URL, got: {host}"
);
}
#[tokio::test]
#[ignore]
async fn test_register_device() {
let client = make_live_client();
let result = client
.register_device("fake-fcm-token-live-test", Some("test-device"), Some("web"), None)
.await;
println!("register_device result: {result:?}");
match result {
Ok(resp) => {
println!("register_device response: status={}", resp.status);
}
Err(e) => {
println!("register_device error (acceptable): {e}");
}
}
}
#[tokio::test]
#[ignore]
async fn test_list_registered_devices() {
let client = make_live_client();
let result = client.list_registered_devices(None).await;
println!("list_registered_devices result: {result:?}");
match result {
Ok(devices) => {
println!("got {} registered devices", devices.len());
}
Err(e) => {
println!("list_registered_devices error (acceptable): {e}");
}
}
}
#[tokio::test]
#[ignore]
async fn test_encrypted_message_round_trip() {
let client = make_live_client();
let identity_key = client.get_identity_key().await.expect("get_identity_key");
let original = format!("round-trip-test-{}", uuid_like_suffix());
let sent_id = client
.send_message(
&identity_key,
"test_inbox",
&original,
false,
false,
None,
None,
)
.await
.expect("send encrypted message should succeed");
println!("sent encrypted message: {sent_id}");
let messages = client
.list_messages("test_inbox", false, None)
.await
.expect("list should succeed");
let our_msg = messages.iter().find(|m| m.message_id == sent_id);
assert!(our_msg.is_some(), "our sent message must appear in list");
let body = &our_msg.unwrap().body;
assert_eq!(body, &original, "decrypted body must match original");
println!("round-trip verified: decrypted body matches original");
let _ = client.acknowledge_message(vec![sent_id], None).await;
}
#[tokio::test]
#[ignore]
async fn test_send_to_invalid_recipient() {
let client = make_live_client();
let result = client
.send_message(
"not-a-valid-hex-pubkey",
"test_inbox",
"should fail",
false,
false,
None,
None,
)
.await;
println!("send to invalid recipient result: {result:?}");
assert!(result.is_err(), "sending to invalid recipient must return an error");
}
#[tokio::test]
#[ignore]
async fn test_acknowledge_empty_ids() {
let client = make_live_client();
let result = client
.acknowledge_message(vec![], None)
.await;
println!("acknowledge empty ids result: {result:?}");
}
#[tokio::test]
#[ignore]
async fn test_send_notification_to_self() {
let client = make_live_client();
let identity_key = client.get_identity_key().await.expect("get_identity_key");
let result = client
.send_notification(&identity_key, "test notification body", None)
.await;
println!("send_notification result: {result:?}");
match result {
Ok(msg_id) => {
assert!(!msg_id.is_empty(), "notification message_id must be non-empty");
println!("notification sent: {msg_id}");
}
Err(e) => {
println!("send_notification error (acceptable for permission-gated inbox): {e}");
}
}
}
#[tokio::test]
#[ignore]
async fn test_brc103_websocket_auth() {
use bsv_messagebox_client::websocket::MessageBoxWebSocket;
let wallet = ArcWallet::new();
let helper_client = Arc::new(MessageBoxClient::new(
LIVE_HOST.to_string(),
wallet.clone(),
None,
bsv::services::overlay_tools::Network::Mainnet,
));
let identity_key = helper_client
.get_identity_key()
.await
.expect("get_identity_key should succeed");
let ws = MessageBoxWebSocket::connect(
LIVE_HOST,
&identity_key,
wallet,
None,
)
.await
.expect("BRC-103 connect should succeed against live server");
assert!(ws.is_connected(), "is_connected() must return true after successful BRC-103 auth");
let server_key = ws.server_identity_key();
assert!(
!server_key.is_empty(),
"server_identity_key must be non-empty"
);
assert_eq!(
server_key.len(),
66,
"server identity key must be 66-char hex (compressed pubkey), got len={}",
server_key.len()
);
assert!(
server_key.chars().all(|c| c.is_ascii_hexdigit()),
"server identity key must be all hex digits"
);
println!("server identity key: {server_key}");
let room_id = format!("{identity_key}-test_brc103_inbox");
ws.join_room(&room_id)
.await
.expect("join_room should succeed after BRC-103 auth");
println!("joined room: {room_id}");
ws.disconnect().await.expect("disconnect should succeed");
assert!(
!ws.is_connected(),
"is_connected() must return false after disconnect"
);
println!("BRC-103 WebSocket auth test passed");
}
#[tokio::test]
#[ignore]
async fn test_two_client_live_messaging() {
use tokio::sync::mpsc;
let client_a = make_live_client();
let client_b = make_live_client();
let key_a = client_a.get_identity_key().await.expect("client A identity key");
let key_b = client_b.get_identity_key().await.expect("client B identity key");
println!("[Phase 1] Client A key: {}...", &key_a[..12]);
println!("[Phase 1] Client B key: {}...", &key_b[..12]);
let (msg_tx, mut msg_rx) = mpsc::channel::<bsv::remittance::PeerMessage>(4);
println!("[Phase 1] Client A subscribing to e2e_test_inbox...");
let tx = msg_tx.clone();
client_a
.listen_for_live_messages(
"e2e_test_inbox",
Arc::new(move |msg| {
let prefix = msg.sender.get(..12).unwrap_or(&msg.sender);
println!("[Callback] Client A received: sender={}..., body={}", prefix, &msg.body);
let _ = tx.try_send(msg);
}),
None,
)
.await
.expect("Client A listen_for_live_messages should succeed");
println!("[Phase 1] Subscription sent. Waiting 3s for joinRoom to settle on server...");
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
println!("[Phase 1] Ready.");
let test_body = format!("hello-from-B-{}", uuid_like_suffix());
println!("[Phase 2] Client B sending: {test_body}");
let send_result = client_b
.send_live_message(&key_a, "e2e_test_inbox", &test_body, true, false, None, None)
.await;
match &send_result {
Ok(d) => println!("[Phase 2] Message sent OK, id={}, live={}", d.message_id(), d.is_live()),
Err(e) => println!("[Phase 2] Send error: {e}"),
}
assert!(send_result.is_ok(), "send_live_message should succeed: {send_result:?}");
println!("[Phase 3] Waiting up to 15s for Client A to receive the message...");
let received = tokio::time::timeout(
std::time::Duration::from_secs(15),
msg_rx.recv(),
)
.await;
match received {
Ok(Some(msg)) => {
let prefix = msg.sender.get(..12).unwrap_or(&msg.sender);
println!("[Phase 3] Client A received: sender={}..., body={}", prefix, &msg.body);
assert_eq!(msg.sender, key_b, "sender must be Client B's identity key");
assert_eq!(msg.body, test_body, "body must match what Client B sent");
assert_eq!(msg.message_box, "e2e_test_inbox", "message_box must match");
println!("test_two_client_live_messaging PASSED");
}
Ok(None) => panic!("[Phase 3] Message channel closed without receiving"),
Err(_) => panic!(
"[Phase 3] Timed out (15s) waiting for Client A to receive message. \
This indicates BRC-103 room broadcast or general_msg_dispatcher pipeline failure."
),
}
client_a.disconnect_web_socket().await.ok();
client_b.disconnect_web_socket().await.ok();
}
#[tokio::test]
#[ignore]
async fn test_bidirectional_live_messaging() {
use tokio::sync::mpsc;
let client_a = make_live_client();
let client_b = make_live_client();
let key_a = client_a.get_identity_key().await.expect("client A identity key");
let key_b = client_b.get_identity_key().await.expect("client B identity key");
println!("Client A key: {}...", &key_a[..12]);
println!("Client B key: {}...", &key_b[..12]);
let (tx_a, mut rx_a) = mpsc::channel::<bsv::remittance::PeerMessage>(4);
let (tx_b, mut rx_b) = mpsc::channel::<bsv::remittance::PeerMessage>(4);
let tx_a2 = tx_a.clone();
client_a
.listen_for_live_messages(
"e2e_bidir_inbox",
Arc::new(move |msg| {
println!("[A callback] sender={}..., body={}", &msg.sender[..12.min(msg.sender.len())], &msg.body);
let _ = tx_a2.try_send(msg);
}),
None,
)
.await
.expect("Client A listen_for_live_messages should succeed");
let tx_b2 = tx_b.clone();
client_b
.listen_for_live_messages(
"e2e_bidir_inbox",
Arc::new(move |msg| {
println!("[B callback] sender={}..., body={}", &msg.sender[..12.min(msg.sender.len())], &msg.body);
let _ = tx_b2.try_send(msg);
}),
None,
)
.await
.expect("Client B listen_for_live_messages should succeed");
println!("Waiting 3s for subscriptions to settle...");
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
let uid = uuid_like_suffix();
let body_a_to_b = format!("hello-from-A-{uid}");
let body_b_to_a = format!("hello-from-B-{uid}");
println!("Client A sending to B: {body_a_to_b}");
client_a
.send_live_message(&key_b, "e2e_bidir_inbox", &body_a_to_b, true, false, None, None)
.await
.expect("Client A send_live_message should succeed");
println!("Client B sending to A: {body_b_to_a}");
client_b
.send_live_message(&key_a, "e2e_bidir_inbox", &body_b_to_a, true, false, None, None)
.await
.expect("Client B send_live_message should succeed");
println!("Waiting for Client B to receive A's message (up to 15s)...");
let recv_b = tokio::time::timeout(std::time::Duration::from_secs(15), async {
loop {
if let Some(msg) = rx_b.recv().await {
if msg.body.contains(&uid) && msg.sender == key_a {
return msg;
}
}
}
})
.await
.expect("Client B must receive A's message within 15s");
assert_eq!(recv_b.sender, key_a, "message to B must be from A");
assert_eq!(recv_b.body, body_a_to_b, "body must match A's send");
println!("Client B received A's message OK");
println!("Waiting for Client A to receive B's message (up to 15s)...");
let recv_a = tokio::time::timeout(std::time::Duration::from_secs(15), async {
loop {
if let Some(msg) = rx_a.recv().await {
if msg.body.contains(&uid) && msg.sender == key_b {
return msg;
}
}
}
})
.await
.expect("Client A must receive B's message within 15s");
assert_eq!(recv_a.sender, key_b, "message to A must be from B");
assert_eq!(recv_a.body, body_b_to_a, "body must match B's send");
println!("Client A received B's message OK");
println!("test_bidirectional_live_messaging PASSED");
client_a.disconnect_web_socket().await.ok();
client_b.disconnect_web_socket().await.ok();
}
#[tokio::test]
#[ignore]
async fn test_join_leave_room() {
use tokio::sync::mpsc;
let client_a = make_live_client();
let client_b = make_live_client();
let key_a = client_a.get_identity_key().await.expect("client A identity key");
println!("Client A key: {}...", &key_a[..12]);
let (msg_tx, mut msg_rx) = mpsc::channel::<bsv::remittance::PeerMessage>(8);
let tx = msg_tx.clone();
client_a
.listen_for_live_messages(
"e2e_joinleave_inbox",
Arc::new(move |msg| {
println!("[JL callback] received: body={}", &msg.body);
let _ = tx.try_send(msg);
}),
None,
)
.await
.expect("Client A listen_for_live_messages should succeed");
println!("Waiting 3s for subscription to settle...");
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
let uid = uuid_like_suffix();
let body1 = format!("msg1-{uid}");
println!("Phase 1: Client B sending message 1: {body1}");
client_b
.send_live_message(&key_a, "e2e_joinleave_inbox", &body1, true, false, None, None)
.await
.expect("Client B send message 1 should succeed");
let received1 = tokio::time::timeout(std::time::Duration::from_secs(15), async {
loop {
if let Some(msg) = msg_rx.recv().await {
if msg.body.contains(&uid) {
return msg;
}
}
}
})
.await
.expect("Client A must receive message 1 within 15s");
assert_eq!(received1.body, body1, "message 1 body must match");
println!("Phase 1 PASSED: Client A received message 1");
println!("Phase 2: Client A leaving room...");
client_a
.leave_room("e2e_joinleave_inbox", None)
.await
.expect("leave_room should succeed");
println!("Phase 2: Client A left room. Waiting 1s...");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let body2 = format!("msg2-{uid}");
println!("Phase 3: Client B sending message 2: {body2}");
client_b
.send_live_message(&key_a, "e2e_joinleave_inbox", &body2, true, false, None, None)
.await
.expect("Client B send message 2 should succeed");
let maybe_received2 = tokio::time::timeout(
std::time::Duration::from_secs(5),
async {
loop {
if let Some(msg) = msg_rx.recv().await {
if msg.body.contains(&uid) && msg.body != body1 {
return Some(msg);
}
}
}
}
)
.await;
assert!(
maybe_received2.is_err(),
"Client A must NOT receive message 2 after leave_room, but got: {:?}",
maybe_received2
);
println!("Phase 3 PASSED: Client A did not receive message 2 after leave_room (timeout as expected)");
println!("test_join_leave_room PASSED");
client_a.disconnect_web_socket().await.ok();
client_b.disconnect_web_socket().await.ok();
}
#[tokio::test]
#[ignore]
async fn test_rapid_sequential_sends() {
let sender = make_live_client();
let receiver = make_live_client();
let key_receiver = receiver
.get_identity_key()
.await
.expect("receiver identity key");
println!("Receiver: {}", &key_receiver[..12]);
let uid = uuid_like_suffix();
let inbox = "e2e_rapid_inbox";
const N: usize = 5;
for i in 0..N {
let body = format!("rapid-msg-{i}-{uid}");
let result = sender
.send_message(
&key_receiver,
inbox,
&body,
true, false, None,
None,
)
.await;
println!("send {i}: {result:?}");
assert!(result.is_ok(), "send_message {i} should succeed: {result:?}");
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
}
let all_messages = receiver
.list_messages(inbox, false, None)
.await
.expect("list_messages should succeed");
let our_messages: Vec<_> = all_messages
.iter()
.filter(|m| m.body.contains(&uid))
.collect();
println!(
"Listed {} total, {} from this run (uid={})",
all_messages.len(),
our_messages.len(),
uid
);
assert_eq!(
our_messages.len(),
N,
"all {N} rapid messages must arrive, got {}",
our_messages.len()
);
let ids: Vec<String> = our_messages
.iter()
.map(|m| m.message_id.clone())
.collect();
receiver
.acknowledge_message(ids, None)
.await
.expect("acknowledge should succeed");
println!("test_rapid_sequential_sends PASSED");
}
#[tokio::test]
#[ignore]
async fn test_large_message_body() {
let sender = make_live_client();
let receiver = make_live_client();
let key_receiver = receiver
.get_identity_key()
.await
.expect("receiver identity key");
let uid = uuid_like_suffix();
let inbox = "e2e_large_inbox";
const TARGET_SIZE: usize = 10240;
let prefix = format!("LARGE-{uid}-");
let chunk = prefix.as_str();
let mut body = String::with_capacity(TARGET_SIZE + chunk.len());
while body.len() < TARGET_SIZE {
body.push_str(chunk);
}
body.truncate(TARGET_SIZE);
assert_eq!(body.len(), TARGET_SIZE, "body must be exactly {TARGET_SIZE} bytes before send");
println!(
"Sending {TARGET_SIZE}-byte body to receiver {}, inbox={inbox}",
&key_receiver[..12]
);
sender
.send_message(
&key_receiver,
inbox,
&body,
true, false,
None,
None,
)
.await
.expect("send_message (large body) should succeed");
let messages = receiver
.list_messages(inbox, false, None)
.await
.expect("list_messages should succeed");
let our_msg = messages
.iter()
.find(|m| m.body.starts_with(&format!("LARGE-{uid}")));
assert!(our_msg.is_some(), "large message must appear in list (uid={uid})");
let received_body = &our_msg.unwrap().body;
println!(
"Received body length: {} (expected {TARGET_SIZE})",
received_body.len()
);
assert_eq!(
received_body.len(),
TARGET_SIZE,
"received body length must equal {TARGET_SIZE}, got {}",
received_body.len()
);
let msg_id = our_msg.unwrap().message_id.clone();
receiver
.acknowledge_message(vec![msg_id], None)
.await
.expect("acknowledge should succeed");
println!("test_large_message_body PASSED");
}
#[tokio::test]
#[ignore]
async fn test_connect_disconnect_cycle() {
use tokio::sync::mpsc;
let client = make_live_client();
let inbox = "e2e_cycle_inbox";
println!("Phase 1: connecting...");
let (tx1, _rx1) = mpsc::channel::<bsv::remittance::PeerMessage>(4);
client
.listen_for_live_messages(
inbox,
Arc::new(move |msg| {
let _ = tx1.blocking_send(msg);
}),
None,
)
.await
.expect("first listen_for_live_messages should succeed");
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let connected = client.is_ws_connected().await;
println!("Phase 1 socket state: {connected:?}");
assert_eq!(
connected,
Some(true),
"WebSocket must be connected after listen_for_live_messages, got {connected:?}"
);
println!("Phase 1 PASSED: connected");
println!("Phase 2: disconnecting...");
client
.disconnect_web_socket()
.await
.expect("disconnect_web_socket should succeed");
let disconnected = client.is_ws_connected().await;
println!("Phase 2 socket state: {disconnected:?}");
assert!(
disconnected.is_none() || disconnected == Some(false),
"WebSocket must be disconnected, got {disconnected:?}"
);
println!("Phase 2 PASSED: disconnected");
println!("Phase 3: reconnecting...");
let (tx2, mut rx2) = mpsc::channel::<bsv::remittance::PeerMessage>(4);
client
.listen_for_live_messages(
inbox,
Arc::new(move |msg| {
println!("reconnected sub received: sender={}", &msg.sender[..12]);
let _ = tx2.blocking_send(msg);
}),
None,
)
.await
.expect("second listen_for_live_messages should succeed");
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let reconnected = client.is_ws_connected().await;
println!("Phase 3 socket state: {reconnected:?}");
assert_eq!(
reconnected,
Some(true),
"WebSocket must be reconnected after second listen_for_live_messages, got {reconnected:?}"
);
println!("Phase 3 PASSED: reconnected");
println!("Phase 4: verifying messaging after reconnect...");
let key_client = client
.get_identity_key()
.await
.expect("get_identity_key should succeed");
let sender = make_live_client();
let test_body = format!("post-reconnect-msg-{}", uuid_like_suffix());
println!("Sender sending: {test_body}");
sender
.send_live_message(&key_client, inbox, &test_body, true, false, None, None)
.await
.expect("send_live_message after reconnect should succeed");
let received = tokio::time::timeout(
std::time::Duration::from_secs(15),
rx2.recv(),
)
.await;
match received {
Ok(Some(msg)) => {
println!("Phase 4: received msg body={}", msg.body);
assert_eq!(
msg.body, test_body,
"message body must match after reconnect"
);
println!("Phase 4 PASSED: messaging works after reconnect");
}
Ok(None) => panic!("message channel closed without receiving"),
Err(_) => {
println!(
"Phase 4: timed out waiting for post-reconnect message \
(live server timing; cycle test Phases 1-3 PASSED)"
);
}
}
client.disconnect_web_socket().await.ok();
sender.disconnect_web_socket().await.ok();
println!("test_connect_disconnect_cycle PASSED");
}
#[tokio::test]
#[ignore]
async fn test_funded_payment_round_trip() {
let client = make_funded_client();
let identity_key = client
.get_identity_key()
.await
.expect("get_identity_key should succeed");
println!("Wallet key: {}", &identity_key[..12]);
println!("Sending 1000 sats...");
let send_result = client.send_payment(&identity_key, 1000).await;
println!("send_payment result: {send_result:?}");
let msg_id = send_result.expect("send_payment must succeed with funded wallet");
assert!(!msg_id.is_empty(), "send_payment must return a non-empty message ID");
println!("Payment sent, message_id={msg_id}");
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let incoming = client
.list_incoming_payments()
.await
.expect("list_incoming_payments must succeed");
println!("Incoming payments count: {}", incoming.len());
assert!(!incoming.is_empty(), "must have at least one incoming payment");
let payment = incoming
.iter()
.find(|p| p.sender == identity_key)
.unwrap_or_else(|| {
panic!(
"Expected at least one payment from {}..., got {} payments",
&identity_key[..12],
incoming.len()
)
});
println!(
"Found payment: amount={}, sender={}...",
payment.token.amount,
&payment.sender[..12]
);
assert_eq!(payment.token.amount, 1000, "payment amount must be 1000 sats");
assert!(
!payment.token.transaction.is_empty(),
"payment must contain BEEF transaction bytes"
);
assert!(
!payment.token.custom_instructions.derivation_prefix.is_empty(),
"derivation_prefix must be present"
);
assert!(
!payment.token.custom_instructions.derivation_suffix.is_empty(),
"derivation_suffix must be present"
);
let accept_result = client.accept_payment(payment).await;
println!("accept_payment result: {accept_result:?}");
match accept_result {
Ok(()) => println!("accept_payment succeeded (two-wallet setup)"),
Err(ref e) => {
let err = e.to_string();
assert!(
err.contains("invalid status") || err.contains("sending"),
"unexpected accept_payment error (not single-wallet limitation): {err}"
);
println!("accept_payment correctly rejected (single-wallet limitation)");
}
}
println!("test_funded_payment_round_trip PASSED");
}
fn uuid_like_suffix() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.subsec_nanos();
format!("{nanos:010}")
}