rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

commit a993592640d581277b5bf9c6eae36682cebbe2a2
parent a3ae988c8c2aae8abf304cf64b7a26631235815e
Author: triesap <tyson@radroots.org>
Date:   Tue,  3 Mar 2026 22:32:14 +0000

refactor: remove orphan modules and normalize feature manifest

Diffstat:
Asrc/features/mod.rs | 1+
Dsrc/features/trade_listing/domain/mod.rs | 1-
Dsrc/features/trade_listing/domain/pricing.rs | 71-----------------------------------------------------------------------
Dsrc/features/trade_listing/handlers/accept.rs | 148-------------------------------------------------------------------------------
Dsrc/features/trade_listing/handlers/conveyance.rs | 126-------------------------------------------------------------------------------
Dsrc/features/trade_listing/handlers/fulfillment.rs | 132-------------------------------------------------------------------------------
Dsrc/features/trade_listing/handlers/invoice.rs | 169-------------------------------------------------------------------------------
Dsrc/features/trade_listing/handlers/order.rs | 111-------------------------------------------------------------------------------
Dsrc/features/trade_listing/handlers/payment.rs | 123-------------------------------------------------------------------------------
Dsrc/features/trade_listing/handlers/receipt.rs | 126-------------------------------------------------------------------------------
Dsrc/infra/mod.rs | 1-
Msrc/lib.rs | 6+-----
12 files changed, 2 insertions(+), 1013 deletions(-)

diff --git a/src/features/mod.rs b/src/features/mod.rs @@ -0,0 +1 @@ +pub mod trade_listing; diff --git a/src/features/trade_listing/domain/mod.rs b/src/features/trade_listing/domain/mod.rs @@ -1 +0,0 @@ -pub mod pricing; diff --git a/src/features/trade_listing/domain/pricing.rs b/src/features/trade_listing/domain/pricing.rs @@ -1,71 +0,0 @@ -use radroots_events::listing::RadrootsListing; -use radroots_trade::prelude::price_ext::BinPricingTryExt; -use radroots_trade::prelude::stage::order::{ - TradeListingOrderRequestPayload, TradeListingOrderResult, -}; - -use crate::features::trade_listing::handlers::order::JobRequestOrderError; - -pub trait ListingOrderCalculator { - fn calculate_order( - &self, - order: &TradeListingOrderRequestPayload, - ) -> Result<TradeListingOrderResult, JobRequestOrderError>; -} - -impl ListingOrderCalculator for RadrootsListing { - fn calculate_order( - &self, - order: &TradeListingOrderRequestPayload, - ) -> Result<TradeListingOrderResult, JobRequestOrderError> { - if order.bin_id.trim().is_empty() { - return Err(JobRequestOrderError::Unsatisfiable(format!( - "requested bin id is empty" - ))); - } - - if order.bin_count == 0 { - return Err(JobRequestOrderError::Unsatisfiable( - "requested bin count must be greater than 0".to_string(), - )); - } - - let bin = self - .bins - .iter() - .find(|bin| bin.bin_id == order.bin_id) - .ok_or_else(|| { - JobRequestOrderError::Unsatisfiable(format!( - "requested bin {} not available", - order.bin_id - )) - })?; - - let out_price = bin.price_per_canonical_unit.clone(); - let out_subtotal = bin - .try_subtotal_for_count(order.bin_count) - .map_err(|err| { - JobRequestOrderError::Unsatisfiable(format!( - "failed to price requested bin: {err}" - )) - })?; - let out_total = bin - .try_total_for_count(order.bin_count) - .map_err(|err| { - JobRequestOrderError::Unsatisfiable(format!( - "failed to total requested bin: {err}" - )) - })?; - - let discounts_out = self.discounts.clone().unwrap_or_default(); - - Ok(TradeListingOrderResult { - bin_id: order.bin_id.clone(), - bin_count: order.bin_count, - price: out_price, - discounts: discounts_out, - subtotal: out_subtotal, - total: out_total, - }) - } -} diff --git a/src/features/trade_listing/handlers/accept.rs b/src/features/trade_listing/handlers/accept.rs @@ -1,148 +0,0 @@ -use radroots_nostr::prelude::{ - radroots_nostr_build_event, - radroots_nostr_fetch_event_by_id, - radroots_nostr_send_event, - RadrootsNostrClient, - RadrootsNostrEvent, - RadrootsNostrKind, - RadrootsNostrKeys, -}; -use radroots_events_codec::job::{result::encode::job_result_build_tags, traits::JobEventBorrow}; -use thiserror::Error; -use tracing::info; - -use radroots_events::{ - RadrootsNostrEventPtr, - job::JobPaymentRequest, - job_request::RadrootsJobInput, - job_result::RadrootsJobResult, - kinds::result_kind_for_request_kind, - tags::{TAG_D, TAG_E_ROOT}, -}; -use radroots_trade::{ - listing::{ - kinds::{KIND_TRADE_LISTING_ACCEPT_RES, KIND_TRADE_LISTING_ORDER_RES}, - tags::push_trade_listing_chain_tags, - }, - prelude::stage::accept::{TradeListingAcceptRequest, TradeListingAcceptResult}, -}; - -use crate::{ - adapters::nostr::event::NostrEventAdapter, - features::trade_listing::subscriber::{JobRequestCtx, JobRequestError}, -}; - -#[derive(Debug, Error)] -pub enum JobRequestAcceptError { - #[error("Failed to parse accept request: {0}")] - ParseRequest(String), - #[error("Failed to fetch reference event: {0}")] - FetchReference(String), - #[error("Reference event not found: {0}")] - MissingReference(String), - #[error("Unauthorized: accepting profile must own the listing")] - Unauthorized, - #[error("Order result not kind 6301 or listing mismatch")] - InvalidOrderResult, - #[error("Failed to send job response")] - ResponseSend(#[from] radroots_nostr::error::RadrootsNostrError), -} - -pub async fn handle_job_request_trade_accept( - event_job_request: RadrootsNostrEvent, - keys: RadrootsNostrKeys, - client: RadrootsNostrClient, - job_req: JobRequestCtx, - job_req_input: RadrootsJobInput, -) -> Result<(), JobRequestError> { - let ev = NostrEventAdapter::new(&event_job_request); - - let req: TradeListingAcceptRequest = serde_json::from_str(&job_req_input.data) - .map_err(|e| JobRequestAcceptError::ParseRequest(e.to_string()))?; - - let order_res_evt = radroots_nostr_fetch_event_by_id(client.clone(), &req.order_result_event_id) - .await - .map_err(|_| JobRequestAcceptError::FetchReference(req.order_result_event_id.clone()))?; - - let listing_evt = radroots_nostr_fetch_event_by_id(client.clone(), &req.listing_event_id) - .await - .map_err(|_| JobRequestAcceptError::FetchReference(req.listing_event_id.clone()))?; - - if listing_evt.pubkey != keys.public_key() { - return Err(JobRequestAcceptError::Unauthorized.into()); - } - - if order_res_evt.kind != RadrootsNostrKind::Custom(KIND_TRADE_LISTING_ORDER_RES) { - return Err(JobRequestAcceptError::InvalidOrderResult.into()); - } - let order_refs_listing = order_res_evt.tags.iter().any(|t| { - let s = t.as_slice(); - s.get(0).map(|k| k.as_str()) == Some(TAG_E_ROOT) - && s.get(1).map(String::as_str) == Some(req.listing_event_id.as_str()) - }); - if !order_refs_listing { - return Err(JobRequestAcceptError::InvalidOrderResult.into()); - } - - let accept_result = TradeListingAcceptResult { - listing_event_id: req.listing_event_id.clone(), - order_result_event_id: req.order_result_event_id.clone(), - accepted_by: keys.public_key().to_string(), - }; - let payload_json = serde_json::to_string(&accept_result)?; - - let result_kind = result_kind_for_request_kind(job_req.model.kind as u32) - .unwrap_or(job_req.model.kind as u32 + 1000); - debug_assert_eq!(result_kind as u16, KIND_TRADE_LISTING_ACCEPT_RES); - - let result_model = RadrootsJobResult { - kind: result_kind as u16, - request_event: RadrootsNostrEventPtr { - id: ev.raw_id().to_string(), - relays: None, - }, - request_json: Some(serde_json::to_string(&job_req.model)?), - inputs: job_req.model.inputs.clone(), - customer_pubkey: Some(ev.raw_author().to_string()), - payment: None::<JobPaymentRequest>, - content: Some(payload_json.clone()), - encrypted: false, - }; - - let mut tag_slices = job_result_build_tags(&result_model); - - let e_root = order_res_evt - .tags - .iter() - .find_map(|t| { - let s = t.as_slice(); - (s.get(0).map(|k| k.as_str()) == Some(TAG_E_ROOT)).then(|| s.get(1).cloned()) - }) - .flatten() - .unwrap_or_else(|| req.listing_event_id.clone()); - - let trade_id = order_res_evt - .tags - .iter() - .find_map(|t| { - let s = t.as_slice(); - (s.get(0).map(|k| k.as_str()) == Some(TAG_D)).then(|| s.get(1).cloned()) - }) - .flatten(); - - push_trade_listing_chain_tags( - &mut tag_slices, - e_root.clone(), - Some(req.order_result_event_id.clone()), - trade_id, - ); - - let builder = radroots_nostr_build_event(result_kind as u32, payload_json, tag_slices)?; - let job_result_event_id = radroots_nostr_send_event(client, builder).await?; - - info!( - "job request trade/accept ({}={}) result sent: {:?}", - TAG_E_ROOT, e_root, job_result_event_id - ); - Ok(()) -} diff --git a/src/features/trade_listing/handlers/conveyance.rs b/src/features/trade_listing/handlers/conveyance.rs @@ -1,126 +0,0 @@ -use radroots_nostr::prelude::{ - radroots_nostr_build_event, - radroots_nostr_fetch_event_by_id, - radroots_nostr_send_event, - RadrootsNostrClient, - RadrootsNostrEvent, - RadrootsNostrKind, - RadrootsNostrKeys, -}; -use radroots_events_codec::job::{result::encode::job_result_build_tags, traits::JobEventBorrow}; -use thiserror::Error; -use tracing::info; - -use radroots_events::{ - RadrootsNostrEventPtr, - job::JobPaymentRequest, - job_request::RadrootsJobInput, - job_result::RadrootsJobResult, - kinds::result_kind_for_request_kind, - tags::{TAG_D, TAG_E_ROOT}, -}; -use radroots_trade::{ - listing::{kinds::KIND_TRADE_LISTING_ACCEPT_RES, tags::push_trade_listing_chain_tags}, - prelude::stage::conveyance::{TradeListingConveyanceRequest, TradeListingConveyanceResult}, -}; - -use crate::{ - adapters::nostr::event::NostrEventAdapter, - features::trade_listing::subscriber::{JobRequestCtx, JobRequestError}, -}; - -#[derive(Debug, Error)] -pub enum JobRequestConveyanceError { - #[error("Failed to parse conveyance request: {0}")] - ParseRequest(String), - #[error("Failed to fetch reference event: {0}")] - FetchReference(String), - #[error("Reference event not found: {0}")] - MissingReference(String), - #[error("Invalid accept result kind")] - InvalidAcceptKind, - #[error("Failed to send job response")] - ResponseSend(#[from] radroots_nostr::error::RadrootsNostrError), -} - -pub async fn handle_job_request_trade_conveyance( - event_job_request: RadrootsNostrEvent, - _keys: RadrootsNostrKeys, - client: RadrootsNostrClient, - job_req: JobRequestCtx, - job_req_input: RadrootsJobInput, -) -> Result<(), JobRequestError> { - let ev = NostrEventAdapter::new(&event_job_request); - - let req: TradeListingConveyanceRequest = serde_json::from_str(&job_req_input.data) - .map_err(|e| JobRequestConveyanceError::ParseRequest(e.to_string()))?; - - let accept_evt = radroots_nostr_fetch_event_by_id(client.clone(), &req.accept_result_event_id) - .await - .map_err(|_| { - JobRequestConveyanceError::FetchReference(req.accept_result_event_id.clone()) - })?; - if accept_evt.kind != RadrootsNostrKind::Custom(KIND_TRADE_LISTING_ACCEPT_RES) { - return Err(JobRequestConveyanceError::InvalidAcceptKind.into()); - } - - let conv_res = TradeListingConveyanceResult { - verified: true, - method: req.method, - message: Some("conveyance method verified".into()), - }; - let payload_json = serde_json::to_string(&conv_res)?; - - let result_kind = result_kind_for_request_kind(job_req.model.kind as u32) - .unwrap_or(job_req.model.kind as u32 + 1000); - - let result_model = RadrootsJobResult { - kind: result_kind as u16, - request_event: RadrootsNostrEventPtr { - id: ev.raw_id().to_string(), - relays: None, - }, - request_json: Some(serde_json::to_string(&job_req.model)?), - inputs: job_req.model.inputs.clone(), - customer_pubkey: Some(ev.raw_author().to_string()), - payment: None::<JobPaymentRequest>, - content: Some(payload_json.clone()), - encrypted: false, - }; - - let mut tag_slices = job_result_build_tags(&result_model); - - let e_root = accept_evt - .tags - .iter() - .find_map(|t| { - let s = t.as_slice(); - (s.get(0).map(|k| k.as_str()) == Some(TAG_E_ROOT)).then(|| s.get(1).cloned()) - }) - .flatten(); - - let d_tag = accept_evt - .tags - .iter() - .find_map(|t| { - let s = t.as_slice(); - (s.get(0).map(|k| k.as_str()) == Some(TAG_D)).then(|| s.get(1).cloned()) - }) - .flatten(); - - push_trade_listing_chain_tags( - &mut tag_slices, - e_root.clone().unwrap_or_default(), - Some(req.accept_result_event_id.clone()), - d_tag, - ); - - let builder = radroots_nostr_build_event(result_kind as u32, payload_json, tag_slices)?; - let job_result_event_id = radroots_nostr_send_event(client, builder).await?; - - info!( - "job request trade/conveyance ({}={:?}) result sent: {:?}", - TAG_E_ROOT, e_root, job_result_event_id - ); - Ok(()) -} diff --git a/src/features/trade_listing/handlers/fulfillment.rs b/src/features/trade_listing/handlers/fulfillment.rs @@ -1,132 +0,0 @@ -use radroots_nostr::prelude::{ - radroots_nostr_build_event, - radroots_nostr_fetch_event_by_id, - radroots_nostr_send_event, - RadrootsNostrClient, - RadrootsNostrEvent, - RadrootsNostrKind, - RadrootsNostrKeys, -}; -use radroots_events_codec::job::{result::encode::job_result_build_tags, traits::JobEventBorrow}; -use thiserror::Error; -use tracing::info; - -use radroots_events::{ - RadrootsNostrEventPtr, - job_request::RadrootsJobInput, - job_result::RadrootsJobResult, - kinds::result_kind_for_request_kind, - tags::{TAG_D, TAG_E_ROOT}, -}; -use radroots_trade::{ - listing::tags::push_trade_listing_chain_tags, - prelude::{ - kinds::KIND_TRADE_LISTING_PAYMENT_RES, - stage::fulfillment::{ - TradeListingFulfillmentRequest, TradeListingFulfillmentResult, - TradeListingFulfillmentState, - }, - }, -}; - -use crate::{ - adapters::nostr::event::NostrEventAdapter, - features::trade_listing::subscriber::{JobRequestCtx, JobRequestError}, -}; - -#[derive(Debug, Error)] -pub enum JobRequestFulfillmentError { - #[error("Failed to parse fulfillment request: {0}")] - ParseRequest(String), - #[error("Failed to fetch reference event: {0}")] - FetchReference(String), - #[error("Reference event not found: {0}")] - MissingReference(String), - #[error("Payment result not kind 6305 or missing chain")] - InvalidPayment, - #[error("Failed to send job response")] - ResponseSend(#[from] radroots_nostr::error::RadrootsNostrError), -} - -pub async fn handle_job_request_trade_fulfillment( - event_job_request: RadrootsNostrEvent, - _keys: RadrootsNostrKeys, - client: RadrootsNostrClient, - job_req: JobRequestCtx, - job_req_input: RadrootsJobInput, -) -> Result<(), JobRequestError> { - let ev = NostrEventAdapter::new(&event_job_request); - - let req: TradeListingFulfillmentRequest = serde_json::from_str(&job_req_input.data) - .map_err(|e| JobRequestFulfillmentError::ParseRequest(e.to_string()))?; - - let payment_evt = radroots_nostr_fetch_event_by_id(client.clone(), &req.payment_result_event_id) - .await - .map_err(|_| { - JobRequestFulfillmentError::FetchReference(req.payment_result_event_id.clone()) - })?; - if payment_evt.kind != RadrootsNostrKind::Custom(KIND_TRADE_LISTING_PAYMENT_RES) { - return Err(JobRequestFulfillmentError::InvalidPayment.into()); - } - - let e_root = payment_evt - .tags - .iter() - .find_map(|t| { - let s = t.as_slice(); - (s.get(0).map(|k| k.as_str()) == Some(TAG_E_ROOT)).then(|| s.get(1).cloned()) - }) - .flatten() - .ok_or(JobRequestFulfillmentError::InvalidPayment)?; - - let d_tag = payment_evt - .tags - .iter() - .find_map(|t| { - let s = t.as_slice(); - (s.get(0).map(|k| k.as_str()) == Some(TAG_D)).then(|| s.get(1).cloned()) - }) - .flatten(); - - let status = TradeListingFulfillmentResult { - state: TradeListingFulfillmentState::Preparing, - tracking: None, - eta: None, - notes: Some("order accepted and paid; preparing shipment".into()), - }; - let payload_json = serde_json::to_string(&status)?; - - let result_kind = result_kind_for_request_kind(job_req.model.kind as u32) - .unwrap_or(job_req.model.kind as u32 + 1000); - - let result_model = RadrootsJobResult { - kind: result_kind as u16, - request_event: RadrootsNostrEventPtr { - id: ev.raw_id().to_string(), - relays: None, - }, - request_json: Some(serde_json::to_string(&job_req.model)?), - inputs: job_req.model.inputs.clone(), - customer_pubkey: Some(ev.raw_author().to_string()), - payment: None, - content: Some(payload_json.clone()), - encrypted: false, - }; - - let mut tag_slices = job_result_build_tags(&result_model); - push_trade_listing_chain_tags( - &mut tag_slices, - e_root.clone(), - Some(req.payment_result_event_id.clone()), - d_tag, - ); - - let builder = radroots_nostr_build_event(result_kind as u32, payload_json, tag_slices)?; - let job_result_event_id = radroots_nostr_send_event(client, builder).await?; - - info!( - "job request trade/fulfillment ({}={}) result sent: {:?}", - TAG_E_ROOT, e_root, job_result_event_id - ); - Ok(()) -} diff --git a/src/features/trade_listing/handlers/invoice.rs b/src/features/trade_listing/handlers/invoice.rs @@ -1,169 +0,0 @@ -use radroots_nostr::prelude::{ - radroots_nostr_build_event, - radroots_nostr_fetch_event_by_id, - radroots_nostr_send_event, - RadrootsNostrClient, - RadrootsNostrEvent, - RadrootsNostrKind, - RadrootsNostrKeys, -}; -use radroots_events_codec::job::{result::encode::job_result_build_tags, traits::JobEventBorrow}; -use thiserror::Error; -use tracing::info; - -use radroots_events::{ - RadrootsNostrEventPtr, - job::JobPaymentRequest, - job_request::{RadrootsJobInput, RadrootsJobParam}, - job_result::RadrootsJobResult, - kinds::result_kind_for_request_kind, - tags::{TAG_D, TAG_E_PREV, TAG_E_ROOT}, -}; -use radroots_trade::{ - listing::tags::push_trade_listing_chain_tags, - prelude::{ - kinds::{ - KIND_TRADE_LISTING_ACCEPT_RES, KIND_TRADE_LISTING_INVOICE_RES, - KIND_TRADE_LISTING_ORDER_RES, - }, - stage::invoice::{TradeListingInvoiceRequest, TradeListingInvoiceResult}, - }, -}; - -use crate::{ - adapters::nostr::event::NostrEventAdapter, - features::trade_listing::subscriber::{JobRequestCtx, JobRequestError}, -}; - -#[derive(Debug, Error)] -pub enum JobRequestInvoiceError { - #[error("Failed to parse invoice request: {0}")] - ParseRequest(String), - #[error("Failed to fetch reference event: {0}")] - FetchReference(String), - #[error("Reference event not found: {0}")] - MissingReference(String), - #[error("Accept result not kind 6302 or missing chain")] - InvalidAccept, - #[error("Failed to send job response")] - ResponseSend(#[from] radroots_nostr::error::RadrootsNostrError), -} - -fn param_lookup<'a>(params: &'a [RadrootsJobParam], key: &str) -> Option<&'a str> { - params - .iter() - .find(|p| p.key == key) - .map(|p| p.value.as_str()) -} - -pub async fn handle_job_request_trade_invoice( - event_job_request: RadrootsNostrEvent, - _keys: RadrootsNostrKeys, - client: RadrootsNostrClient, - job_req: JobRequestCtx, - job_req_input: RadrootsJobInput, -) -> Result<(), JobRequestError> { - let ev = NostrEventAdapter::new(&event_job_request); - - let req: TradeListingInvoiceRequest = serde_json::from_str(&job_req_input.data) - .map_err(|e| JobRequestInvoiceError::ParseRequest(e.to_string()))?; - - let accept_evt = radroots_nostr_fetch_event_by_id(client.clone(), &req.accept_result_event_id) - .await - .map_err(|_| JobRequestInvoiceError::FetchReference(req.accept_result_event_id.clone()))?; - if accept_evt.kind != RadrootsNostrKind::Custom(KIND_TRADE_LISTING_ACCEPT_RES) { - return Err(JobRequestInvoiceError::InvalidAccept.into()); - } - - let e_root = accept_evt - .tags - .iter() - .find_map(|t| { - let s = t.as_slice(); - (s.get(0).map(|k| k.as_str()) == Some(TAG_E_ROOT)).then(|| s.get(1).cloned()) - }) - .flatten() - .ok_or(JobRequestInvoiceError::InvalidAccept)?; - - let d_tag = accept_evt - .tags - .iter() - .find_map(|t| { - let s = t.as_slice(); - (s.get(0).map(|k| k.as_str()) == Some(TAG_D)).then(|| s.get(1).cloned()) - }) - .flatten(); - - let order_res_id = accept_evt - .tags - .iter() - .find_map(|t| { - let s = t.as_slice(); - (s.get(0).map(|k| k.as_str()) == Some(TAG_E_PREV)).then(|| s.get(1).cloned()) - }) - .flatten(); - - if let Some(prev_id) = &order_res_id { - if let Ok(prev_evt) = radroots_nostr_fetch_event_by_id(client.clone(), prev_id).await { - if prev_evt.kind != RadrootsNostrKind::Custom(KIND_TRADE_LISTING_ORDER_RES) {} - } - } - - let amount_sat = param_lookup(&job_req.model.params, "amount_sat") - .and_then(|v| v.parse::<u32>().ok()) - .or_else(|| { - param_lookup(&job_req.model.params, "amount_msat") - .and_then(|v| v.parse::<u64>().ok()) - .map(|msat| (msat / 1000) as u32) - }) - .unwrap_or(0); - - let bolt11 = param_lookup(&job_req.model.params, "bolt11").map(|s| s.to_string()); - let note = param_lookup(&job_req.model.params, "note").map(|s| s.to_string()); - let expires_at = - param_lookup(&job_req.model.params, "expires_at").and_then(|v| v.parse::<u32>().ok()); - - let invoice = TradeListingInvoiceResult { - total_sat: amount_sat, - bolt11: bolt11.clone(), - note, - expires_at, - }; - let payload_json = serde_json::to_string(&invoice)?; - - let result_kind = result_kind_for_request_kind(job_req.model.kind as u32) - .unwrap_or(job_req.model.kind as u32 + 1000); - debug_assert_eq!(result_kind as u16, KIND_TRADE_LISTING_INVOICE_RES); - - let result_model = RadrootsJobResult { - kind: result_kind as u16, - request_event: RadrootsNostrEventPtr { - id: ev.raw_id().to_string(), - relays: None, - }, - request_json: Some(serde_json::to_string(&job_req.model)?), - inputs: job_req.model.inputs.clone(), - customer_pubkey: Some(ev.raw_author().to_string()), - payment: Some(JobPaymentRequest { amount_sat, bolt11 }), - content: Some(payload_json.clone()), - encrypted: false, - }; - - let mut tag_slices = job_result_build_tags(&result_model); - - push_trade_listing_chain_tags( - &mut tag_slices, - e_root.clone(), - Some(req.accept_result_event_id.clone()), - d_tag, - ); - - let builder = radroots_nostr_build_event(result_kind as u32, payload_json, tag_slices)?; - let job_result_event_id = radroots_nostr_send_event(client, builder).await?; - - info!( - "job request trade/invoice ({}={}) result sent: {:?}", - TAG_E_ROOT, e_root, job_result_event_id - ); - Ok(()) -} diff --git a/src/features/trade_listing/handlers/order.rs b/src/features/trade_listing/handlers/order.rs @@ -1,111 +0,0 @@ -use radroots_nostr::prelude::{ - radroots_nostr_build_event, - radroots_nostr_fetch_event_by_id, - radroots_nostr_send_event, - RadrootsNostrClient, - RadrootsNostrEvent, - RadrootsNostrKeys, -}; -use radroots_events_codec::job::{result::encode::job_result_build_tags, traits::JobEventBorrow}; -use thiserror::Error; -use tracing::info; - -use radroots_events::{ - RadrootsNostrEventPtr, - job::JobPaymentRequest, - job_request::RadrootsJobInput, - job_result::RadrootsJobResult, - kinds::result_kind_for_request_kind, - listing::RadrootsListing, -}; -use radroots_trade::prelude::{ - kinds::KIND_TRADE_LISTING_ORDER_RES, stage::order::TradeListingOrderRequest, tags, -}; - -use crate::{ - adapters::nostr::event::NostrEventAdapter, - features::trade_listing::{ - domain::pricing::ListingOrderCalculator, - subscriber::{JobRequestCtx, JobRequestError}, - }, -}; - -#[derive(Debug, Error)] -pub enum JobRequestOrderError { - #[error("Failed to parse reference event: {0}")] - ParseReference(String), - #[error("Failed to fetch reference event: {0}")] - FetchReference(String), - #[error("Reference event not found: {0}")] - MissingReference(String), - #[error("Reference event does not meet request requirements: {0}")] - MissingRequested(String), - #[error("Failed to send job response")] - ResponseSend(#[from] radroots_nostr::error::RadrootsNostrError), - #[error("Request cannot be satisfied: {0}")] - Unsatisfiable(String), -} - -pub async fn handle_job_request_trade_order( - event_job_request: RadrootsNostrEvent, - _keys: RadrootsNostrKeys, - client: RadrootsNostrClient, - job_req: JobRequestCtx, - job_req_input: RadrootsJobInput, -) -> Result<(), JobRequestError> { - let ev = NostrEventAdapter::new(&event_job_request); - - let order_data: TradeListingOrderRequest = serde_json::from_str(&job_req_input.data) - .map_err(|e| JobRequestOrderError::ParseReference(e.to_string()))?; - - let ref_id = &order_data.event.id; - let ref_event = radroots_nostr_fetch_event_by_id(client.clone(), ref_id) - .await - .map_err(|_| JobRequestOrderError::FetchReference(ref_id.clone()))?; - - let listing: RadrootsListing = serde_json::from_str(&ref_event.content).map_err(|_| { - JobRequestOrderError::ParseReference(format!("invalid listing content for {}", ref_id)) - })?; - - let order_result = listing.calculate_order(&order_data.payload)?; - - let result_kind = result_kind_for_request_kind(job_req.model.kind as u32) - .unwrap_or(job_req.model.kind as u32 + 1000); - debug_assert_eq!(result_kind as u16, KIND_TRADE_LISTING_ORDER_RES as u16); - - let payload_json = serde_json::to_string(&order_result)?; - - let result_model = RadrootsJobResult { - kind: result_kind as u16, - request_event: RadrootsNostrEventPtr { - id: ev.raw_id().to_string(), - relays: None, - }, - request_json: Some(serde_json::to_string(&job_req.model)?), - inputs: job_req.model.inputs.clone(), - customer_pubkey: Some(ev.raw_author().to_string()), - payment: None::<JobPaymentRequest>, - content: Some(payload_json.clone()), - encrypted: false, - }; - - let mut tag_slices = job_result_build_tags(&result_model); - - let e_root = ref_event.id.to_hex(); - let trade_id = format!("trade:{}:{}", e_root, event_job_request.id.to_hex()); - tags::push_trade_listing_chain_tags( - &mut tag_slices, - e_root.clone(), - None::<String>, - Some(trade_id.clone()), - ); - - let builder = radroots_nostr_build_event(result_kind as u32, payload_json, tag_slices)?; - let job_result_event_id = radroots_nostr_send_event(client, builder).await?; - - info!( - "job request trade/order (e_root={}) result sent: {:?}", - e_root, job_result_event_id - ); - Ok(()) -} diff --git a/src/features/trade_listing/handlers/payment.rs b/src/features/trade_listing/handlers/payment.rs @@ -1,123 +0,0 @@ -use radroots_nostr::prelude::{ - radroots_nostr_build_event, - radroots_nostr_fetch_event_by_id, - radroots_nostr_send_event, - RadrootsNostrClient, - RadrootsNostrEvent, - RadrootsNostrKind, - RadrootsNostrKeys, -}; -use radroots_events_codec::job::{result::encode::job_result_build_tags, traits::JobEventBorrow}; -use thiserror::Error; -use tracing::info; - -use radroots_events::{ - RadrootsNostrEventPtr, - job_request::RadrootsJobInput, - job_result::RadrootsJobResult, - kinds::result_kind_for_request_kind, - tags::{TAG_D, TAG_E_ROOT}, -}; -use radroots_trade::prelude::{ - kinds::KIND_TRADE_LISTING_INVOICE_RES, - stage::payment::{TradeListingPaymentProofRequest, TradeListingPaymentResult}, - tags::push_trade_listing_chain_tags, -}; - -use crate::{ - adapters::nostr::event::NostrEventAdapter, - features::trade_listing::subscriber::{JobRequestCtx, JobRequestError}, -}; - -#[derive(Debug, Error)] -pub enum JobRequestPaymentError { - #[error("Failed to parse payment request: {0}")] - ParseRequest(String), - #[error("Failed to fetch reference event: {0}")] - FetchReference(String), - #[error("Reference event not found: {0}")] - MissingReference(String), - #[error("Invoice result not kind 6304 or missing chain")] - InvalidInvoice, - #[error("Failed to send job response")] - ResponseSend(#[from] radroots_nostr::error::RadrootsNostrError), -} - -pub async fn handle_job_request_trade_payment( - event_job_request: RadrootsNostrEvent, - _keys: RadrootsNostrKeys, - client: RadrootsNostrClient, - job_req: JobRequestCtx, - job_req_input: RadrootsJobInput, -) -> Result<(), JobRequestError> { - let ev = NostrEventAdapter::new(&event_job_request); - - let req: TradeListingPaymentProofRequest = serde_json::from_str(&job_req_input.data) - .map_err(|e| JobRequestPaymentError::ParseRequest(e.to_string()))?; - - let invoice_evt = radroots_nostr_fetch_event_by_id(client.clone(), &req.invoice_result_event_id) - .await - .map_err(|_| JobRequestPaymentError::FetchReference(req.invoice_result_event_id.clone()))?; - if invoice_evt.kind != RadrootsNostrKind::Custom(KIND_TRADE_LISTING_INVOICE_RES) { - return Err(JobRequestPaymentError::InvalidInvoice.into()); - } - - let e_root = invoice_evt - .tags - .iter() - .find_map(|t| { - let s = t.as_slice(); - (s.get(0).map(|k| k.as_str()) == Some(TAG_E_ROOT)).then(|| s.get(1).cloned()) - }) - .flatten() - .ok_or(JobRequestPaymentError::InvalidInvoice)?; - - let d_tag = invoice_evt - .tags - .iter() - .find_map(|t| { - let s = t.as_slice(); - (s.get(0).map(|k| k.as_str()) == Some(TAG_D)).then(|| s.get(1).cloned()) - }) - .flatten(); - - let ack = TradeListingPaymentResult { - verified: true, - message: Some("payment proof accepted".into()), - }; - let payload_json = serde_json::to_string(&ack)?; - - let result_kind = result_kind_for_request_kind(job_req.model.kind as u32) - .unwrap_or(job_req.model.kind as u32 + 1000); - - let result_model = RadrootsJobResult { - kind: result_kind as u16, - request_event: RadrootsNostrEventPtr { - id: ev.raw_id().to_string(), - relays: None, - }, - request_json: Some(serde_json::to_string(&job_req.model)?), - inputs: job_req.model.inputs.clone(), - customer_pubkey: Some(ev.raw_author().to_string()), - payment: None, - content: Some(payload_json.clone()), - encrypted: false, - }; - - let mut tag_slices = job_result_build_tags(&result_model); - push_trade_listing_chain_tags( - &mut tag_slices, - e_root.clone(), - Some(req.invoice_result_event_id.clone()), - d_tag, - ); - - let builder = radroots_nostr_build_event(result_kind as u32, payload_json, tag_slices)?; - let job_result_event_id = radroots_nostr_send_event(client, builder).await?; - - info!( - "job request trade/payment ({}={}) result sent: {:?}", - TAG_E_ROOT, e_root, job_result_event_id - ); - Ok(()) -} diff --git a/src/features/trade_listing/handlers/receipt.rs b/src/features/trade_listing/handlers/receipt.rs @@ -1,126 +0,0 @@ -use radroots_nostr::prelude::{ - radroots_nostr_build_event, - radroots_nostr_fetch_event_by_id, - radroots_nostr_send_event, - RadrootsNostrClient, - RadrootsNostrEvent, - RadrootsNostrKind, - RadrootsNostrKeys, -}; -use radroots_events_codec::job::{result::encode::job_result_build_tags, traits::JobEventBorrow}; -use thiserror::Error; -use tracing::info; - -use radroots_events::{ - RadrootsNostrEventPtr, - job_request::RadrootsJobInput, - job_result::RadrootsJobResult, - kinds::result_kind_for_request_kind, - tags::{TAG_D, TAG_E_ROOT}, -}; -use radroots_trade::prelude::{ - kinds::KIND_TRADE_LISTING_FULFILL_RES, - stage::receipt::{TradeListingReceiptRequest, TradeListingReceiptResult}, - tags::push_trade_listing_chain_tags, -}; - -use crate::{ - adapters::nostr::event::NostrEventAdapter, - features::trade_listing::subscriber::{JobRequestCtx, JobRequestError}, -}; - -#[derive(Debug, Error)] -pub enum JobRequestReceiptError { - #[error("Failed to parse receipt request: {0}")] - ParseRequest(String), - #[error("Failed to fetch reference event: {0}")] - FetchReference(String), - #[error("Reference event not found: {0}")] - MissingReference(String), - #[error("Fulfillment result not kind 6306 or missing chain")] - InvalidFulfillment, - #[error("Failed to send job response")] - ResponseSend(#[from] radroots_nostr::error::RadrootsNostrError), -} - -pub async fn handle_job_request_trade_receipt( - event_job_request: RadrootsNostrEvent, - _keys: RadrootsNostrKeys, - client: RadrootsNostrClient, - job_req: JobRequestCtx, - job_req_input: RadrootsJobInput, -) -> Result<(), JobRequestError> { - let ev = NostrEventAdapter::new(&event_job_request); - - let req: TradeListingReceiptRequest = serde_json::from_str(&job_req_input.data) - .map_err(|e| JobRequestReceiptError::ParseRequest(e.to_string()))?; - - let fulfill_evt = - radroots_nostr_fetch_event_by_id(client.clone(), &req.fulfillment_result_event_id) - .await - .map_err(|_| { - JobRequestReceiptError::FetchReference(req.fulfillment_result_event_id.clone()) - })?; - if fulfill_evt.kind != RadrootsNostrKind::Custom(KIND_TRADE_LISTING_FULFILL_RES) { - return Err(JobRequestReceiptError::InvalidFulfillment.into()); - } - - let e_root = fulfill_evt - .tags - .iter() - .find_map(|t| { - let s = t.as_slice(); - (s.get(0).map(|k| k.as_str()) == Some(TAG_E_ROOT)).then(|| s.get(1).cloned()) - }) - .flatten() - .ok_or(JobRequestReceiptError::InvalidFulfillment)?; - - let d_tag = fulfill_evt - .tags - .iter() - .find_map(|t| { - let s = t.as_slice(); - (s.get(0).map(|k| k.as_str()) == Some(TAG_D)).then(|| s.get(1).cloned()) - }) - .flatten(); - - let ack = TradeListingReceiptResult { - acknowledged: true, - at: event_job_request.created_at.as_u64() as u32, - }; - let payload_json = serde_json::to_string(&ack)?; - - let result_kind = result_kind_for_request_kind(job_req.model.kind as u32) - .unwrap_or(job_req.model.kind as u32 + 1000); - - let result_model = RadrootsJobResult { - kind: result_kind as u16, - request_event: RadrootsNostrEventPtr { - id: ev.raw_id().to_string(), - relays: None, - }, - request_json: Some(serde_json::to_string(&job_req.model)?), - inputs: job_req.model.inputs.clone(), - customer_pubkey: Some(ev.raw_author().to_string()), - payment: None, - content: Some(payload_json.clone()), - encrypted: false, - }; - - let mut tag_slices = job_result_build_tags(&result_model); - push_trade_listing_chain_tags( - &mut tag_slices, - e_root.clone(), - Some(req.fulfillment_result_event_id.clone()), - d_tag, - ); - - let builder = radroots_nostr_build_event(result_kind as u32, payload_json, tag_slices)?; - let job_result_event_id = radroots_nostr_send_event(client, builder).await?; - - info!( - "job request trade/receipt ({}={}) result sent: {:?}", - TAG_E_ROOT, e_root, job_result_event_id - ); - Ok(()) -} diff --git a/src/infra/mod.rs b/src/infra/mod.rs @@ -1 +0,0 @@ -#![forbid(unsafe_code)] diff --git a/src/lib.rs b/src/lib.rs @@ -3,13 +3,9 @@ pub mod adapters; pub mod cli; pub mod config; -pub mod infra; +pub mod features; pub mod rhi; -pub mod features { - pub mod trade_listing; -} - pub use cli::Args as cli_args; use anyhow::Result;