sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit 873a2daee1a1895ffa8e7c60bb3d9fd4b0dfb25a
parent d8ad5e3bbf77649df87e7f9edbf497613ed7f271
Author: triesap <tyson@radroots.org>
Date:   Mon, 15 Jun 2026 14:20:49 -0700

sdk: add order status runtime

Add the local order status API over the rr-rs trade projection contract with SDK-owned product receipts. Cover missing orders, accepted lifecycle projection, invalid limits, and sanitized malformed-store errors.

Diffstat:
MCargo.lock | 2++
MCargo.toml | 1+
Mcrates/sdk/Cargo.toml | 9+++++++++
Mcrates/sdk/src/lib.rs | 8++++++++
Acrates/sdk/src/orders_runtime.rs | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/src/product_clients.rs | 8+++-----
Acrates/sdk/tests/orders_runtime.rs | 296+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 543 insertions(+), 5 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1996,6 +1996,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "sqlx", "tempfile", "tokio", "tokio-tungstenite", @@ -2065,6 +2066,7 @@ dependencies = [ "hex", "radroots_authority", "radroots_core", + "radroots_event_store", "radroots_events", "radroots_events_codec", "serde", diff --git a/Cargo.toml b/Cargo.toml @@ -65,6 +65,7 @@ serde = { version = "1", default-features = false, features = [ ] } serde_json = { version = "1", default-features = false, features = ["alloc"] } serde-wasm-bindgen = { version = "0.6" } +sqlx = { version = "0.8.6", default-features = false } tempfile = { version = "3" } tokio = { version = "1" } tokio-tungstenite = "0.26.2" diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml @@ -55,6 +55,7 @@ runtime = [ "radroots_relay_transport/std", "radroots_relay_transport/storage", "radroots_relay_transport/runtime-tokio", + "radroots_trade/event_store", ] local-signer = ["runtime", "radroots_authority/local_signer"] relay-runtime = ["runtime", "radroots_relay_transport/client"] @@ -95,6 +96,14 @@ radroots_replica_db = { workspace = true, default-features = false, features = [ radroots_replica_db_schema = { workspace = true } radroots_replica_sync = { workspace = true, features = ["std"] } radroots_sql_core = { workspace = true, features = ["native"] } +radroots_nostr = { workspace = true, default-features = false, features = [ + "std", + "events", +] } +sqlx = { workspace = true, default-features = false, features = [ + "runtime-tokio", + "sqlite", +] } tempfile = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } tokio-tungstenite = "0.26.2" diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -28,6 +28,8 @@ pub mod listing; mod listings_runtime; pub mod order; #[cfg(feature = "runtime")] +mod orders_runtime; +#[cfg(feature = "runtime")] mod product_clients; pub mod profile; #[cfg(feature = "runtime")] @@ -76,6 +78,12 @@ pub use crate::listings_runtime::{ ListingEnqueueReceipt, ListingPublishRequest, PreparedListingPublish, }; #[cfg(feature = "runtime")] +pub use crate::orders_runtime::{ + ORDER_STATUS_DEFAULT_LIMIT, ORDER_STATUS_MAX_LIMIT, OrderFulfillmentStatusKind, + OrderPaymentStateKind, OrderSettlementStateKind, OrderStatusKind, OrderStatusReceipt, + OrderStatusRequest, +}; +#[cfg(feature = "runtime")] pub use crate::product_clients::{ListingsClient, OrdersClient, SyncClient}; #[cfg(feature = "runtime")] pub use crate::receipt::{RadrootsSdkEventReference, RadrootsSdkLocalMutationReceipt}; diff --git a/crates/sdk/src/orders_runtime.rs b/crates/sdk/src/orders_runtime.rs @@ -0,0 +1,224 @@ +#[cfg(feature = "runtime")] +use crate::{OrdersClient, RadrootsSdkError}; +#[cfg(feature = "runtime")] +use radroots_events::{ids::RadrootsOrderId, order::RadrootsOrderFulfillmentState}; +#[cfg(feature = "runtime")] +use radroots_trade::order::{ + RadrootsOrderPaymentState, RadrootsOrderProjection, RadrootsOrderSettlementState, + RadrootsOrderStatus, RadrootsOrderStoreQueryError, order_projection_for_order_id, +}; + +#[cfg(feature = "runtime")] +pub const ORDER_STATUS_DEFAULT_LIMIT: u32 = 500; +#[cfg(feature = "runtime")] +pub const ORDER_STATUS_MAX_LIMIT: u32 = 1_000; + +#[cfg(feature = "runtime")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OrderStatusRequest { + pub order_id: String, + pub limit: u32, +} + +#[cfg(feature = "runtime")] +impl OrderStatusRequest { + pub fn new(order_id: impl Into<String>) -> Self { + Self { + order_id: order_id.into(), + limit: ORDER_STATUS_DEFAULT_LIMIT, + } + } + + pub fn with_limit(mut self, limit: u32) -> Self { + self.limit = limit; + self + } + + fn validate(&self) -> Result<RadrootsOrderId, RadrootsSdkError> { + if self.limit == 0 || self.limit > ORDER_STATUS_MAX_LIMIT { + return Err(RadrootsSdkError::InvalidRequest { + message: format!( + "order status limit must be between 1 and {ORDER_STATUS_MAX_LIMIT}" + ), + }); + } + RadrootsOrderId::parse(self.order_id.as_str()).map_err(|error| { + RadrootsSdkError::InvalidRequest { + message: format!("order_id is invalid: {error}"), + } + }) + } +} + +#[cfg(feature = "runtime")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct OrderStatusReceipt { + pub order_id: String, + pub found: bool, + pub status: OrderStatusKind, + pub fulfillment_status: Option<OrderFulfillmentStatusKind>, + pub payment_state: OrderPaymentStateKind, + pub settlement_state: OrderSettlementStateKind, + pub lifecycle_terminal: bool, + pub request_event_id: Option<String>, + pub decision_event_id: Option<String>, + pub fulfillment_event_id: Option<String>, + pub cancellation_event_id: Option<String>, + pub receipt_event_id: Option<String>, + pub last_event_id: Option<String>, + pub issue_count: usize, +} + +#[cfg(feature = "runtime")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum OrderStatusKind { + Missing, + Requested, + Accepted, + Declined, + Cancelled, + Completed, + Disputed, + Invalid, +} + +#[cfg(feature = "runtime")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum OrderFulfillmentStatusKind { + AcceptedNotFulfilled, + Preparing, + ReadyForPickup, + OutForDelivery, + Delivered, + SellerCancelled, +} + +#[cfg(feature = "runtime")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum OrderPaymentStateKind { + NotRecorded, + Recorded, + Settled, + Rejected, + Invalid, +} + +#[cfg(feature = "runtime")] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum OrderSettlementStateKind { + NotRequired, + Pending, + Accepted, + Rejected, + Invalid, +} + +#[cfg(feature = "runtime")] +impl<'sdk> OrdersClient<'sdk> { + pub async fn status( + &self, + request: OrderStatusRequest, + ) -> Result<OrderStatusReceipt, RadrootsSdkError> { + let order_id = request.validate()?; + let projection = + order_projection_for_order_id(&self.sdk._event_store, &order_id, request.limit) + .await + .map_err(projection_error)?; + Ok(OrderStatusReceipt::from_projection(projection)) + } +} + +#[cfg(feature = "runtime")] +impl OrderStatusReceipt { + fn from_projection(projection: RadrootsOrderProjection) -> Self { + let found = projection.status != RadrootsOrderStatus::Missing; + Self { + order_id: projection.order_id.into_string(), + found, + status: projection.status.into(), + fulfillment_status: projection.fulfillment_status.map(Into::into), + payment_state: projection.payment.state.into(), + settlement_state: projection.payment.settlement_state.into(), + lifecycle_terminal: projection.lifecycle_terminal, + request_event_id: projection.request_event_id.map(Into::into), + decision_event_id: projection.decision_event_id.map(Into::into), + fulfillment_event_id: projection.fulfillment_event_id.map(Into::into), + cancellation_event_id: projection.cancellation_event_id.map(Into::into), + receipt_event_id: projection.receipt_event_id.map(Into::into), + last_event_id: projection.last_event_id.map(Into::into), + issue_count: projection.issues.len(), + } + } +} + +#[cfg(feature = "runtime")] +impl From<RadrootsOrderStatus> for OrderStatusKind { + fn from(status: RadrootsOrderStatus) -> Self { + match status { + RadrootsOrderStatus::Missing => Self::Missing, + RadrootsOrderStatus::Requested => Self::Requested, + RadrootsOrderStatus::Accepted => Self::Accepted, + RadrootsOrderStatus::Declined => Self::Declined, + RadrootsOrderStatus::Cancelled => Self::Cancelled, + RadrootsOrderStatus::Completed => Self::Completed, + RadrootsOrderStatus::Disputed => Self::Disputed, + RadrootsOrderStatus::Invalid => Self::Invalid, + } + } +} + +#[cfg(feature = "runtime")] +impl From<RadrootsOrderFulfillmentState> for OrderFulfillmentStatusKind { + fn from(status: RadrootsOrderFulfillmentState) -> Self { + match status { + RadrootsOrderFulfillmentState::AcceptedNotFulfilled => Self::AcceptedNotFulfilled, + RadrootsOrderFulfillmentState::Preparing => Self::Preparing, + RadrootsOrderFulfillmentState::ReadyForPickup => Self::ReadyForPickup, + RadrootsOrderFulfillmentState::OutForDelivery => Self::OutForDelivery, + RadrootsOrderFulfillmentState::Delivered => Self::Delivered, + RadrootsOrderFulfillmentState::SellerCancelled => Self::SellerCancelled, + } + } +} + +#[cfg(feature = "runtime")] +impl From<RadrootsOrderPaymentState> for OrderPaymentStateKind { + fn from(state: RadrootsOrderPaymentState) -> Self { + match state { + RadrootsOrderPaymentState::NotRecorded => Self::NotRecorded, + RadrootsOrderPaymentState::Recorded => Self::Recorded, + RadrootsOrderPaymentState::Settled => Self::Settled, + RadrootsOrderPaymentState::Rejected => Self::Rejected, + RadrootsOrderPaymentState::Invalid => Self::Invalid, + } + } +} + +#[cfg(feature = "runtime")] +impl From<RadrootsOrderSettlementState> for OrderSettlementStateKind { + fn from(state: RadrootsOrderSettlementState) -> Self { + match state { + RadrootsOrderSettlementState::NotRequired => Self::NotRequired, + RadrootsOrderSettlementState::Pending => Self::Pending, + RadrootsOrderSettlementState::Accepted => Self::Accepted, + RadrootsOrderSettlementState::Rejected => Self::Rejected, + RadrootsOrderSettlementState::Invalid => Self::Invalid, + } + } +} + +#[cfg(feature = "runtime")] +fn projection_error(error: RadrootsOrderStoreQueryError) -> RadrootsSdkError { + let message = match error { + RadrootsOrderStoreQueryError::Store(_) => "order status store query failed", + RadrootsOrderStoreQueryError::InvalidStoredTagsJson { .. } => { + "stored order event tags could not be decoded" + } + RadrootsOrderStoreQueryError::Decode { .. } => { + "stored order event could not decode as order record" + } + }; + RadrootsSdkError::Projection { + message: message.to_owned(), + } +} diff --git a/crates/sdk/src/product_clients.rs b/crates/sdk/src/product_clients.rs @@ -1,7 +1,5 @@ #[cfg(feature = "runtime")] use crate::RadrootsSdk; -#[cfg(feature = "runtime")] -use core::marker::PhantomData; #[cfg(feature = "runtime")] #[derive(Clone, Copy)] @@ -19,13 +17,13 @@ impl<'sdk> ListingsClient<'sdk> { #[cfg(feature = "runtime")] #[derive(Clone, Copy)] pub struct OrdersClient<'sdk> { - _sdk: PhantomData<&'sdk RadrootsSdk>, + pub(crate) sdk: &'sdk RadrootsSdk, } #[cfg(feature = "runtime")] impl<'sdk> OrdersClient<'sdk> { - pub(crate) fn new(_sdk: &'sdk RadrootsSdk) -> Self { - Self { _sdk: PhantomData } + pub(crate) fn new(sdk: &'sdk RadrootsSdk) -> Self { + Self { sdk } } } diff --git a/crates/sdk/tests/orders_runtime.rs b/crates/sdk/tests/orders_runtime.rs @@ -0,0 +1,296 @@ +#![cfg(feature = "runtime")] + +use radroots_core::{ + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, +}; +use radroots_event_store::{RadrootsEventIngest, RadrootsEventStore}; +use radroots_events::kinds::KIND_LISTING; +use radroots_events::{ + RadrootsNostrEvent, + ids::{RadrootsEventId, RadrootsOrderId}, +}; +use radroots_nostr::prelude::{ + RadrootsNostrKeys, RadrootsNostrSecretKey, RadrootsNostrTimestamp, radroots_event_from_nostr, + radroots_nostr_build_event, +}; +use radroots_sdk::WireEventParts; +use radroots_sdk::order::{ + RadrootsListingAddress, RadrootsOrderDecision, RadrootsOrderDecisionOutcome, + RadrootsOrderEconomicItem, RadrootsOrderEconomicLine, RadrootsOrderEconomics, + RadrootsOrderInventoryCommitment, RadrootsOrderItem, RadrootsOrderPricingBasis, + RadrootsOrderRequest, +}; +use radroots_sdk::{ + ORDER_STATUS_DEFAULT_LIMIT, ORDER_STATUS_MAX_LIMIT, OrderPaymentStateKind, + OrderSettlementStateKind, OrderStatusKind, OrderStatusRequest, RadrootsNostrEventPtr, + RadrootsSdk, RadrootsSdkError, RadrootsSdkTimestamp, +}; + +const BUYER_SECRET_KEY_HEX: &str = + "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5"; +const BUYER_PUBLIC_KEY_HEX: &str = + "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; +const SELLER_SECRET_KEY_HEX: &str = + "59392e9068f66431b12f70218fb61281cb6b433d7f27c55d61f1a63fe1a96ff8"; +const SELLER_PUBLIC_KEY_HEX: &str = + "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af"; + +async fn directory_sdk_and_store() -> (tempfile::TempDir, RadrootsSdk, RadrootsEventStore) { + let tempdir = tempfile::tempdir().expect("tempdir"); + let sdk = RadrootsSdk::builder() + .directory_storage(tempdir.path().join("sdk")) + .fixed_clock(RadrootsSdkTimestamp::from_unix_seconds(1_700_000_000)) + .build() + .await + .expect("sdk"); + let store = + RadrootsEventStore::open_file(&sdk.storage_paths().expect("paths").event_store_path) + .await + .expect("event store"); + (tempdir, sdk, store) +} + +fn order_id(raw: &str) -> RadrootsOrderId { + RadrootsOrderId::parse(raw).expect("order id") +} + +fn listing_address() -> RadrootsListingAddress { + RadrootsListingAddress::parse(format!( + "{KIND_LISTING}:{SELLER_PUBLIC_KEY_HEX}:AAAAAAAAAAAAAAAAAAAAAg" + )) + .expect("listing address") +} + +fn listing_event_ptr() -> RadrootsNostrEventPtr { + RadrootsNostrEventPtr { + id: deterministic_event_id("listing-event").into_string(), + relays: Some("wss://relay.radroots.test".to_owned()), + } +} + +fn deterministic_event_id(raw: &str) -> RadrootsEventId { + let mut bytes = [0u8; 32]; + for (index, byte) in raw.bytes().enumerate() { + let primary = index % bytes.len(); + let secondary = (index * 7 + 13) % bytes.len(); + bytes[primary] = bytes[primary] + .wrapping_add(byte) + .wrapping_add((index as u8).wrapping_mul(31)); + bytes[secondary] ^= byte.rotate_left((index % 8) as u32); + } + let mut hex = String::with_capacity(64); + for byte in bytes { + use core::fmt::Write as _; + write!(&mut hex, "{byte:02x}").expect("write hex"); + } + RadrootsEventId::parse(hex).expect("event id") +} + +fn decimal(raw: &str) -> RadrootsCoreDecimal { + raw.parse().expect("decimal") +} + +fn usd(raw: &str) -> RadrootsCoreMoney { + RadrootsCoreMoney::new(decimal(raw), RadrootsCoreCurrency::USD) +} + +fn economics() -> RadrootsOrderEconomics { + RadrootsOrderEconomics { + quote_id: "quote-1".parse().expect("quote id"), + quote_version: 1, + pricing_basis: RadrootsOrderPricingBasis::ListingEvent, + currency: RadrootsCoreCurrency::USD, + items: vec![RadrootsOrderEconomicItem { + bin_id: "bin-1".parse().expect("bin id"), + bin_count: 2, + quantity_amount: decimal("1"), + quantity_unit: RadrootsCoreUnit::Each, + unit_price_amount: decimal("5"), + unit_price_currency: RadrootsCoreCurrency::USD, + line_subtotal: usd("10"), + }], + discounts: Vec::<RadrootsOrderEconomicLine>::new(), + adjustments: Vec::<RadrootsOrderEconomicLine>::new(), + subtotal: usd("10"), + discount_total: usd("0"), + adjustment_total: usd("0"), + total: usd("10"), + } +} + +fn order_request(raw_order_id: &str) -> RadrootsOrderRequest { + RadrootsOrderRequest { + order_id: order_id(raw_order_id), + listing_addr: listing_address(), + buyer_pubkey: BUYER_PUBLIC_KEY_HEX.parse().expect("buyer pubkey"), + seller_pubkey: SELLER_PUBLIC_KEY_HEX.parse().expect("seller pubkey"), + items: vec![RadrootsOrderItem { + bin_id: "bin-1".parse().expect("bin id"), + bin_count: 2, + }], + economics: economics(), + } +} + +fn order_decision(raw_order_id: &str) -> RadrootsOrderDecision { + RadrootsOrderDecision { + order_id: order_id(raw_order_id), + listing_addr: listing_address(), + buyer_pubkey: BUYER_PUBLIC_KEY_HEX.parse().expect("buyer pubkey"), + seller_pubkey: SELLER_PUBLIC_KEY_HEX.parse().expect("seller pubkey"), + decision: RadrootsOrderDecisionOutcome::Accepted { + inventory_commitments: vec![RadrootsOrderInventoryCommitment { + bin_id: "bin-1".parse().expect("bin id"), + bin_count: 2, + }], + }, + } +} + +fn signed_event( + secret_key_hex: &str, + created_at: u32, + parts: WireEventParts, +) -> RadrootsNostrEvent { + let secret_key = RadrootsNostrSecretKey::from_hex(secret_key_hex).expect("secret key"); + let keys = RadrootsNostrKeys::new(secret_key); + let event = radroots_nostr_build_event(parts.kind, parts.content, parts.tags) + .expect("event builder") + .custom_created_at(RadrootsNostrTimestamp::from_secs(u64::from(created_at))) + .sign_with_keys(&keys) + .expect("signed event"); + radroots_event_from_nostr(&event) +} + +fn signed_order_request_event(raw_order_id: &str, created_at: u32) -> RadrootsNostrEvent { + let draft = radroots_sdk::order::build_order_request_draft( + &listing_event_ptr(), + &order_request(raw_order_id), + ) + .expect("request draft"); + signed_event(BUYER_SECRET_KEY_HEX, created_at, draft.into_wire_parts()) +} + +fn signed_order_decision_event( + raw_order_id: &str, + root_event_id: &RadrootsEventId, + created_at: u32, +) -> RadrootsNostrEvent { + let draft = radroots_sdk::order::build_order_decision_draft( + root_event_id, + root_event_id, + &order_decision(raw_order_id), + ) + .expect("decision draft"); + signed_event(SELLER_SECRET_KEY_HEX, created_at, draft.into_wire_parts()) +} + +#[tokio::test] +async fn order_status_returns_not_found_for_missing_local_order() { + let (_tempdir, sdk, _store) = directory_sdk_and_store().await; + let request = OrderStatusRequest::new("order-1"); + + assert_eq!(request.limit, ORDER_STATUS_DEFAULT_LIMIT); + + let receipt = sdk.orders().status(request).await.expect("status"); + + assert!(!receipt.found); + assert_eq!(receipt.order_id, "order-1"); + assert_eq!(receipt.status, OrderStatusKind::Missing); + assert_eq!(receipt.payment_state, OrderPaymentStateKind::NotRecorded); + assert_eq!( + receipt.settlement_state, + OrderSettlementStateKind::NotRequired + ); + assert_eq!(receipt.issue_count, 0); +} + +#[tokio::test] +async fn order_status_rejects_invalid_limits_before_querying() { + let (_tempdir, sdk, _store) = directory_sdk_and_store().await; + + let zero = sdk + .orders() + .status(OrderStatusRequest::new("order-1").with_limit(0)) + .await + .expect_err("zero limit"); + let too_large = sdk + .orders() + .status(OrderStatusRequest::new("order-1").with_limit(ORDER_STATUS_MAX_LIMIT + 1)) + .await + .expect_err("too large"); + + assert!(matches!(zero, RadrootsSdkError::InvalidRequest { .. })); + assert!(matches!(too_large, RadrootsSdkError::InvalidRequest { .. })); +} + +#[tokio::test] +async fn order_status_projects_local_request_and_decision_events() { + let (_tempdir, sdk, store) = directory_sdk_and_store().await; + let request_event = signed_order_request_event("order-1", 20); + let request_event_id = RadrootsEventId::parse(request_event.id.as_str()).expect("request id"); + let decision_event = signed_order_decision_event("order-1", &request_event_id, 21); + + for (event, observed_at_ms) in [ + (request_event.clone(), 2_000), + (decision_event.clone(), 2_100), + ] { + store + .ingest_event(RadrootsEventIngest::new(event, observed_at_ms)) + .await + .expect("ingest"); + } + + let receipt = sdk + .orders() + .status(OrderStatusRequest::new("order-1").with_limit(1_000)) + .await + .expect("status"); + + assert!(receipt.found); + assert_eq!(receipt.status, OrderStatusKind::Accepted); + assert_eq!( + receipt.request_event_id.as_deref(), + Some(request_event.id.as_str()) + ); + assert_eq!( + receipt.decision_event_id.as_deref(), + Some(decision_event.id.as_str()) + ); + assert_eq!( + receipt.last_event_id.as_deref(), + Some(decision_event.id.as_str()) + ); + assert_eq!(receipt.issue_count, 0); + assert!(!receipt.lifecycle_terminal); +} + +#[tokio::test] +async fn order_status_maps_malformed_local_data_to_sanitized_error() { + let (_tempdir, sdk, store) = directory_sdk_and_store().await; + let request_event = signed_order_request_event("order-1", 30); + let raw_event_json = serde_json::to_string(&request_event).expect("raw event json"); + store + .ingest_event(RadrootsEventIngest::new(request_event.clone(), 3_000)) + .await + .expect("ingest"); + sqlx::query("UPDATE nostr_event SET tags_json = '[' WHERE event_id = ?") + .bind(request_event.id.as_str()) + .execute(store.pool()) + .await + .expect("corrupt tags"); + + let error = sdk + .orders() + .status(OrderStatusRequest::new("order-1")) + .await + .expect_err("projection error"); + let message = error.to_string(); + + assert!(matches!(error, RadrootsSdkError::Projection { .. })); + assert!(message.contains("stored order event tags could not be decoded")); + assert!(!message.contains(raw_event_json.as_str())); + assert!(!message.contains(request_event.sig.as_str())); + assert!(!message.contains("\"tags\"")); + assert!(!message.contains("\"content\"")); +}