radrootsd

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

commit 1b468871eb1e63c41e88ca395d4d2c2b6cad4fd8
parent 214b068ddb1085ff89ac08f5839462b7fe708454
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 02:31:58 -0700

bridge: align listing publish prep

- replace deleted trade publish helpers with direct listing validation
- keep bridge listing kind normalization local to radrootsd
- update bridge order test fixtures for typed rr-rs IDs
- validate with nix run .#check and nix run .#test

Diffstat:
MCargo.lock | 9+++++++++
Msrc/transport/jsonrpc/methods/bridge/listing_publish.rs | 61+++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/transport/jsonrpc/methods/bridge/order_request.rs | 76++++++++++++++++++++++++++++++++++++++--------------------------------------
3 files changed, 90 insertions(+), 56 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1725,6 +1725,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] +name = "radroots_authority" +version = "0.1.0-alpha.2" +dependencies = [ + "radroots_events", + "thiserror 1.0.69", +] + +[[package]] name = "radroots_core" version = "0.1.0-alpha.2" dependencies = [ @@ -1879,6 +1887,7 @@ version = "0.1.0-alpha.2" dependencies = [ "base64 0.22.1", "hex", + "radroots_authority", "radroots_core", "radroots_events", "radroots_events_codec", diff --git a/src/transport/jsonrpc/methods/bridge/listing_publish.rs b/src/transport/jsonrpc/methods/bridge/listing_publish.rs @@ -1,13 +1,12 @@ use anyhow::Result; use jsonrpsee::server::RpcModule; +use radroots_events::RadrootsNostrEvent; +use radroots_events::kinds::{KIND_LISTING, KIND_LISTING_DRAFT, is_listing_kind}; use radroots_events::listing::RadrootsListing; use radroots_events_codec::listing::encode::to_wire_parts_with_kind; use radroots_events_codec::wire::WireEventParts; use radroots_nostr::prelude::radroots_nostr_build_event; -use radroots_trade::listing::publish::{ - RadrootsTradeListingPublishError, canonicalize_listing_for_seller, resolve_listing_kind, - validate_listing_for_seller, -}; +use radroots_trade::listing::validation::{RadrootsTradeListing, validate_listing_event}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -65,7 +64,7 @@ async fn publish_listing( ) -> Result<BridgePublishResponse, RpcError> { ensure_bridge_enabled(&ctx)?; let idempotency_key = normalize_idempotency_key(params.idempotency_key)?; - let kind = resolve_listing_kind(params.kind).map_err(map_listing_publish_error)?; + let kind = resolve_bridge_listing_kind(params.kind)?; let signer = resolve_actor_bridge_signer( &ctx, params.signer_session_id.as_deref(), @@ -75,7 +74,7 @@ async fn publish_listing( ) .await?; let signer_pubkey = signer.signer_pubkey_hex(); - let listing = canonicalize_listing_for_seller(params.listing, signer_pubkey.as_str()); + let listing = canonicalize_bridge_listing_for_signer(params.listing, signer_pubkey.as_str()); let canonical = CanonicalBridgeListingPublishRequest { kind, listing }; let request_fingerprint = fingerprint_bridge_request("bridge.listing.publish", &signer, &canonical)?; @@ -150,15 +149,40 @@ fn validate_canonical_listing_contract_for_signer( listing: &RadrootsListing, signer_pubkey: &str, parts: &WireEventParts, -) -> Result<radroots_trade::listing::validation::RadrootsTradeListing, RpcError> { - let validated = validate_listing_for_seller(listing.clone(), signer_pubkey, parts.kind) - .map_err(map_listing_publish_error)?; +) -> Result<RadrootsTradeListing, RpcError> { + let event = RadrootsNostrEvent { + id: String::new(), + author: signer_pubkey.to_string(), + created_at: 0, + kind: parts.kind, + tags: parts.tags.clone(), + content: parts.content.clone(), + sig: String::new(), + }; + let validated = validate_listing_event(&event) + .map_err(|error| RpcError::InvalidParams(format!("invalid listing contract: {error}")))?; debug_assert_eq!(validated.listing.d_tag, listing.d_tag); Ok(validated) } -fn map_listing_publish_error(error: RadrootsTradeListingPublishError) -> RpcError { - RpcError::InvalidParams(error.to_string()) +fn resolve_bridge_listing_kind(kind: Option<u32>) -> Result<u32, RpcError> { + let kind = kind.unwrap_or(KIND_LISTING); + if !is_listing_kind(kind) { + return Err(RpcError::InvalidParams(format!( + "listing kind must be {KIND_LISTING} or {KIND_LISTING_DRAFT}" + ))); + } + Ok(kind) +} + +fn canonicalize_bridge_listing_for_signer( + mut listing: RadrootsListing, + signer_pubkey: &str, +) -> RadrootsListing { + if listing.farm.pubkey.trim().is_empty() { + listing.farm.pubkey = signer_pubkey.to_string(); + } + listing } #[cfg(test)] @@ -168,6 +192,7 @@ mod tests { RadrootsCoreQuantityPrice, RadrootsCoreUnit, }; use radroots_events::farm::RadrootsFarmRef; + use radroots_events::ids::{RadrootsDTag, RadrootsInventoryBinId}; use radroots_events::kinds::{KIND_LISTING, KIND_LISTING_DRAFT}; use radroots_events::listing::{ RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, @@ -184,21 +209,21 @@ mod tests { use crate::core::Radrootsd; use crate::core::nip46::session::Nip46Session; use crate::transport::jsonrpc::{MethodRegistry, RpcContext}; - use radroots_trade::listing::publish::canonicalize_listing_for_seller; use super::{ - BridgeListingPublishParams, publish_listing, validate_canonical_listing_contract_for_signer, + BridgeListingPublishParams, canonicalize_bridge_listing_for_signer, publish_listing, + validate_canonical_listing_contract_for_signer, }; #[test] fn canonicalize_listing_sets_missing_farm_pubkey() { - let listing = canonicalize_listing_for_seller(base_listing(), "abc123"); + let listing = canonicalize_bridge_listing_for_signer(base_listing(), "abc123"); assert_eq!(listing.farm.pubkey, "abc123"); } #[test] fn validate_canonical_listing_contract_rejects_mismatched_seller_before_sign() { - let listing = canonicalize_listing_for_seller(base_listing(), "abc123"); + let listing = canonicalize_bridge_listing_for_signer(base_listing(), "abc123"); let mut invalid = listing.clone(); invalid.farm.pubkey = "other".to_string(); let parts = to_wire_parts_with_kind(&invalid, KIND_LISTING).expect("wire parts"); @@ -401,7 +426,7 @@ mod tests { fn base_listing() -> RadrootsListing { RadrootsListing { - d_tag: "AAAAAAAAAAAAAAAAAAAAAg".to_string(), + d_tag: RadrootsDTag::parse("AAAAAAAAAAAAAAAAAAAAAg").expect("listing d tag"), farm: RadrootsFarmRef { pubkey: String::new(), d_tag: "AAAAAAAAAAAAAAAAAAAAAw".to_string(), @@ -417,9 +442,9 @@ mod tests { profile: None, year: None, }, - primary_bin_id: "bin-1".to_string(), + primary_bin_id: RadrootsInventoryBinId::parse("bin-1").expect("primary bin id"), bins: vec![RadrootsListingBin { - bin_id: "bin-1".to_string(), + bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"), quantity: RadrootsCoreQuantity::new( RadrootsCoreDecimal::from(1000u32), RadrootsCoreUnit::MassG, diff --git a/src/transport/jsonrpc/methods/bridge/order_request.rs b/src/transport/jsonrpc/methods/bridge/order_request.rs @@ -155,6 +155,10 @@ mod tests { RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreUnit, }; use radroots_events::RadrootsNostrEventPtr; + use radroots_events::ids::{ + RadrootsInventoryBinId, RadrootsListingAddress, RadrootsOrderId, RadrootsOrderQuoteId, + RadrootsPublicKey, + }; use radroots_events::order::{ RadrootsOrderEconomicItem as TradeOrderEconomicItem, RadrootsOrderEconomicLine as TradeOrderEconomicLine, @@ -175,36 +179,26 @@ mod tests { use super::{BridgeOrderRequestParams, publish_order_request}; + const BUYER: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + const SELLER: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; + #[test] - fn canonicalize_order_request_sets_missing_buyer_and_seller_pubkeys() { - let order = canonicalize_order_request_for_signer( - base_order("", ""), - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - ) - .expect("canonicalize"); + fn canonicalize_order_request_accepts_matching_buyer_and_seller_pubkeys() { + let order = canonicalize_order_request_for_signer(base_order("", ""), BUYER) + .expect("canonicalize"); - assert_eq!( - order.buyer_pubkey, - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - ); - assert_eq!( - order.seller_pubkey, - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - ); + assert_eq!(order.buyer_pubkey, BUYER); + assert_eq!(order.seller_pubkey, SELLER); } #[test] fn canonicalize_order_request_rejects_items_with_zero_bin_count() { let mut order = base_order( - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "", + BUYER, "", ); order.items[0].bin_count = 0; - let err = canonicalize_order_request_for_signer( - order, - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - ) - .expect_err("zero bin count"); + let err = + canonicalize_order_request_for_signer(order, BUYER).expect_err("zero bin count"); assert!(err.to_string().contains("bin_count")); } @@ -225,9 +219,9 @@ mod tests { ) .expect("state"); let ctx = RpcContext::new(state, MethodRegistry::default()); - let session_id = insert_signer_session(&ctx, "session-1").await; + let (session_id, signer_pubkey) = insert_signer_session(&ctx, "session-1").await; let params = BridgeOrderRequestParams { - order: base_order("", ""), + order: base_order(signer_pubkey.as_str(), ""), listing_event: base_listing_event(), signer_session_id: Some(session_id.clone()), signer_authority: None, @@ -244,7 +238,7 @@ mod tests { let second = publish_order_request( ctx, BridgeOrderRequestParams { - order: base_order("", ""), + order: base_order(signer_pubkey.as_str(), ""), listing_event: base_listing_event(), signer_session_id: Some(session_id), signer_authority: None, @@ -274,11 +268,11 @@ mod tests { ) .expect("state"); let ctx = RpcContext::new(state, MethodRegistry::default()); - let session_id = insert_signer_session(&ctx, "session-1").await; + let (session_id, signer_pubkey) = insert_signer_session(&ctx, "session-1").await; publish_order_request( ctx.clone(), BridgeOrderRequestParams { - order: base_order("", ""), + order: base_order(signer_pubkey.as_str(), ""), listing_event: base_listing_event(), signer_session_id: Some(session_id.clone()), signer_authority: None, @@ -288,8 +282,8 @@ mod tests { .await .expect("first"); - let mut conflicting = base_order("", ""); - conflicting.order_id = "order-2".to_string(); + let mut conflicting = base_order(signer_pubkey.as_str(), ""); + conflicting.order_id = RadrootsOrderId::parse("order-2").expect("order id"); let err = publish_order_request( ctx, BridgeOrderRequestParams { @@ -338,7 +332,7 @@ mod tests { assert!(err.to_string().contains("requires signer_session_id")); } - async fn insert_signer_session(ctx: &RpcContext, session_id: &str) -> String { + async fn insert_signer_session(ctx: &RpcContext, session_id: &str) -> (String, String) { let signer_keys = RadrootsNostrKeys::generate(); let signer_pubkey = signer_keys.public_key().to_hex(); let remote_signer_pubkey = @@ -368,7 +362,7 @@ mod tests { signer_authority: None, }) .await; - session_id.to_string() + (session_id.to_string(), signer_pubkey) } fn base_listing_addr() -> &'static str { @@ -377,19 +371,19 @@ mod tests { fn base_listing_event() -> RadrootsNostrEventPtr { RadrootsNostrEventPtr { - id: "listing-event-1".to_string(), + id: "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".to_string(), relays: None, } } fn base_order_economics() -> TradeOrderEconomics { TradeOrderEconomics { - quote_id: "quote-1".to_string(), + quote_id: RadrootsOrderQuoteId::parse("quote-1").expect("quote id"), quote_version: 1, pricing_basis: TradePricingBasis::ListingEvent, currency: RadrootsCoreCurrency::USD, items: vec![TradeOrderEconomicItem { - bin_id: "bin-1".to_string(), + bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"), bin_count: 2, quantity_amount: RadrootsCoreDecimal::from(1u32), quantity_unit: RadrootsCoreUnit::Each, @@ -423,15 +417,21 @@ mod tests { fn base_order(buyer_pubkey: &str, seller_pubkey: &str) -> TradeOrder { TradeOrder { - order_id: "order-1".to_string(), - listing_addr: base_listing_addr().to_string(), - buyer_pubkey: buyer_pubkey.to_string(), - seller_pubkey: seller_pubkey.to_string(), + order_id: RadrootsOrderId::parse("order-1").expect("order id"), + listing_addr: RadrootsListingAddress::parse(base_listing_addr()) + .expect("listing address"), + buyer_pubkey: pubkey_or(BUYER, buyer_pubkey), + seller_pubkey: pubkey_or(SELLER, seller_pubkey), items: vec![TradeOrderItem { - bin_id: "bin-1".to_string(), + bin_id: RadrootsInventoryBinId::parse("bin-1").expect("bin id"), bin_count: 2, }], economics: base_order_economics(), } } + + fn pubkey_or(default: &str, value: &str) -> RadrootsPublicKey { + let value = if value.is_empty() { default } else { value }; + RadrootsPublicKey::parse(value).expect("pubkey") + } }