radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

commit 28715fad5165fc6eaea1ca5b3d9b05655ac30685
parent ef33ca1ec24b3b17e3171dae80f911c810d60ebf
Author: triesap <tyson@radroots.org>
Date:   Fri,  8 May 2026 17:17:59 +0000

bridge: publish active order requests

- require bridge.order.request callers to provide the listing event pointer
- canonicalize and build order-request events with active trade request contracts
- remove daemon-side listing snapshot fallback and generic request envelope publishing
- validate radrootsd fmt, check, tests, diff checks, and legacy scans

Diffstat:
MCargo.lock | 2++
Msrc/transport/jsonrpc/methods/bridge/listing_publish.rs | 2+-
Msrc/transport/jsonrpc/methods/bridge/order_request.rs | 205+++++++++++++++++++++++++++++++++++++------------------------------------------
3 files changed, 98 insertions(+), 111 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1877,11 +1877,13 @@ version = "0.1.0-alpha.2" name = "radroots_trade" version = "0.1.0-alpha.2" dependencies = [ + "hex", "radroots_core", "radroots_events", "radroots_events_codec", "serde", "serde_json", + "sha2", "thiserror 1.0.69", "ts-rs", ] diff --git a/src/transport/jsonrpc/methods/bridge/listing_publish.rs b/src/transport/jsonrpc/methods/bridge/listing_publish.rs @@ -167,8 +167,8 @@ mod tests { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; - use radroots_events::kinds::{KIND_LISTING, KIND_LISTING_DRAFT}; use radroots_events::farm::RadrootsFarmRef; + use radroots_events::kinds::{KIND_LISTING, KIND_LISTING_DRAFT}; use radroots_events::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, diff --git a/src/transport/jsonrpc/methods/bridge/order_request.rs b/src/transport/jsonrpc/methods/bridge/order_request.rs @@ -1,23 +1,12 @@ use anyhow::Result; use jsonrpsee::server::RpcModule; use radroots_events::RadrootsNostrEventPtr; -use radroots_events::kinds::KIND_LISTING; -use radroots_events::trade::{ - RadrootsTradeEnvelope as TradeListingEnvelope, - RadrootsTradeMessagePayload as TradeListingMessagePayload, - RadrootsTradeMessageType as TradeListingMessageType, RadrootsTradeOrder as TradeOrder, -}; -use radroots_events_codec::trade::{ - RadrootsTradeListingAddress as TradeListingAddress, - trade_envelope_event_build as trade_listing_envelope_event_build, -}; -use radroots_nostr::prelude::{ - RadrootsNostrFilter, RadrootsNostrKind, radroots_event_ptr_from_nostr, - radroots_nostr_build_event, radroots_nostr_filter_tag, radroots_nostr_parse_pubkey, -}; -use radroots_trade::order::canonicalize_order_request_for_signer; -use serde::Deserialize; -use std::time::Duration; +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_nostr::prelude::{radroots_nostr_build_event, radroots_nostr_parse_pubkey}; +use radroots_trade::order::canonicalize_active_order_request_for_signer; +use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::core::bridge::publish::{ @@ -36,6 +25,7 @@ use crate::transport::jsonrpc::{MethodRegistry, RpcContext, RpcError}; #[derive(Debug, Deserialize)] struct BridgeOrderRequestParams { order: TradeOrder, + listing_event: RadrootsNostrEventPtr, #[serde(default)] signer_session_id: Option<String>, #[serde(default)] @@ -44,6 +34,12 @@ struct BridgeOrderRequestParams { idempotency_key: Option<String>, } +#[derive(Serialize)] +struct CanonicalBridgeOrderRequest<'a> { + order: &'a TradeOrder, + listing_event: &'a RadrootsNostrEventPtr, +} + pub fn register(m: &mut RpcModule<RpcContext>, registry: &MethodRegistry) -> Result<()> { registry.track("bridge.order.request"); m.register_async_method( @@ -71,42 +67,33 @@ async fn publish_order_request( &ctx, params.signer_session_id.as_deref(), params.signer_authority.as_ref(), - u32::from(TradeListingMessageType::OrderRequest.kind()), + KIND_TRADE_ORDER_REQUEST, "bridge.order.request", ) .await?; let signer_pubkey = signer.signer_pubkey_hex(); - let order = canonicalize_order_request_for_signer(params.order, signer_pubkey.as_str()) + let listing_event = params.listing_event; + let order = canonicalize_active_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}")))?; radroots_nostr_parse_pubkey(&order.seller_pubkey).map_err(|error| { RpcError::InvalidParams(format!("invalid order.seller_pubkey: {error}")) })?; - let request_fingerprint = fingerprint_bridge_request("bridge.order.request", &signer, &order)?; - let envelope = TradeListingEnvelope::new( - TradeListingMessageType::OrderRequest, - order.listing_addr.clone(), - Some(order.order_id.clone()), - order.clone(), - ); - envelope.validate().map_err(|error| { - RpcError::InvalidParams(format!("invalid order request envelope: {error}")) - })?; - let listing_snapshot = fetch_listing_snapshot(&ctx, &order.listing_addr).await?; - let built = trade_listing_envelope_event_build( - order.seller_pubkey.clone(), - TradeListingMessageType::OrderRequest, - order.listing_addr.clone(), - Some(order.order_id.clone()), - Some(&listing_snapshot), - None, - None, - &TradeListingMessagePayload::OrderRequest(order.clone()), - ) - .map_err(|error| RpcError::Other(format!("failed to build order request event: {error}")))?; - let builder = radroots_nostr_build_event(u32::from(built.kind), built.content, built.tags) - .map_err(|error| { + let request_fingerprint = fingerprint_bridge_request( + "bridge.order.request", + &signer, + &CanonicalBridgeOrderRequest { + order: &order, + 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 builder = + radroots_nostr_build_event(built.kind, built.content, built.tags).map_err(|error| { RpcError::Other(format!("failed to build order request event: {error}")) })?; @@ -116,7 +103,7 @@ async fn publish_order_request( Uuid::new_v4().to_string(), idempotency_key, signer.signer_mode(), - u32::from(TradeListingMessageType::OrderRequest.kind()), + KIND_TRADE_ORDER_REQUEST, None, order.listing_addr.clone(), ctx.state.bridge_config.delivery_policy, @@ -163,57 +150,18 @@ async fn publish_order_request( }) } -async fn fetch_listing_snapshot( - ctx: &RpcContext, - listing_addr: &str, -) -> Result<RadrootsNostrEventPtr, RpcError> { - let listing_addr = TradeListingAddress::parse(listing_addr) - .map_err(|error| RpcError::InvalidParams(format!("invalid order.listing_addr: {error}")))?; - if ctx.state.client.relays().await.is_empty() { - return Ok(synthetic_listing_snapshot(&listing_addr)); - } - let filter = RadrootsNostrFilter::new() - .author( - radroots_nostr_parse_pubkey(&listing_addr.seller_pubkey).map_err(|error| { - RpcError::InvalidParams(format!("invalid order.seller_pubkey: {error}")) - })?, - ) - .kind(RadrootsNostrKind::Custom(KIND_LISTING as u16)); - let filter = radroots_nostr_filter_tag(filter, "d", vec![listing_addr.listing_id.clone()]) - .map_err(|error| { - RpcError::Other(format!("failed to build listing snapshot filter: {error}")) - })?; - let mut events = ctx - .state - .client - .fetch_events(filter, Duration::from_secs(10)) - .await - .map_err(|error| { - RpcError::Other(format!( - "failed to fetch listing snapshot for bridge.order.request: {error}" - )) - })?; - events.sort_by_key(|event| event.created_at); - let event = events.pop().ok_or_else(|| { - RpcError::InvalidParams( - "order.listing_addr must reference an existing public NIP-99 listing".to_string(), - ) - })?; - Ok(radroots_event_ptr_from_nostr(&event)) -} - -fn synthetic_listing_snapshot(listing_addr: &TradeListingAddress) -> RadrootsNostrEventPtr { - RadrootsNostrEventPtr { - id: format!("listing:{}", listing_addr.as_str()), - relays: None, - } -} - #[cfg(test)] mod tests { - use radroots_core::RadrootsCoreDiscountValue; + use radroots_core::{ + RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, + }; + use radroots_events::RadrootsNostrEventPtr; use radroots_events::trade::{ - RadrootsTradeOrder as TradeOrder, RadrootsTradeOrderItem as TradeOrderItem, + RadrootsTradeOrderEconomicItem as TradeOrderEconomicItem, + RadrootsTradeOrderEconomicLine as TradeOrderEconomicLine, + RadrootsTradeOrderEconomics as TradeOrderEconomics, + RadrootsTradeOrderItem as TradeOrderItem, RadrootsTradeOrderRequested as TradeOrder, + RadrootsTradePricingBasis as TradePricingBasis, }; use radroots_identity::RadrootsIdentity; use radroots_nostr::prelude::{ @@ -225,13 +173,13 @@ mod tests { use crate::core::Radrootsd; use crate::core::nip46::session::Nip46Session; use crate::transport::jsonrpc::{MethodRegistry, RpcContext}; - use radroots_trade::order::canonicalize_order_request_for_signer; + use radroots_trade::order::canonicalize_active_order_request_for_signer; use super::{BridgeOrderRequestParams, publish_order_request}; #[test] fn canonicalize_order_request_sets_missing_buyer_and_seller_pubkeys() { - let order = canonicalize_order_request_for_signer( + let order = canonicalize_active_order_request_for_signer( base_order("", ""), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ) @@ -254,7 +202,7 @@ mod tests { "", ); order.items[0].bin_count = 0; - let err = canonicalize_order_request_for_signer( + let err = canonicalize_active_order_request_for_signer( order, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ) @@ -262,20 +210,6 @@ mod tests { assert!(err.to_string().contains("bin_count")); } - #[test] - fn canonicalize_order_request_drops_empty_discounts() { - let order = canonicalize_order_request_for_signer( - base_order( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "", - ), - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - ) - .expect("canonicalize"); - - assert_eq!(order.discounts, None); - } - #[tokio::test] async fn publish_order_request_is_job_backed_and_idempotent() { let identity = RadrootsIdentity::generate(); @@ -296,6 +230,7 @@ mod tests { let session_id = insert_signer_session(&ctx, "session-1").await; let params = BridgeOrderRequestParams { order: base_order("", ""), + listing_event: base_listing_event(), signer_session_id: Some(session_id.clone()), signer_authority: None, idempotency_key: Some("same-key".to_string()), @@ -312,6 +247,7 @@ mod tests { ctx, BridgeOrderRequestParams { order: base_order("", ""), + listing_event: base_listing_event(), signer_session_id: Some(session_id), signer_authority: None, idempotency_key: Some("same-key".to_string()), @@ -345,6 +281,7 @@ mod tests { ctx.clone(), BridgeOrderRequestParams { order: base_order("", ""), + listing_event: base_listing_event(), signer_session_id: Some(session_id.clone()), signer_authority: None, idempotency_key: Some("same-key".to_string()), @@ -359,6 +296,7 @@ mod tests { ctx, BridgeOrderRequestParams { order: conflicting, + listing_event: base_listing_event(), signer_session_id: Some(session_id), signer_authority: None, idempotency_key: Some("same-key".to_string()), @@ -391,6 +329,7 @@ mod tests { ctx, BridgeOrderRequestParams { order: base_order("", ""), + listing_event: base_listing_event(), signer_session_id: None, signer_authority: None, idempotency_key: Some("missing-session".to_string()), @@ -438,6 +377,52 @@ mod tests { "30402:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:AAAAAAAAAAAAAAAAAAAAAg" } + fn base_listing_event() -> RadrootsNostrEventPtr { + RadrootsNostrEventPtr { + id: "listing-event-1".to_string(), + relays: None, + } + } + + fn base_order_economics() -> TradeOrderEconomics { + TradeOrderEconomics { + quote_id: "quote-1".to_string(), + quote_version: 1, + pricing_basis: TradePricingBasis::ListingEvent, + currency: RadrootsCoreCurrency::USD, + items: vec![TradeOrderEconomicItem { + bin_id: "bin-1".to_string(), + bin_count: 2, + quantity_amount: RadrootsCoreDecimal::from(1u32), + quantity_unit: RadrootsCoreUnit::Each, + unit_price_amount: RadrootsCoreDecimal::from(5u32), + unit_price_currency: RadrootsCoreCurrency::USD, + line_subtotal: RadrootsCoreMoney::new( + RadrootsCoreDecimal::from(10u32), + RadrootsCoreCurrency::USD, + ), + }], + discounts: Vec::<TradeOrderEconomicLine>::new(), + adjustments: Vec::<TradeOrderEconomicLine>::new(), + subtotal: RadrootsCoreMoney::new( + RadrootsCoreDecimal::from(10u32), + RadrootsCoreCurrency::USD, + ), + discount_total: RadrootsCoreMoney::new( + RadrootsCoreDecimal::from(0u32), + RadrootsCoreCurrency::USD, + ), + adjustment_total: RadrootsCoreMoney::new( + RadrootsCoreDecimal::from(0u32), + RadrootsCoreCurrency::USD, + ), + total: RadrootsCoreMoney::new( + RadrootsCoreDecimal::from(10u32), + RadrootsCoreCurrency::USD, + ), + } + } + fn base_order(buyer_pubkey: &str, seller_pubkey: &str) -> TradeOrder { TradeOrder { order_id: "order-1".to_string(), @@ -448,7 +433,7 @@ mod tests { bin_id: "bin-1".to_string(), bin_count: 2, }], - discounts: Some(Vec::<RadrootsCoreDiscountValue>::new()), + economics: base_order_economics(), } } }