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:
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")
+ }
}