commit 8912fddd6a3e1c2fd1b102c16b80e63fe2709fee
parent 132310dfe4295570d9d821d37dc716a70e785eac
Author: triesap <tyson@radroots.org>
Date: Fri, 12 Jun 2026 20:11:53 -0700
radrootsd: align bridge order contracts
Diffstat:
8 files changed, 44 insertions(+), 942 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1731,7 +1731,6 @@ dependencies = [
"rust_decimal",
"rust_decimal_macros",
"serde",
- "typeshare",
]
[[package]]
@@ -1740,8 +1739,6 @@ version = "0.1.0-alpha.2"
dependencies = [
"radroots_core",
"serde",
- "ts-rs",
- "typeshare",
]
[[package]]
@@ -1886,7 +1883,6 @@ dependencies = [
"serde_json",
"sha2",
"thiserror 1.0.69",
- "ts-rs",
]
[[package]]
@@ -2449,15 +2445,6 @@ dependencies = [
]
[[package]]
-name = "termcolor"
-version = "1.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
-dependencies = [
- "winapi-util",
-]
-
-[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2829,28 +2816,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
-name = "ts-rs"
-version = "11.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4994acea2522cd2b3b85c1d9529a55991e3ad5e25cdcd3de9d505972c4379424"
-dependencies = [
- "thiserror 2.0.18",
- "ts-rs-macros",
-]
-
-[[package]]
-name = "ts-rs-macros"
-version = "11.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee6ff59666c9cbaec3533964505d39154dc4e0a56151fdea30a09ed0301f62e2"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
- "termcolor",
-]
-
-[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2876,28 +2841,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
-name = "typeshare"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da1bf9fe204f358ffea7f8f779b53923a20278b3ab8e8d97962c5e1b3a54edb7"
-dependencies = [
- "chrono",
- "serde",
- "serde_json",
- "typeshare-annotation",
-]
-
-[[package]]
-name = "typeshare-annotation"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "621963e302416b389a1ec177397e9e62de849a78bd8205d428608553def75350"
-dependencies = [
- "quote",
- "syn",
-]
-
-[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3165,15 +3108,6 @@ dependencies = [
]
[[package]]
-name = "winapi-util"
-version = "0.1.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
-dependencies = [
- "windows-sys 0.61.2",
-]
-
-[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
@@ -24,7 +24,7 @@ radroots_trade = { path = "../lib/crates/trade" }
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] }
[dependencies]
-radroots_core = { workspace = true, features = ["std", "serde", "typeshare"] }
+radroots_core = { workspace = true, features = ["std", "serde"] }
radroots_events = { workspace = true, features = ["serde"] }
radroots_events_codec = { workspace = true, features = ["nostr", "serde_json"] }
radroots_identity = { workspace = true }
diff --git a/src/transport/jsonrpc/methods/bridge/listing_publish.rs b/src/transport/jsonrpc/methods/bridge/listing_publish.rs
@@ -458,6 +458,7 @@ mod tests {
geohash: None,
}),
images: None,
+ published_at: None,
}
}
}
diff --git a/src/transport/jsonrpc/methods/bridge/mod.rs b/src/transport/jsonrpc/methods/bridge/mod.rs
@@ -9,7 +9,6 @@ mod job_status;
mod listing_publish;
mod order_request;
mod profile_publish;
-mod public_trade;
mod shared;
mod status;
@@ -22,6 +21,5 @@ pub fn module(ctx: RpcContext, registry: MethodRegistry) -> Result<RpcModule<Rpc
farm_publish::register(&mut m, ®istry)?;
listing_publish::register(&mut m, ®istry)?;
order_request::register(&mut m, ®istry)?;
- public_trade::register(&mut m, ®istry)?;
Ok(m)
}
diff --git a/src/transport/jsonrpc/methods/bridge/order_request.rs b/src/transport/jsonrpc/methods/bridge/order_request.rs
@@ -1,11 +1,11 @@
use anyhow::Result;
use jsonrpsee::server::RpcModule;
use radroots_events::RadrootsNostrEventPtr;
-use radroots_events::kinds::KIND_TRADE_ORDER_REQUEST;
-use radroots_events::trade::RadrootsTradeOrderRequested as TradeOrder;
-use radroots_events_codec::trade::active_trade_order_request_event_build;
+use radroots_events::kinds::KIND_ORDER_REQUEST;
+use radroots_events::order::RadrootsOrderRequest as TradeOrder;
+use radroots_events_codec::order::order_request_event_build;
use radroots_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_parse_pubkey};
-use radroots_trade::order::canonicalize_active_order_request_for_signer;
+use radroots_trade::order::canonicalize_order_request_for_signer;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@@ -67,13 +67,13 @@ async fn publish_order_request(
&ctx,
params.signer_session_id.as_deref(),
params.signer_authority.as_ref(),
- KIND_TRADE_ORDER_REQUEST,
+ KIND_ORDER_REQUEST,
"bridge.order.request",
)
.await?;
let signer_pubkey = signer.signer_pubkey_hex();
let listing_event = params.listing_event;
- let order = canonicalize_active_order_request_for_signer(params.order, signer_pubkey.as_str())
+ let order = canonicalize_order_request_for_signer(params.order, signer_pubkey.as_str())
.map_err(|error| RpcError::InvalidParams(error.to_string()))?;
radroots_nostr_parse_pubkey(&order.buyer_pubkey)
.map_err(|error| RpcError::InvalidParams(format!("invalid order.buyer_pubkey: {error}")))?;
@@ -88,10 +88,9 @@ async fn publish_order_request(
listing_event: &listing_event,
},
)?;
- let built =
- active_trade_order_request_event_build(&listing_event, &order).map_err(|error| {
- RpcError::Other(format!("failed to build order request event: {error}"))
- })?;
+ let built = order_request_event_build(&listing_event, &order).map_err(|error| {
+ RpcError::Other(format!("failed to build order request event: {error}"))
+ })?;
let builder =
radroots_nostr_build_event(built.kind, built.content, built.tags).map_err(|error| {
RpcError::Other(format!("failed to build order request event: {error}"))
@@ -103,7 +102,7 @@ async fn publish_order_request(
Uuid::new_v4().to_string(),
idempotency_key,
signer.signer_mode(),
- KIND_TRADE_ORDER_REQUEST,
+ KIND_ORDER_REQUEST,
None,
order.listing_addr.clone(),
ctx.state.bridge_config.delivery_policy,
@@ -156,12 +155,11 @@ mod tests {
RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit,
};
use radroots_events::RadrootsNostrEventPtr;
- use radroots_events::trade::{
- RadrootsTradeOrderEconomicItem as TradeOrderEconomicItem,
- RadrootsTradeOrderEconomicLine as TradeOrderEconomicLine,
- RadrootsTradeOrderEconomics as TradeOrderEconomics,
- RadrootsTradeOrderItem as TradeOrderItem, RadrootsTradeOrderRequested as TradeOrder,
- RadrootsTradePricingBasis as TradePricingBasis,
+ use radroots_events::order::{
+ RadrootsOrderEconomicItem as TradeOrderEconomicItem,
+ RadrootsOrderEconomicLine as TradeOrderEconomicLine,
+ RadrootsOrderEconomics as TradeOrderEconomics, RadrootsOrderItem as TradeOrderItem,
+ RadrootsOrderPricingBasis as TradePricingBasis, RadrootsOrderRequest as TradeOrder,
};
use radroots_identity::RadrootsIdentity;
use radroots_nostr::prelude::{
@@ -173,13 +171,13 @@ mod tests {
use crate::core::Radrootsd;
use crate::core::nip46::session::Nip46Session;
use crate::transport::jsonrpc::{MethodRegistry, RpcContext};
- use radroots_trade::order::canonicalize_active_order_request_for_signer;
+ use radroots_trade::order::canonicalize_order_request_for_signer;
use super::{BridgeOrderRequestParams, publish_order_request};
#[test]
fn canonicalize_order_request_sets_missing_buyer_and_seller_pubkeys() {
- let order = canonicalize_active_order_request_for_signer(
+ let order = canonicalize_order_request_for_signer(
base_order("", ""),
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
)
@@ -202,7 +200,7 @@ mod tests {
"",
);
order.items[0].bin_count = 0;
- let err = canonicalize_active_order_request_for_signer(
+ let err = canonicalize_order_request_for_signer(
order,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
)
diff --git a/src/transport/jsonrpc/methods/bridge/public_trade.rs b/src/transport/jsonrpc/methods/bridge/public_trade.rs
@@ -1,743 +0,0 @@
-use anyhow::Result;
-use jsonrpsee::server::RpcModule;
-use radroots_events::RadrootsNostrEventPtr;
-use radroots_events::trade::{
- RadrootsTradeDiscountDecision as TradeDiscountDecision,
- RadrootsTradeMessagePayload as TradeListingMessagePayload,
- RadrootsTradeMessageType as TradeListingMessageType,
-};
-use radroots_events_codec::trade::{
- RadrootsTradeListingAddress as TradeListingAddress,
- trade_envelope_event_build as trade_listing_envelope_event_build,
-};
-use radroots_nostr::prelude::{
- radroots_event_from_nostr, radroots_nostr_build_event, radroots_nostr_fetch_event_by_id,
- radroots_nostr_parse_pubkey,
-};
-use radroots_trade::listing::validation::validate_listing_event;
-use radroots_trade::public_trade::canonicalize_public_trade_context;
-use serde::{Deserialize, Serialize, de::DeserializeOwned};
-use uuid::Uuid;
-
-use crate::core::bridge::publish::{
- BridgePublishSettings, connect_and_publish_event, failed_prepublish_execution,
-};
-use crate::core::bridge::store::new_publish_job;
-use crate::transport::jsonrpc::auth::require_bridge_auth;
-use crate::transport::jsonrpc::methods::bridge::shared::{
- BridgePublishResponse, ensure_bridge_enabled, fingerprint_bridge_request,
- normalize_idempotency_key, reserve_bridge_job, resolve_bridge_signer,
- sign_bridge_event_builder,
-};
-use crate::transport::jsonrpc::{MethodRegistry, RpcContext, RpcError};
-
-#[derive(Debug, Clone, Deserialize, Serialize)]
-struct BridgePublicTradeParams<T> {
- listing_addr: String,
- order_id: String,
- counterparty_pubkey: String,
- #[serde(default)]
- listing_event: Option<RadrootsNostrEventPtr>,
- #[serde(default)]
- root_event_id: Option<String>,
- #[serde(default)]
- prev_event_id: Option<String>,
- payload: T,
- #[serde(default)]
- signer_session_id: Option<String>,
- #[serde(default)]
- idempotency_key: Option<String>,
-}
-
-#[derive(Debug, Clone, Serialize)]
-struct CanonicalBridgePublicTradeRequest<T> {
- listing_addr: String,
- order_id: String,
- counterparty_pubkey: String,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- listing_event: Option<RadrootsNostrEventPtr>,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- root_event_id: Option<String>,
- #[serde(default, skip_serializing_if = "Option::is_none")]
- prev_event_id: Option<String>,
- payload: T,
-}
-
-pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> {
- register_public_trade_method(
- m,
- registry,
- "bridge.order.response",
- TradeListingMessageType::OrderResponse,
- TradeListingMessagePayload::OrderResponse,
- )?;
- register_public_trade_method(
- m,
- registry,
- "bridge.order.revision",
- TradeListingMessageType::OrderRevision,
- TradeListingMessagePayload::OrderRevision,
- )?;
- register_public_trade_method(
- m,
- registry,
- "bridge.order.revision.accept",
- TradeListingMessageType::OrderRevisionAccept,
- TradeListingMessagePayload::OrderRevisionAccept,
- )?;
- register_public_trade_method(
- m,
- registry,
- "bridge.order.revision.decline",
- TradeListingMessageType::OrderRevisionDecline,
- TradeListingMessagePayload::OrderRevisionDecline,
- )?;
- register_public_trade_method(
- m,
- registry,
- "bridge.order.question",
- TradeListingMessageType::Question,
- TradeListingMessagePayload::Question,
- )?;
- register_public_trade_method(
- m,
- registry,
- "bridge.order.answer",
- TradeListingMessageType::Answer,
- TradeListingMessagePayload::Answer,
- )?;
- register_public_trade_method(
- m,
- registry,
- "bridge.order.discount.request",
- TradeListingMessageType::DiscountRequest,
- TradeListingMessagePayload::DiscountRequest,
- )?;
- register_public_trade_method(
- m,
- registry,
- "bridge.order.discount.offer",
- TradeListingMessageType::DiscountOffer,
- TradeListingMessagePayload::DiscountOffer,
- )?;
- register_public_trade_method(
- m,
- registry,
- "bridge.order.discount.accept",
- TradeListingMessageType::DiscountAccept,
- TradeListingMessagePayload::DiscountAccept,
- )?;
- register_public_trade_method(
- m,
- registry,
- "bridge.order.discount.decline",
- TradeListingMessageType::DiscountDecline,
- TradeListingMessagePayload::DiscountDecline,
- )?;
- register_public_trade_method(
- m,
- registry,
- "bridge.order.cancel",
- TradeListingMessageType::Cancel,
- TradeListingMessagePayload::Cancel,
- )?;
- register_public_trade_method(
- m,
- registry,
- "bridge.order.fulfillment.update",
- TradeListingMessageType::FulfillmentUpdate,
- TradeListingMessagePayload::FulfillmentUpdate,
- )?;
- register_public_trade_method(
- m,
- registry,
- "bridge.order.receipt",
- TradeListingMessageType::Receipt,
- TradeListingMessagePayload::Receipt,
- )?;
- Ok(())
-}
-
-fn register_public_trade_method<T>(
- m: &mut RpcModule<RpcContext>,
- registry: &MethodRegistry,
- method_name: &'static str,
- message_type: TradeListingMessageType,
- payload_into: fn(T) -> TradeListingMessagePayload,
-) -> Result<()>
-where
- T: DeserializeOwned + Serialize + Clone + Send + Sync + 'static,
-{
- registry.track(method_name);
- m.register_async_method(method_name, move |params, ctx, extensions| async move {
- require_bridge_auth(&extensions)?;
- let params: BridgePublicTradeParams<T> = params
- .parse()
- .map_err(|e| RpcError::InvalidParams(e.to_string()))?;
- let response = publish_public_trade(
- ctx.as_ref().clone(),
- method_name,
- message_type,
- params,
- payload_into,
- )
- .await?;
- Ok::<BridgePublishResponse, RpcError>(response)
- })?;
- Ok(())
-}
-
-async fn publish_public_trade<T>(
- ctx: RpcContext,
- command: &'static str,
- message_type: TradeListingMessageType,
- params: BridgePublicTradeParams<T>,
- payload_into: fn(T) -> TradeListingMessagePayload,
-) -> Result<BridgePublishResponse, RpcError>
-where
- T: Serialize + Clone,
-{
- ensure_bridge_enabled(&ctx)?;
-
- let idempotency_key = normalize_idempotency_key(params.idempotency_key.clone())?;
- let signer = resolve_bridge_signer(
- &ctx,
- params.signer_session_id.as_deref(),
- message_type.kind(),
- )
- .await?;
- let signer_pubkey = signer.signer_pubkey_hex();
- let (mut canonical, listing_addr) =
- canonicalize_public_trade_params(params, signer_pubkey.as_str(), message_type)?;
- canonical.listing_event =
- resolve_listing_snapshot(&ctx, &listing_addr, message_type, canonical.listing_event)
- .await?;
-
- let request_fingerprint = fingerprint_bridge_request(command, &signer, &canonical)?;
- let payload = payload_into(canonical.payload.clone());
- validate_payload_for_message_type(&payload, message_type)?;
- let built = trade_listing_envelope_event_build(
- canonical.counterparty_pubkey.clone(),
- message_type,
- canonical.listing_addr.clone(),
- Some(canonical.order_id.clone()),
- canonical.listing_event.as_ref(),
- canonical.root_event_id.as_deref(),
- canonical.prev_event_id.as_deref(),
- &payload,
- )
- .map_err(|error| RpcError::InvalidParams(format!("invalid {command} envelope: {error}")))?;
- let builder = radroots_nostr_build_event(built.kind, built.content, built.tags)
- .map_err(|error| RpcError::Other(format!("failed to build {command} event: {error}")))?;
-
- let reserved = reserve_bridge_job(
- &ctx,
- new_publish_job(
- command,
- Uuid::new_v4().to_string(),
- idempotency_key,
- signer.signer_mode(),
- message_type.kind(),
- None,
- Some(canonical.listing_addr.clone()),
- ctx.state.bridge_config.delivery_policy,
- ctx.state.bridge_config.delivery_quorum,
- ),
- request_fingerprint,
- command,
- )?;
- let job = match reserved {
- crate::core::bridge::store::BridgeJobReservation::Accepted(job) => job,
- crate::core::bridge::store::BridgeJobReservation::Duplicate(existing) => {
- return Ok(BridgePublishResponse {
- deduplicated: true,
- job: existing.into(),
- });
- }
- };
-
- let publish_settings = BridgePublishSettings::from_config(&ctx.state.bridge_config);
- let event = match sign_bridge_event_builder(&ctx, &signer, builder, command).await {
- Ok(event) => event,
- Err(error) => {
- let _ = ctx.state.bridge_jobs.complete(
- &job.job_id,
- None,
- failed_prepublish_execution(&publish_settings, error.to_string()),
- );
- return Err(error);
- }
- };
-
- let execution = connect_and_publish_event(&ctx.state.client, &publish_settings, &event).await;
- let job = ctx
- .state
- .bridge_jobs
- .complete(&job.job_id, Some(event.id.to_hex()), execution)
- .map_err(|error| RpcError::Other(format!("failed to persist {command} job: {error}")))?
- .ok_or_else(|| RpcError::Other("bridge job disappeared during completion".to_string()))?;
-
- Ok(BridgePublishResponse {
- deduplicated: false,
- job: job.into(),
- })
-}
-
-fn canonicalize_public_trade_params<T>(
- params: BridgePublicTradeParams<T>,
- signer_pubkey: &str,
- message_type: TradeListingMessageType,
-) -> Result<(CanonicalBridgePublicTradeRequest<T>, TradeListingAddress), RpcError> {
- let context = canonicalize_public_trade_context(
- params.listing_addr,
- params.order_id,
- params.counterparty_pubkey,
- signer_pubkey,
- message_type,
- )
- .map_err(|error| RpcError::InvalidParams(error.to_string()))?;
- radroots_nostr_parse_pubkey(&context.counterparty_pubkey).map_err(|error| {
- RpcError::InvalidParams(format!("invalid counterparty_pubkey: {error}"))
- })?;
- let parsed_listing_addr = TradeListingAddress::parse(&context.listing_addr)
- .map_err(|error| RpcError::InvalidParams(format!("invalid listing_addr: {error}")))?;
-
- let listing_event = if message_type.requires_listing_snapshot() {
- Some(normalize_listing_event_ptr(
- params.listing_event.ok_or_else(|| {
- RpcError::InvalidParams(
- "listing_event is required for this trade message".to_string(),
- )
- })?,
- )?)
- } else {
- None
- };
-
- let (root_event_id, prev_event_id) = if message_type.requires_trade_chain() {
- (
- Some(normalized_required_string(
- params.root_event_id.unwrap_or_default(),
- "root_event_id",
- )?),
- Some(normalized_required_string(
- params.prev_event_id.unwrap_or_default(),
- "prev_event_id",
- )?),
- )
- } else {
- (None, None)
- };
-
- Ok((
- CanonicalBridgePublicTradeRequest {
- listing_addr: context.listing_addr,
- order_id: context.order_id,
- counterparty_pubkey: context.counterparty_pubkey,
- listing_event,
- root_event_id,
- prev_event_id,
- payload: params.payload,
- },
- parsed_listing_addr,
- ))
-}
-
-fn normalize_listing_event_ptr(
- ptr: RadrootsNostrEventPtr,
-) -> Result<RadrootsNostrEventPtr, RpcError> {
- if ptr.id.trim().is_empty() {
- return Err(RpcError::InvalidParams(
- "listing_event.id cannot be empty".to_string(),
- ));
- }
- if ptr
- .relays
- .as_ref()
- .is_some_and(|relay| relay.trim().is_empty())
- {
- return Err(RpcError::InvalidParams(
- "listing_event.relays cannot be empty".to_string(),
- ));
- }
- Ok(ptr)
-}
-
-async fn resolve_listing_snapshot(
- ctx: &RpcContext,
- listing_addr: &TradeListingAddress,
- message_type: TradeListingMessageType,
- listing_event: Option<RadrootsNostrEventPtr>,
-) -> Result<Option<RadrootsNostrEventPtr>, RpcError> {
- if !message_type.requires_listing_snapshot() {
- return Ok(None);
- }
- let Some(listing_event) = listing_event else {
- return Err(RpcError::InvalidParams(
- "listing_event is required for this trade message".to_string(),
- ));
- };
- if ctx.state.client.relays().await.is_empty() {
- return Ok(Some(listing_event));
- }
- let event = radroots_nostr_fetch_event_by_id(&ctx.state.client, &listing_event.id)
- .await
- .map_err(|error| {
- RpcError::Other(format!(
- "failed to fetch listing_event `{}`: {error}",
- listing_event.id
- ))
- })?;
- let validated = validate_listing_event(&radroots_event_from_nostr(&event))
- .map_err(|error| RpcError::InvalidParams(format!("invalid listing_event: {error}")))?;
- if validated.listing_addr != listing_addr.as_str() {
- return Err(RpcError::InvalidParams(
- "listing_event must match listing_addr".to_string(),
- ));
- }
- Ok(Some(listing_event))
-}
-
-fn validate_payload_for_message_type(
- payload: &TradeListingMessagePayload,
- message_type: TradeListingMessageType,
-) -> Result<(), RpcError> {
- match (message_type, payload) {
- (
- TradeListingMessageType::OrderRevisionAccept,
- TradeListingMessagePayload::OrderRevisionAccept(response),
- ) => {
- if !response.accepted {
- return Err(RpcError::InvalidParams(
- "bridge.order.revision.accept payload.accepted must be true".to_string(),
- ));
- }
- }
- (
- TradeListingMessageType::OrderRevisionDecline,
- TradeListingMessagePayload::OrderRevisionDecline(response),
- ) => {
- if response.accepted {
- return Err(RpcError::InvalidParams(
- "bridge.order.revision.decline payload.accepted must be false".to_string(),
- ));
- }
- }
- (
- TradeListingMessageType::DiscountAccept,
- TradeListingMessagePayload::DiscountAccept(TradeDiscountDecision::Accept { .. }),
- )
- | (
- TradeListingMessageType::DiscountDecline,
- TradeListingMessagePayload::DiscountDecline(TradeDiscountDecision::Decline { .. }),
- ) => {}
- (TradeListingMessageType::DiscountAccept, _) => {
- return Err(RpcError::InvalidParams(
- "bridge.order.discount.accept payload must be an accept decision".to_string(),
- ));
- }
- (TradeListingMessageType::DiscountDecline, _) => {
- return Err(RpcError::InvalidParams(
- "bridge.order.discount.decline payload must be a decline decision".to_string(),
- ));
- }
- _ => {}
- }
- Ok(())
-}
-
-fn normalized_required_string(value: String, field: &str) -> Result<String, RpcError> {
- let value = value.trim().to_string();
- if value.is_empty() {
- return Err(RpcError::InvalidParams(format!("{field} cannot be empty")));
- }
- Ok(value)
-}
-
-#[cfg(test)]
-mod tests {
- use radroots_core::{RadrootsCoreDecimal, RadrootsCoreDiscountValue, RadrootsCorePercent};
- use radroots_events::kinds::KIND_LISTING;
- use radroots_events::trade::{
- RadrootsTradeDiscountRequest as TradeDiscountRequest,
- RadrootsTradeOrderResponse as TradeOrderResponse,
- RadrootsTradeOrderRevisionResponse as TradeOrderRevisionResponse,
- RadrootsTradeQuestion as TradeQuestion,
- };
- use radroots_identity::RadrootsIdentity;
- use radroots_nostr::prelude::RadrootsNostrMetadata;
-
- use crate::app::config::{BridgeConfig, Nip46Config};
- use crate::core::Radrootsd;
- use crate::transport::jsonrpc::{MethodRegistry, RpcContext};
-
- use super::*;
-
- #[tokio::test]
- async fn publish_order_response_is_job_backed_and_idempotent() {
- let identity = RadrootsIdentity::generate();
- let seller_pubkey = identity.public_key_hex();
- let metadata: RadrootsNostrMetadata =
- serde_json::from_str(r#"{"name":"radrootsd-test"}"#).expect("metadata");
- let state = Radrootsd::new(
- identity,
- metadata,
- BridgeConfig {
- enabled: true,
- bearer_token: Some("secret".to_string()),
- ..BridgeConfig::default()
- },
- Nip46Config::default(),
- )
- .expect("state");
- let ctx = RpcContext::new(state, MethodRegistry::default());
- let params = BridgePublicTradeParams {
- listing_addr: base_listing_addr(&seller_pubkey),
- order_id: "order-1".to_string(),
- counterparty_pubkey: base_buyer_pubkey().to_string(),
- listing_event: None,
- root_event_id: Some("order-request-event".to_string()),
- prev_event_id: Some("order-request-event".to_string()),
- payload: TradeOrderResponse {
- accepted: true,
- reason: None,
- },
- signer_session_id: None,
- idempotency_key: Some("same-key".to_string()),
- };
-
- let first = publish_public_trade(
- ctx.clone(),
- "bridge.order.response",
- TradeListingMessageType::OrderResponse,
- params.clone(),
- TradeListingMessagePayload::OrderResponse,
- )
- .await
- .expect("first");
- assert!(!first.deduplicated);
- assert_eq!(first.job.command, "bridge.order.response");
- assert_eq!(
- first.job.event_kind,
- TradeListingMessageType::OrderResponse.kind()
- );
- assert_eq!(
- first.job.event_addr.as_deref(),
- Some(base_listing_addr(&seller_pubkey).as_str())
- );
- assert_eq!(first.job.signer_mode, "embedded_service_identity");
-
- let second = publish_public_trade(
- ctx,
- "bridge.order.response",
- TradeListingMessageType::OrderResponse,
- params,
- TradeListingMessagePayload::OrderResponse,
- )
- .await
- .expect("second");
- assert!(second.deduplicated);
- assert_eq!(second.job.job_id, first.job.job_id);
- }
-
- #[tokio::test]
- async fn publish_snapshot_message_requires_listing_event() {
- let ctx = buyer_ctx().expect("ctx");
- let err = publish_public_trade(
- ctx,
- "bridge.order.discount.request",
- TradeListingMessageType::DiscountRequest,
- BridgePublicTradeParams {
- listing_addr: base_listing_addr(base_seller_pubkey()),
- order_id: "order-1".to_string(),
- counterparty_pubkey: base_seller_pubkey().to_string(),
- listing_event: None,
- root_event_id: Some("root".to_string()),
- prev_event_id: Some("prev".to_string()),
- payload: TradeDiscountRequest {
- discount_id: "discount-1".to_string(),
- value: RadrootsCoreDiscountValue::Percent(RadrootsCorePercent::new(
- RadrootsCoreDecimal::from(5u32),
- )),
- },
- signer_session_id: None,
- idempotency_key: None,
- },
- TradeListingMessagePayload::DiscountRequest,
- )
- .await
- .expect_err("missing listing_event");
- assert!(err.to_string().contains("listing_event"));
- }
-
- #[tokio::test]
- async fn publish_chain_message_requires_root_and_prev() {
- let identity = RadrootsIdentity::generate();
- let seller_pubkey = identity.public_key_hex();
- let metadata: RadrootsNostrMetadata =
- serde_json::from_str(r#"{"name":"radrootsd-test"}"#).expect("metadata");
- let state = Radrootsd::new(
- identity,
- metadata,
- BridgeConfig {
- enabled: true,
- bearer_token: Some("secret".to_string()),
- ..BridgeConfig::default()
- },
- Nip46Config::default(),
- )
- .expect("state");
- let ctx = RpcContext::new(state, MethodRegistry::default());
- let err = publish_public_trade(
- ctx,
- "bridge.order.response",
- TradeListingMessageType::OrderResponse,
- BridgePublicTradeParams {
- listing_addr: base_listing_addr(&seller_pubkey),
- order_id: "order-1".to_string(),
- counterparty_pubkey: base_buyer_pubkey().to_string(),
- listing_event: None,
- root_event_id: None,
- prev_event_id: Some("prev".to_string()),
- payload: TradeOrderResponse {
- accepted: true,
- reason: None,
- },
- signer_session_id: None,
- idempotency_key: None,
- },
- TradeListingMessagePayload::OrderResponse,
- )
- .await
- .expect_err("missing root_event_id");
- assert!(err.to_string().contains("root_event_id"));
- }
-
- #[tokio::test]
- async fn publish_buyer_message_rejects_listing_seller_signer() {
- let identity = RadrootsIdentity::generate();
- let seller_pubkey = identity.public_key_hex();
- let metadata: RadrootsNostrMetadata =
- serde_json::from_str(r#"{"name":"radrootsd-test"}"#).expect("metadata");
- let state = Radrootsd::new(
- identity,
- metadata,
- BridgeConfig {
- enabled: true,
- bearer_token: Some("secret".to_string()),
- ..BridgeConfig::default()
- },
- Nip46Config::default(),
- )
- .expect("state");
- let ctx = RpcContext::new(state, MethodRegistry::default());
- let err = publish_public_trade(
- ctx,
- "bridge.order.question",
- TradeListingMessageType::Question,
- BridgePublicTradeParams {
- listing_addr: base_listing_addr(&seller_pubkey),
- order_id: "order-1".to_string(),
- counterparty_pubkey: base_buyer_pubkey().to_string(),
- listing_event: None,
- root_event_id: Some("root".to_string()),
- prev_event_id: Some("prev".to_string()),
- payload: TradeQuestion {
- question_id: "q-1".to_string(),
- },
- signer_session_id: None,
- idempotency_key: None,
- },
- TradeListingMessagePayload::Question,
- )
- .await
- .expect_err("seller signed buyer message");
- assert!(err.to_string().contains("buyer"));
- }
-
- #[tokio::test]
- async fn publish_revision_accept_rejects_decline_payload() {
- let (ctx, seller_pubkey) = signer_ctx().expect("ctx");
- let err = publish_public_trade(
- ctx,
- "bridge.order.revision.accept",
- TradeListingMessageType::OrderRevisionAccept,
- BridgePublicTradeParams {
- listing_addr: base_listing_addr(&seller_pubkey),
- order_id: "order-1".to_string(),
- counterparty_pubkey: base_buyer_pubkey().to_string(),
- listing_event: None,
- root_event_id: Some("root".to_string()),
- prev_event_id: Some("prev".to_string()),
- payload: TradeOrderRevisionResponse {
- accepted: false,
- reason: Some("no".to_string()),
- },
- signer_session_id: None,
- idempotency_key: None,
- },
- TradeListingMessagePayload::OrderRevisionAccept,
- )
- .await
- .expect_err("decline payload");
- assert!(err.to_string().contains("payload.accepted"));
- }
-
- fn buyer_ctx() -> Result<RpcContext, RpcError> {
- signer_ctx().map(|(ctx, _)| ctx)
- }
-
- fn signer_ctx() -> Result<(RpcContext, String), RpcError> {
- let identity = RadrootsIdentity::generate();
- let signer_pubkey = identity.public_key_hex();
- let metadata: RadrootsNostrMetadata =
- serde_json::from_str(r#"{"name":"radrootsd-test"}"#).expect("metadata");
- let state = Radrootsd::new(
- identity,
- metadata,
- BridgeConfig {
- enabled: true,
- bearer_token: Some("secret".to_string()),
- ..BridgeConfig::default()
- },
- Nip46Config::default(),
- )
- .map_err(|error| RpcError::Other(format!("build state: {error}")))?;
- Ok((
- RpcContext::new(state, MethodRegistry::default()),
- signer_pubkey,
- ))
- }
-
- fn base_seller_pubkey() -> &'static str {
- "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
- }
-
- fn base_buyer_pubkey() -> &'static str {
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
- }
-
- fn base_listing_addr(seller_pubkey: &str) -> String {
- format!("{KIND_LISTING}:{seller_pubkey}:AAAAAAAAAAAAAAAAAAAAAg")
- }
-
- #[test]
- fn validate_discount_decline_payload_shape() {
- let payload = TradeListingMessagePayload::DiscountDecline(TradeDiscountDecision::Decline {
- reason: Some("no".to_string()),
- });
- validate_payload_for_message_type(&payload, TradeListingMessageType::DiscountDecline)
- .expect("decline");
-
- let err = validate_payload_for_message_type(
- &TradeListingMessagePayload::DiscountDecline(TradeDiscountDecision::Accept {
- value: RadrootsCoreDiscountValue::Percent(RadrootsCorePercent::new(
- RadrootsCoreDecimal::from(5u32),
- )),
- }),
- TradeListingMessageType::DiscountDecline,
- )
- .expect_err("accept");
- assert!(err.to_string().contains("decline"));
- }
-}
diff --git a/src/transport/jsonrpc/methods/bridge/shared.rs b/src/transport/jsonrpc/methods/bridge/shared.rs
@@ -1,7 +1,6 @@
use anyhow::Result;
use nostr::Event;
use radroots_nostr::prelude::RadrootsNostrEventBuilder;
-use radroots_nostr_signer::prelude::RadrootsNostrSignerBackend;
use serde::Serialize;
use sha2::{Digest, Sha256};
@@ -101,9 +100,6 @@ pub(super) fn ensure_bridge_enabled(ctx: &RpcContext) -> Result<(), RpcError> {
#[derive(Clone)]
pub(super) enum BridgeSignerSelection {
- EmbeddedServiceIdentity {
- signer_pubkey_hex: String,
- },
Nip46Session {
session_id: String,
session: crate::core::nip46::session::Nip46Session,
@@ -113,52 +109,17 @@ pub(super) enum BridgeSignerSelection {
impl BridgeSignerSelection {
pub(super) fn signer_pubkey_hex(&self) -> String {
match self {
- Self::EmbeddedServiceIdentity { signer_pubkey_hex } => signer_pubkey_hex.clone(),
Self::Nip46Session { session, .. } => session.remote_signer_pubkey.to_hex(),
}
}
pub(super) fn signer_mode(&self) -> String {
match self {
- Self::EmbeddedServiceIdentity { .. } => "embedded_service_identity".to_string(),
Self::Nip46Session { session_id, .. } => format!("nip46_session:{session_id}"),
}
}
}
-pub(super) fn bridge_signer_pubkey_hex(ctx: &RpcContext) -> Result<String, RpcError> {
- Ok(ctx
- .state
- .bridge_signer
- .signer_identity()
- .map_err(|error| RpcError::Other(format!("bridge signer unavailable: {error}")))?
- .ok_or_else(|| RpcError::Other("bridge signer identity is missing".to_string()))?
- .public_key_hex)
-}
-
-pub(super) async fn resolve_bridge_signer(
- ctx: &RpcContext,
- signer_session_id: Option<&str>,
- event_kind: u32,
-) -> Result<BridgeSignerSelection, RpcError> {
- match signer_session_id
- .map(str::trim)
- .filter(|value| !value.is_empty())
- {
- Some(session_id) => {
- let session = nip46_session::get_session(ctx, session_id).await?;
- nip46_session::require_sign_event_permission(&session, event_kind)?;
- Ok(BridgeSignerSelection::Nip46Session {
- session_id: session_id.to_string(),
- session,
- })
- }
- None => Ok(BridgeSignerSelection::EmbeddedServiceIdentity {
- signer_pubkey_hex: bridge_signer_pubkey_hex(ctx)?,
- }),
- }
-}
-
pub(super) async fn resolve_actor_bridge_signer(
ctx: &RpcContext,
signer_session_id: Option<&str>,
@@ -240,18 +201,12 @@ fn require_signer_authority(
}
pub(super) async fn sign_bridge_event_builder(
- ctx: &RpcContext,
+ _ctx: &RpcContext,
signer: &BridgeSignerSelection,
builder: RadrootsNostrEventBuilder,
label: &str,
) -> Result<Event, RpcError> {
match signer {
- BridgeSignerSelection::EmbeddedServiceIdentity { .. } => ctx
- .state
- .bridge_signer
- .sign_event_builder(builder)
- .map(|signed| signed.event)
- .map_err(|error| RpcError::Other(format!("failed to sign {label} event: {error}"))),
BridgeSignerSelection::Nip46Session { session, .. } => match session.role() {
Nip46SessionRole::InboundLocalSigner => builder
.sign_with_keys(&session.client_keys)
@@ -329,7 +284,7 @@ mod tests {
use super::{
BridgeJobView, fingerprint_bridge_request, normalize_idempotency_key,
- resolve_actor_bridge_signer, resolve_bridge_signer,
+ resolve_actor_bridge_signer,
};
use std::time::Instant;
@@ -340,53 +295,6 @@ mod tests {
}
#[tokio::test]
- async fn resolve_bridge_signer_prefers_requested_nip46_session() {
- let identity = RadrootsIdentity::generate();
- let metadata: RadrootsNostrMetadata =
- serde_json::from_str(r#"{"name":"radrootsd-test"}"#).expect("metadata");
- let state = Radrootsd::new(
- identity.clone(),
- metadata,
- BridgeConfig::default(),
- Nip46Config::default(),
- )
- .expect("state");
- let session_keys = RadrootsNostrKeys::generate();
- state
- .nip46_sessions
- .insert(Nip46Session {
- id: "session-1".to_string(),
- client: RadrootsNostrClient::new(session_keys.clone()),
- client_keys: session_keys.clone(),
- client_pubkey: session_keys.public_key(),
- remote_signer_pubkey: session_keys.public_key(),
- user_pubkey: None,
- relays: vec!["wss://relay.example.com".to_string()],
- perms: vec!["sign_event".to_string()],
- name: None,
- url: None,
- image: None,
- expires_at: Some(Instant::now() + std::time::Duration::from_secs(60)),
- auth_required: false,
- authorized: true,
- auth_url: None,
- pending_request: None,
- signer_authority: None,
- })
- .await;
- let ctx = RpcContext::new(state, MethodRegistry::default());
-
- let signer = resolve_bridge_signer(&ctx, Some("session-1"), 30402)
- .await
- .expect("session signer");
- assert_eq!(
- signer.signer_pubkey_hex(),
- session_keys.public_key().to_hex()
- );
- assert_eq!(signer.signer_mode(), "nip46_session:session-1");
- }
-
- #[tokio::test]
async fn resolve_actor_bridge_signer_rejects_missing_session_id() {
let identity = RadrootsIdentity::generate();
let metadata: RadrootsNostrMetadata =
@@ -526,9 +434,28 @@ mod tests {
#[test]
fn fingerprint_bridge_request_changes_when_request_changes() {
- let signer = super::BridgeSignerSelection::EmbeddedServiceIdentity {
- signer_pubkey_hex: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
- .to_string(),
+ let session_keys = RadrootsNostrKeys::generate();
+ let signer = super::BridgeSignerSelection::Nip46Session {
+ session_id: "session-1".to_string(),
+ session: Nip46Session {
+ id: "session-1".to_string(),
+ client: RadrootsNostrClient::new(session_keys.clone()),
+ client_keys: session_keys.clone(),
+ client_pubkey: session_keys.public_key(),
+ remote_signer_pubkey: session_keys.public_key(),
+ user_pubkey: None,
+ relays: vec!["wss://relay.example.com".to_string()],
+ perms: vec!["sign_event".to_string()],
+ name: None,
+ url: None,
+ image: None,
+ expires_at: None,
+ auth_required: false,
+ authorized: true,
+ auth_url: None,
+ pending_request: None,
+ signer_authority: None,
+ },
};
let first = fingerprint_bridge_request(
"bridge.order.request",
diff --git a/src/transport/jsonrpc/methods/mod.rs b/src/transport/jsonrpc/methods/mod.rs
@@ -64,19 +64,6 @@ mod tests {
assert!(root.method("bridge.farm.publish").is_some());
assert!(root.method("bridge.listing.publish").is_some());
assert!(root.method("bridge.order.request").is_some());
- assert!(root.method("bridge.order.response").is_some());
- assert!(root.method("bridge.order.revision").is_some());
- assert!(root.method("bridge.order.revision.accept").is_some());
- assert!(root.method("bridge.order.revision.decline").is_some());
- assert!(root.method("bridge.order.question").is_some());
- assert!(root.method("bridge.order.answer").is_some());
- assert!(root.method("bridge.order.discount.request").is_some());
- assert!(root.method("bridge.order.discount.offer").is_some());
- assert!(root.method("bridge.order.discount.accept").is_some());
- assert!(root.method("bridge.order.discount.decline").is_some());
- assert!(root.method("bridge.order.cancel").is_some());
- assert!(root.method("bridge.order.fulfillment.update").is_some());
- assert!(root.method("bridge.order.receipt").is_some());
assert!(root.method("nip46.connect").is_none());
}