From 44115e0393212a6f346fbfb93bce8672221e9739 Mon Sep 17 00:00:00 2001 From: Mathih13 Date: Thu, 3 Sep 2026 19:19:29 +0200 Subject: [PATCH] refactor(auction): typed refusals across the tier contract The module tagged auction refusals as bracketed strings and the gateway matched substrings of rendered error text. A reducer timeout or any unrelated failure fell through that match and reached the client as a gameplay refusal. One shared AuctionRefusal enum now crosses the reducer edge as a tag. The module's listing and bid cores return it directly; the gateway classifies on ReducerCallError first and parses the tag only from a rejected reducer, so a timeout ends the session instead of posing as a refusal. Co-Authored-By: Claude Fable 5.1 --- crates/lyracore-shared/src/auction.rs | 51 +++- gateway/src/stdb/reducers.rs | 109 ++++----- gateway/src/world/handlers/auction.rs | 170 ++++++++------ module/src/auction.rs | 324 +++++++++++++------------- 4 files changed, 353 insertions(+), 301 deletions(-) diff --git a/crates/lyracore-shared/src/auction.rs b/crates/lyracore-shared/src/auction.rs index 107df6539..8affd1a12 100644 --- a/crates/lyracore-shared/src/auction.rs +++ b/crates/lyracore-shared/src/auction.rs @@ -23,11 +23,38 @@ pub fn minimum_next_bid(start_bid: u32, current_bid: u32) -> Option { } } -/// Stable reducer-boundary result tags for listing gameplay refusals. -pub mod result { - pub const DATABASE: &str = "AUCTION_DATABASE"; - pub const ITEM_NOT_FOUND: &str = "AUCTION_ITEM_NOT_FOUND"; - pub const NOT_ENOUGH_MONEY: &str = "AUCTION_NOT_ENOUGH_MONEY"; +/// Why the Module refused an auction Durable Request. The tag is the whole reducer error text, +/// so neither tier matches on human prose. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AuctionRefusal { + ItemNotFound, + NotEnoughMoney, + InvalidTerms, + Database, +} + +impl AuctionRefusal { + pub const ALL: [Self; 4] = [ + Self::ItemNotFound, + Self::NotEnoughMoney, + Self::InvalidTerms, + Self::Database, + ]; + + pub fn as_tag(self) -> &'static str { + match self { + Self::ItemNotFound => "auction:item_not_found", + Self::NotEnoughMoney => "auction:not_enough_money", + Self::InvalidTerms => "auction:invalid_terms", + Self::Database => "auction:database", + } + } + + pub fn parse_tag(tag: &str) -> Option { + Self::ALL + .into_iter() + .find(|refusal| refusal.as_tag() == tag) + } } /// Stable terminal outcome codes shared by bid Hold and decision rows. @@ -43,6 +70,20 @@ pub mod bid_outcome { #[cfg(test)] mod tests { + use super::AuctionRefusal; + + #[test] + fn every_refusal_tag_round_trips() { + for refusal in AuctionRefusal::ALL { + assert_eq!(AuctionRefusal::parse_tag(refusal.as_tag()), Some(refusal)); + } + assert_eq!(AuctionRefusal::parse_tag("auction:"), None); + assert_eq!( + AuctionRefusal::parse_tag("gw_auction_hold_bid reducer timed out after 10s"), + None + ); + } + #[test] fn bid_increment_is_five_percent_rounded_up() { assert_eq!(super::bid_increment(0), 0); diff --git a/gateway/src/stdb/reducers.rs b/gateway/src/stdb/reducers.rs index 429550492..615ca93ce 100644 --- a/gateway/src/stdb/reducers.rs +++ b/gateway/src/stdb/reducers.rs @@ -11,6 +11,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use super::bindings::*; use super::connection::{call_reducer, recv_reducer_on, reducer_refusal_reason, Coordinator}; use super::views::entity_view; +use lyracore_shared::auction::AuctionRefusal; static NEXT_TAXI_REQUEST_ID: OnceLock = OnceLock::new(); @@ -276,8 +277,8 @@ impl Coordinator { match result { Ok(auction_id) => Ok(CreateAuctionOutcome::Created { auction_id }), - Err(error) => match map_auction_refusal(&error) { - Some(outcome) => Ok(outcome), + Err(error) => match auction_refusal(&error) { + Some(refusal) => Ok(refusal.into()), None => Err(error), }, } @@ -501,8 +502,8 @@ impl Coordinator { }; match result { Ok(hold) => bid_outcome(&hold), - Err(error) => match map_bid_refusal(&error) { - Some(outcome) => Ok(outcome), + Err(error) => match auction_refusal(&error) { + Some(refusal) => Ok(refusal.into()), None => Err(error), }, } @@ -589,7 +590,9 @@ impl Coordinator { decision: &AuctionBidDecision, ) -> Result<()> { if !bid_payload_matches(hold, decision) { - return Err(anyhow!("auction bid decision payload does not match its Hold")); + return Err(anyhow!( + "auction bid decision payload does not match its Hold" + )); } call_reducer!( self.0.call_pipe().conn.reducers, @@ -681,9 +684,7 @@ impl Coordinator { .game_auction_bid_hold() .operation_id() .find(&operation_id) - .filter(|hold| { - hold.outcome != lyracore_shared::auction::bid_outcome::PENDING - }) + .filter(|hold| hold.outcome != lyracore_shared::auction::bid_outcome::PENDING) }) } @@ -3058,18 +3059,10 @@ fn next_auction_operation_id() -> Result { } } -fn map_auction_refusal(error: &anyhow::Error) -> Option { - use crate::world::CreateAuctionOutcome; - let text = format!("{error:#}"); - if text.contains(lyracore_shared::auction::result::ITEM_NOT_FOUND) { - Some(CreateAuctionOutcome::ItemNotFound) - } else if text.contains(lyracore_shared::auction::result::NOT_ENOUGH_MONEY) { - Some(CreateAuctionOutcome::NotEnoughMoney) - } else if text.contains(lyracore_shared::auction::result::DATABASE) { - Some(CreateAuctionOutcome::Database) - } else { - None - } +/// The Module's typed auction Refusal. Only a reducer the Module rejected carries a tag; a timeout, +/// transport, or SDK failure stays an error with an unknown outcome. +fn auction_refusal(error: &anyhow::Error) -> Option { + reducer_refusal_reason(error).and_then(AuctionRefusal::parse_tag) } fn bid_payload_matches(hold: &AuctionBidHold, decision: &AuctionBidDecision) -> bool { @@ -3114,63 +3107,49 @@ fn bid_outcome(hold: &AuctionBidHold) -> Result { bid_outcome::BID_INCREMENT => PlaceBidOutcome::BidIncrement, bid_outcome::BID_OWN => PlaceBidOutcome::BidOwn, bid_outcome::DATABASE => PlaceBidOutcome::Database, - outcome => return Err(anyhow!("auction bid Hold has non-terminal outcome {outcome}")), + outcome => { + return Err(anyhow!( + "auction bid Hold has non-terminal outcome {outcome}" + )) + } }) } -fn map_bid_refusal(error: &anyhow::Error) -> Option { - use crate::world::PlaceBidOutcome; - let text = format!("{error:#}"); - if text.contains(lyracore_shared::auction::result::NOT_ENOUGH_MONEY) { - Some(PlaceBidOutcome::NotEnoughMoney) - } else if text.contains(lyracore_shared::auction::result::DATABASE) { - Some(PlaceBidOutcome::Database) - } else { - None - } -} - #[cfg(test)] mod auction_reducer_tests { use super::*; - use crate::world::CreateAuctionOutcome; + use crate::stdb::connection::ReducerCallError; #[test] - fn reducer_tags_map_to_typed_listing_outcomes() { - let cases = [ - ( - lyracore_shared::auction::result::ITEM_NOT_FOUND, - CreateAuctionOutcome::ItemNotFound, - ), - ( - lyracore_shared::auction::result::NOT_ENOUGH_MONEY, - CreateAuctionOutcome::NotEnoughMoney, - ), - ( - lyracore_shared::auction::result::DATABASE, - CreateAuctionOutcome::Database, - ), - ]; - for (tag, expected) in cases { - assert_eq!(map_auction_refusal(&anyhow!("[{tag}] refused")), Some(expected)); + fn only_a_rejected_reducer_carries_a_typed_refusal() { + for refusal in AuctionRefusal::ALL { + let error = anyhow::Error::from(ReducerCallError::Rejected { + operation: "gw_auction_hold_listing".to_string(), + reason: refusal.as_tag().to_string(), + }) + .context("listing phase 1"); + assert_eq!(auction_refusal(&error), Some(refusal)); } - assert_eq!(map_auction_refusal(&anyhow!("transport disconnected")), None); - assert_eq!( - map_bid_refusal(&anyhow!( - "[{}] refused", - lyracore_shared::auction::result::NOT_ENOUGH_MONEY + let not_refusals = [ + anyhow::Error::from(ReducerCallError::fatal( + "gw_auction_hold_listing reducer timed out after 10s".to_string(), )), - Some(crate::world::PlaceBidOutcome::NotEnoughMoney) - ); - assert_eq!( - map_bid_refusal(&anyhow!( - "[{}] refused", - lyracore_shared::auction::result::DATABASE + anyhow::Error::from(ReducerCallError::fatal( + "gw_auction_hold_bid reducer failed: transport disconnected".to_string(), )), - Some(crate::world::PlaceBidOutcome::Database) - ); - assert_eq!(map_bid_refusal(&anyhow!("transport disconnected")), None); + anyhow::Error::from(ReducerCallError::Rejected { + operation: "gw_auction_hold_bid".to_string(), + reason: "operator only".to_string(), + }), + anyhow!( + "wrapped text that mentions {}", + AuctionRefusal::Database.as_tag() + ), + ]; + for error in not_refusals { + assert_eq!(auction_refusal(&error), None, "{error:#}"); + } } #[test] diff --git a/gateway/src/world/handlers/auction.rs b/gateway/src/world/handlers/auction.rs index 9ac84e452..3cd244159 100644 --- a/gateway/src/world/handlers/auction.rs +++ b/gateway/src/world/handlers/auction.rs @@ -1,6 +1,7 @@ //! Auction-house protocol family. use super::super::*; +use lyracore_shared::auction::AuctionRefusal; use spacetimedb_sdk::Table; #[derive(Clone, Copy, Debug, Default, PartialEq)] @@ -52,6 +53,17 @@ pub(crate) enum CreateAuctionOutcome { Database, } +// The vanilla client has no code for invalid terms; it reads as the generic database failure. +impl From for CreateAuctionOutcome { + fn from(refusal: AuctionRefusal) -> Self { + match refusal { + AuctionRefusal::ItemNotFound => Self::ItemNotFound, + AuctionRefusal::NotEnoughMoney => Self::NotEnoughMoney, + AuctionRefusal::InvalidTerms | AuctionRefusal::Database => Self::Database, + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct PlaceBidRequest { pub(crate) actor_guid: u64, @@ -78,6 +90,16 @@ pub(crate) enum PlaceBidOutcome { Database, } +impl From for PlaceBidOutcome { + fn from(refusal: AuctionRefusal) -> Self { + match refusal { + AuctionRefusal::ItemNotFound => Self::ItemNotFound, + AuctionRefusal::NotEnoughMoney => Self::NotEnoughMoney, + AuctionRefusal::InvalidTerms | AuctionRefusal::Database => Self::Database, + } + } +} + #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct AuctionBrowseRequest { pub(crate) auctioneer_guid: u64, @@ -317,20 +339,8 @@ fn validated_auction_player_guid( let Some(player_guid) = player.self_guid else { return Ok(None); }; - let entities = match store.auction_entities(player_guid, auctioneer_guid) { - Ok(entities) => entities, - Err(error) - if classify_auction_action_error(&error) - == AuctionActionErrorClass::GameplayRefusal => - { - log::debug!( - "world: auctioneer {auctioneer_guid} interaction unavailable for player \ - {player_guid}: {error}" - ); - None - } - Err(error) => return Err(error), - }; + // A missing entity or house is `None`; a failed Durable Read is a failure, not a Refusal. + let entities = store.auction_entities(player_guid, auctioneer_guid)?; Ok(entities .filter(|interaction| interaction_allowed(*interaction)) .map(|interaction| (player_guid, interaction.house))) @@ -400,24 +410,6 @@ fn bid_result(auction_id: u32, outcome: PlaceBidOutcome) -> AuctionActionOutcome } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum AuctionActionErrorClass { - GameplayRefusal, - Fatal, -} - -fn classify_auction_action_error(error: &anyhow::Error) -> AuctionActionErrorClass { - if error.chain().any(|cause| { - let text = cause.to_string(); - text.contains("reducer transport disconnected") - || (text.contains("realm-core database") && text.contains("is not connected")) - }) { - AuctionActionErrorClass::Fatal - } else { - AuctionActionErrorClass::GameplayRefusal - } -} - pub(crate) fn dispatch_auction_action( store: &St, player: AuctionActionPlayer, @@ -467,22 +459,15 @@ pub(crate) fn dispatch_auction_action( else { return Ok(bid_result(auction_id, PlaceBidOutcome::Database)); }; - let outcome = match store.place_bid(PlaceBidRequest { + // A Refusal arrives as an outcome. An error is a failure with an unknown durable + // result, so it ends the session instead of posing as a gameplay answer. + let outcome = store.place_bid(PlaceBidRequest { actor_guid: player_guid, auctioneer_guid, auction_id, offer: message.price.as_int(), house_id: house.id, - }) { - Ok(outcome) => outcome, - Err(error) - if classify_auction_action_error(&error) - == AuctionActionErrorClass::GameplayRefusal => - { - PlaceBidOutcome::Database - } - Err(error) => return Err(error), - }; + })?; return Ok(bid_result(auction_id, outcome)); } ClientOpcodeMessage::CMSG_AUCTION_SELL_ITEM(message) => { @@ -492,7 +477,7 @@ pub(crate) fn dispatch_auction_action( else { return Ok(create_result(CreateAuctionOutcome::Database)); }; - let outcome = match store.create_auction(CreateAuctionRequest { + let outcome = store.create_auction(CreateAuctionRequest { actor_guid: player_guid, auctioneer_guid, item_guid: message.item.guid(), @@ -500,16 +485,7 @@ pub(crate) fn dispatch_auction_action( buyout: message.buyout, duration_minutes: message.auction_duration_in_minutes, house_id: house.id, - }) { - Ok(outcome) => outcome, - Err(error) - if classify_auction_action_error(&error) - == AuctionActionErrorClass::GameplayRefusal => - { - CreateAuctionOutcome::Database - } - Err(error) => return Err(error), - }; + })?; return Ok(create_result(outcome)); } other => return Ok(AuctionActionOutcome::PassThrough(other)), @@ -968,6 +944,13 @@ mod tests { } } + fn session_error(result: Result>, what: &str) -> anyhow::Error { + match result { + Ok(_) => panic!("{what} must end the session"), + Err(error) => error, + } + } + fn bid_outbound(store: &InMemoryAuctionActions) -> Result> { match dispatch_auction_action( store, @@ -1248,6 +1231,67 @@ mod tests { )); } + #[test] + fn every_module_refusal_has_one_client_result_code() { + use SMSG_AUCTION_COMMAND_RESULT_AuctionCommandResult as Bid; + use SMSG_AUCTION_COMMAND_RESULT_AuctionCommandResultTwo as Sell; + let expected = |refusal| match refusal { + AuctionRefusal::ItemNotFound => (Sell::ErrItemNotFound, Bid::ErrItemNotFound), + AuctionRefusal::NotEnoughMoney => (Sell::ErrNotEnoughMoney, Bid::ErrNotEnoughMoney), + AuctionRefusal::InvalidTerms | AuctionRefusal::Database => { + (Sell::ErrDatabase, Bid::ErrDatabase) + } + }; + for refusal in AuctionRefusal::ALL { + let (sell_code, bid_code) = expected(refusal); + + let store = store_with(Some(valid_interaction())); + *store.create_result.lock().unwrap() = Ok(refusal.into()); + let outbound = sell_outbound(&store).unwrap(); + assert!( + matches!( + outbound.as_slice(), + [Outbound::One(ServerOpcodeMessage::SMSG_AUCTION_COMMAND_RESULT(message))] + if message.action + == SMSG_AUCTION_COMMAND_RESULT_AuctionCommandAction::Started { + result2: sell_code, + } + ), + "{refusal:?}" + ); + + let store = store_with(Some(valid_interaction())); + *store.bid_result.lock().unwrap() = Ok(refusal.into()); + let outbound = bid_outbound(&store).unwrap(); + assert!( + matches!( + outbound.as_slice(), + [Outbound::One(ServerOpcodeMessage::SMSG_AUCTION_COMMAND_RESULT(message))] + if message.action + == SMSG_AUCTION_COMMAND_RESULT_AuctionCommandAction::BidPlaced { + result: bid_code, + } + ), + "{refusal:?}" + ); + } + } + + #[test] + fn a_reducer_timeout_is_not_answered_as_a_refusal() { + let store = store_with(Some(valid_interaction())); + *store.create_result.lock().unwrap() = + Err("gw_auction_hold_listing reducer timed out after 10s".to_string()); + let error = session_error(sell_outbound(&store), "an unknown listing outcome"); + assert!(error.to_string().contains("timed out")); + + let store = store_with(Some(valid_interaction())); + *store.bid_result.lock().unwrap() = + Err("gw_auction_hold_bid reducer timed out after 10s".to_string()); + let error = session_error(bid_outbound(&store), "an unknown bid outcome"); + assert!(error.to_string().contains("timed out")); + } + #[test] fn sell_transport_failure_is_fatal() { let store = store_with(Some(valid_interaction())); @@ -1409,17 +1453,13 @@ mod tests { } #[test] - fn gameplay_read_failures_are_refusals_but_a_dead_transport_is_fatal() { - assert!(hello_outbound(&store_error("auction state unavailable")) - .unwrap() - .is_empty()); - - let error = match hello_outbound(&store_error( + fn a_failed_interaction_read_is_fatal() { + for message in [ + "auction state unavailable", "auction interaction reducer transport disconnected: channel closed", - )) { - Ok(_) => panic!("a dead reducer transport must end the session"), - Err(error) => error, - }; - assert!(error.to_string().contains("transport disconnected")); + ] { + let error = session_error(hello_outbound(&store_error(message)), "a failed read"); + assert_eq!(error.to_string(), message); + } } } diff --git a/module/src/auction.rs b/module/src/auction.rs index 894f6cd71..d658216f9 100644 --- a/module/src/auction.rs +++ b/module/src/auction.rs @@ -6,6 +6,7 @@ use lyracore_shared::auction::bid_outcome::{ ACCEPTED as BID_ACCEPTED, BID_INCREMENT, BID_OWN, DATABASE as BID_DATABASE, HIGHER_BID as BID_HIGHER, ITEM_NOT_FOUND as BID_ITEM_NOT_FOUND, PENDING as BID_PENDING, }; +use lyracore_shared::auction::AuctionRefusal; #[cfg(feature = "debug_reducers")] use crate::mail::game_mail; @@ -238,14 +239,14 @@ fn imported_house_policy(ctx: &ReducerContext, house: u32) -> Result Option { ) } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum BidRefusal { - NotEnoughMoney, - Database, -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct HeldBid { request: BidRequest, @@ -622,33 +610,36 @@ struct HeldBid { trait BidSource { fn money(&self, bidder_guid: u64) -> Option; fn hold(&self, operation_id: u64) -> Option; - fn create_hold(&mut self, request: BidRequest) -> Result<(), BidRefusal>; - fn finish_hold(&mut self, request: BidRequest, decision: BidDecision) - -> Result<(), BidRefusal>; - fn confirm_refund(&mut self, request: BidRequest) -> Result<(), BidRefusal>; + fn create_hold(&mut self, request: BidRequest) -> Result<(), AuctionRefusal>; + fn finish_hold( + &mut self, + request: BidRequest, + decision: BidDecision, + ) -> Result<(), AuctionRefusal>; + fn confirm_refund(&mut self, request: BidRequest) -> Result<(), AuctionRefusal>; } -fn fence_bid(source: &mut S, request: BidRequest) -> Result<(), BidRefusal> { +fn fence_bid(source: &mut S, request: BidRequest) -> Result<(), AuctionRefusal> { if request.operation_id == 0 || request.bidder_guid == 0 || request.auction_id == 0 || request.house == 0 || request.offer == 0 { - return Err(BidRefusal::Database); + return Err(AuctionRefusal::Database); } if let Some(hold) = source.hold(request.operation_id) { return if hold.request == request { Ok(()) } else { - Err(BidRefusal::Database) + Err(AuctionRefusal::Database) }; } if source .money(request.bidder_guid) .is_none_or(|money| money < request.offer) { - return Err(BidRefusal::NotEnoughMoney); + return Err(AuctionRefusal::NotEnoughMoney); } source.create_hold(request)?; Ok(()) @@ -658,18 +649,18 @@ fn finish_bid( source: &mut S, request: BidRequest, decision: BidDecision, -) -> Result { +) -> Result { let hold = source .hold(request.operation_id) - .ok_or(BidRefusal::Database)?; + .ok_or(AuctionRefusal::Database)?; if hold.request != request { - return Err(BidRefusal::Database); + return Err(AuctionRefusal::Database); } if let Some(existing) = hold.decision { return if existing == decision { Ok(existing) } else { - Err(BidRefusal::Database) + Err(AuctionRefusal::Database) }; } source.finish_hold(request, decision)?; @@ -772,35 +763,35 @@ trait BidMarket { request: BidRequest, auction: Option, decision: BidDecision, - ) -> Result<(), BidRefusal>; + ) -> Result<(), AuctionRefusal>; } trait BidRefundSink { fn refund_decision(&self, operation_id: u64) -> Option<(BidRequest, BidDecision, u32)>; - fn commit_refund(&mut self, request: BidRequest, amount: u32) -> Result<(), BidRefusal>; + fn commit_refund(&mut self, request: BidRequest, amount: u32) -> Result<(), AuctionRefusal>; } fn relay_bid_refund( sink: &mut S, request: BidRequest, amount: u32, -) -> Result<(), BidRefusal> { +) -> Result<(), AuctionRefusal> { if amount == 0 { return Ok(()); } let (existing_request, decision, recorded) = sink .refund_decision(request.operation_id) - .ok_or(BidRefusal::Database)?; + .ok_or(AuctionRefusal::Database)?; if existing_request != request || refundable_bid_value(decision, request.offer).is_none_or(|limit| amount > limit) { - return Err(BidRefusal::Database); + return Err(AuctionRefusal::Database); } if recorded != 0 { return if recorded == amount { Ok(()) } else { - Err(BidRefusal::Database) + Err(AuctionRefusal::Database) }; } sink.commit_refund(request, amount) @@ -810,24 +801,24 @@ fn confirm_bid_refund( source: &mut S, request: BidRequest, amount: u32, -) -> Result<(), BidRefusal> { +) -> Result<(), AuctionRefusal> { if amount == 0 { - return Err(BidRefusal::Database); + return Err(AuctionRefusal::Database); } let hold = source .hold(request.operation_id) - .ok_or(BidRefusal::Database)?; - let decision = hold.decision.ok_or(BidRefusal::Database)?; + .ok_or(AuctionRefusal::Database)?; + let decision = hold.decision.ok_or(AuctionRefusal::Database)?; if hold.request != request || refundable_bid_value(decision, request.offer).is_none_or(|limit| amount > limit) { - return Err(BidRefusal::Database); + return Err(AuctionRefusal::Database); } if hold.deferred_refund == 0 { return Ok(()); } if hold.deferred_refund != amount { - return Err(BidRefusal::Database); + return Err(AuctionRefusal::Database); } source.confirm_refund(request) } @@ -835,12 +826,12 @@ fn confirm_bid_refund( fn resolve_bid( market: &mut S, request: BidRequest, -) -> Result { +) -> Result { if let Some((existing_request, decision)) = market.decision(request.operation_id) { return if existing_request == request { Ok(decision) } else { - Err(BidRefusal::Database) + Err(AuctionRefusal::Database) }; } let auction = market.auction(request.auction_id); @@ -853,7 +844,7 @@ fn drive_bid( source: &mut S, market: &mut M, request: BidRequest, -) -> Result { +) -> Result { fence_bid(source, request)?; if let Some(decision) = source .hold(request.operation_id) @@ -889,14 +880,14 @@ fn operation_match(sink: &S, request: ListingRequest) -> Op fn prepare_from_source( sink: &S, request: ListingRequest, -) -> Result { +) -> Result { if request.operation_id == 0 || request.house.id == 0 { - return Err(ListingRefusal::InvalidTerms); + return Err(AuctionRefusal::InvalidTerms); } let item = sink.item(request.item_guid); let seller_money = sink .seller_money(request.seller_guid) - .ok_or(ListingRefusal::ItemNotFound)?; + .ok_or(AuctionRefusal::ItemNotFound)?; let deposit = prepare_listing( item.as_ref(), request.seller_guid, @@ -908,7 +899,7 @@ fn prepare_from_source( let expires_micros = i64::from(request.terms.duration_minutes) .checked_mul(MICROS_PER_MINUTE) .and_then(|duration| created_micros.checked_add(duration)) - .ok_or(ListingRefusal::InvalidTerms)?; + .ok_or(AuctionRefusal::InvalidTerms)?; Ok(PreparedListing { request, snapshot: item.expect("validated listing item is present").snapshot, @@ -921,10 +912,10 @@ fn prepare_from_source( fn create_local_listing( sink: &mut S, request: ListingRequest, -) -> Result { +) -> Result { match operation_match(sink, request) { OperationMatch::Replay(auction_id) => return Ok(auction_id), - OperationMatch::Conflict => return Err(ListingRefusal::InvalidTerms), + OperationMatch::Conflict => return Err(AuctionRefusal::InvalidTerms), OperationMatch::Fresh => {} } let listing = prepare_from_source(sink, request)?; @@ -945,19 +936,19 @@ trait MarketSink { fn commit_market(&mut self, listing: PreparedListing) -> u32; } -fn fence_listing(sink: &mut S, request: ListingRequest) -> Result<(), ListingRefusal> { +fn fence_listing(sink: &mut S, request: ListingRequest) -> Result<(), AuctionRefusal> { if let Some(receipt) = sink.receipt(request.operation_id) { return if receipt.listing.request == request { Ok(()) } else { - Err(ListingRefusal::InvalidTerms) + Err(AuctionRefusal::InvalidTerms) }; } if let Some(hold) = sink.hold(request.operation_id) { return if hold.listing.request == request { Ok(()) } else { - Err(ListingRefusal::InvalidTerms) + Err(AuctionRefusal::InvalidTerms) }; } let listing = prepare_from_source(sink, request)?; @@ -968,12 +959,12 @@ fn fence_listing(sink: &mut S, request: ListingRequest) -> Result<( fn commit_held_listing( sink: &mut S, listing: PreparedListing, -) -> Result { +) -> Result { if let Some(receipt) = sink.receipt(listing.request.operation_id) { return if receipt.listing == listing { Ok(receipt.auction_id) } else { - Err(ListingRefusal::InvalidTerms) + Err(AuctionRefusal::InvalidTerms) }; } Ok(sink.commit_market(listing)) @@ -982,35 +973,35 @@ fn commit_held_listing( fn confirm_listing( sink: &mut S, receipt: ListingReceipt, -) -> Result<(), ListingRefusal> { +) -> Result<(), AuctionRefusal> { if let Some(existing) = sink.receipt(receipt.listing.request.operation_id) { return if existing == receipt { Ok(()) } else { - Err(ListingRefusal::InvalidTerms) + Err(AuctionRefusal::InvalidTerms) }; } let hold = sink .hold(receipt.listing.request.operation_id) - .ok_or(ListingRefusal::InvalidTerms)?; + .ok_or(AuctionRefusal::InvalidTerms)?; if hold.listing != receipt.listing { - return Err(ListingRefusal::InvalidTerms); + return Err(AuctionRefusal::InvalidTerms); } sink.confirm_hold(receipt); Ok(()) } -fn settle_listing(sink: &mut S, operation_id: u64) -> Result<(), ListingRefusal> { +fn settle_listing(sink: &mut S, operation_id: u64) -> Result<(), AuctionRefusal> { let receipt = sink .receipt(operation_id) - .ok_or(ListingRefusal::InvalidTerms)?; + .ok_or(AuctionRefusal::InvalidTerms)?; match sink.hold(operation_id) { None => Ok(()), Some(hold) if hold.listing == receipt.listing => { sink.delete_hold(operation_id); Ok(()) } - Some(_) => Err(ListingRefusal::InvalidTerms), + Some(_) => Err(AuctionRefusal::InvalidTerms), } } @@ -1032,15 +1023,15 @@ fn release_listing( sink: &mut S, operation_id: u64, seller_guid: u64, -) -> Result<(), ListingRefusal> { +) -> Result<(), AuctionRefusal> { if sink.receipt(operation_id).is_some() { - return Err(ListingRefusal::InvalidTerms); + return Err(AuctionRefusal::InvalidTerms); } let Some(hold) = sink.hold(operation_id) else { return Ok(()); }; if hold.listing.request.seller_guid != seller_guid { - return Err(ListingRefusal::InvalidTerms); + return Err(AuctionRefusal::InvalidTerms); } sink.release_hold(operation_id, listing_release_mail(&hold.listing)); Ok(()) @@ -1102,13 +1093,11 @@ fn expire_active(sink: &mut S, auction_id: u32) -> Result<(), Str Ok(()) } -fn tagged(refusal: ListingRefusal, detail: &str) -> String { - let tag = match refusal { - ListingRefusal::ItemNotFound => lyracore_shared::auction::result::ITEM_NOT_FOUND, - ListingRefusal::NotEnoughMoney => lyracore_shared::auction::result::NOT_ENOUGH_MONEY, - ListingRefusal::InvalidTerms => lyracore_shared::auction::result::DATABASE, - }; - format!("[{tag}] {detail}") +/// Reducer edge: only the tag crosses to the gateway; the detail stays in module logs. +fn refused(refusal: AuctionRefusal, detail: &str) -> String { + let tag = refusal.as_tag(); + spacetimedb::log::info!("auction refused {tag}: {detail}"); + tag.to_string() } fn listing_from_hold(row: AuctionHold) -> PreparedListing { @@ -1409,13 +1398,13 @@ impl BidSource for CtxBidSource<'_> { }) } - fn create_hold(&mut self, request: BidRequest) -> Result<(), BidRefusal> { + fn create_hold(&mut self, request: BidRequest) -> Result<(), AuctionRefusal> { let mut bidder = crate::helpers::acting_entity_by_guid(self.ctx, request.bidder_guid) - .ok_or(BidRefusal::NotEnoughMoney)?; + .ok_or(AuctionRefusal::NotEnoughMoney)?; bidder.money = bidder .money .checked_sub(request.offer) - .ok_or(BidRefusal::NotEnoughMoney)?; + .ok_or(AuctionRefusal::NotEnoughMoney)?; self.ctx.db.game_world_entity().guid().update(bidder); self.ctx.db.game_auction_bid_hold().insert(AuctionBidHold { operation_id: request.operation_id, @@ -1438,11 +1427,12 @@ impl BidSource for CtxBidSource<'_> { &mut self, request: BidRequest, decision: BidDecision, - ) -> Result<(), BidRefusal> { - let refund = refundable_bid_value(decision, request.offer).ok_or(BidRefusal::Database)?; + ) -> Result<(), AuctionRefusal> { + let refund = + refundable_bid_value(decision, request.offer).ok_or(AuctionRefusal::Database)?; let deferred_refund = if refund != 0 { let mut bidder = crate::helpers::acting_entity_by_guid(self.ctx, request.bidder_guid) - .ok_or(BidRefusal::Database)?; + .ok_or(AuctionRefusal::Database)?; let (money, deferred_refund) = split_bid_refund(bidder.money, refund); bidder.money = money; self.ctx.db.game_world_entity().guid().update(bidder); @@ -1472,20 +1462,20 @@ impl BidSource for CtxBidSource<'_> { Ok(()) } - fn confirm_refund(&mut self, request: BidRequest) -> Result<(), BidRefusal> { + fn confirm_refund(&mut self, request: BidRequest) -> Result<(), AuctionRefusal> { let mut row = self .ctx .db .game_auction_bid_hold() .operation_id() .find(request.operation_id) - .ok_or(BidRefusal::Database)?; + .ok_or(AuctionRefusal::Database)?; if row.bidder_guid != request.bidder_guid || row.auction_id != request.auction_id || row.house != request.house || row.offer != request.offer { - return Err(BidRefusal::Database); + return Err(AuctionRefusal::Database); } row.deferred_refund = 0; self.ctx @@ -1559,21 +1549,21 @@ impl BidMarket for CtxBidMarket<'_> { request: BidRequest, auction: Option, decision: BidDecision, - ) -> Result<(), BidRefusal> { + ) -> Result<(), AuctionRefusal> { if let BidDecision::Accepted(accepted) = decision { - let expected = auction.ok_or(BidRefusal::Database)?; + let expected = auction.ok_or(AuctionRefusal::Database)?; let mut row = self .ctx .db .game_auction() .id() .find(request.auction_id) - .ok_or(BidRefusal::Database)?; + .ok_or(AuctionRefusal::Database)?; if row.revision != expected.revision || row.highest_bidder_guid != expected.highest_bidder_guid || row.highest_bid != expected.highest_bid { - return Err(BidRefusal::Database); + return Err(AuctionRefusal::Database); } let refund_mail = displaced_bid_refund_mail(accepted.displaced_bidder_guid, accepted.displaced_bid); @@ -1581,7 +1571,7 @@ impl BidMarket for CtxBidMarket<'_> { AuctionBidEffect::SettleBuyout => { let sale_mail = buyout_settlement_mail(expected, request.bidder_guid, accepted.price) - .ok_or(BidRefusal::Database)?; + .ok_or(AuctionRefusal::Database)?; self.ctx .db .game_auction_expiry() @@ -1648,21 +1638,21 @@ impl BidRefundSink for CtxBidMarket<'_> { )) } - fn commit_refund(&mut self, request: BidRequest, amount: u32) -> Result<(), BidRefusal> { + fn commit_refund(&mut self, request: BidRequest, amount: u32) -> Result<(), AuctionRefusal> { let mut row = self .ctx .db .game_auction_bid_decision() .operation_id() .find(request.operation_id) - .ok_or(BidRefusal::Database)?; + .ok_or(AuctionRefusal::Database)?; if row.bidder_guid != request.bidder_guid || row.auction_id != request.auction_id || row.house != request.house || row.offer != request.offer || row.deferred_refund != 0 { - return Err(BidRefusal::Database); + return Err(AuctionRefusal::Database); } crate::mail::insert_mail( self.ctx, @@ -1700,14 +1690,6 @@ fn bid_request( } } -fn tagged_bid(refusal: BidRefusal, detail: &str) -> String { - let tag = match refusal { - BidRefusal::NotEnoughMoney => lyracore_shared::auction::result::NOT_ENOUGH_MONEY, - BidRefusal::Database => lyracore_shared::auction::result::DATABASE, - }; - format!("[{tag}] {detail}") -} - fn validate_market_listing(ctx: &ReducerContext, listing: &PreparedListing) -> Result<(), String> { if listing.request.operation_id == 0 || listing.request.house.id == 0 @@ -1719,25 +1701,28 @@ fn validate_market_listing(ctx: &ReducerContext, listing: &PreparedListing) -> R || (listing.request.terms.buyout != 0 && listing.request.terms.buyout < listing.request.terms.start_bid) { - return Err(tagged(ListingRefusal::InvalidTerms, "invalid held listing")); + return Err(refused( + AuctionRefusal::InvalidTerms, + "invalid held listing", + )); } let template = ctx .db .game_item_template() .entry() .find(listing.snapshot.entry) - .ok_or_else(|| tagged(ListingRefusal::InvalidTerms, "item template missing"))?; + .ok_or_else(|| refused(AuctionRefusal::InvalidTerms, "item template missing"))?; let expected_deposit = listing_deposit( template.sell_price, listing.snapshot.stack_count, listing.request.terms.duration_minutes, listing.request.house.deposit_rate, ) - .ok_or_else(|| tagged(ListingRefusal::InvalidTerms, "invalid listing arithmetic"))?; + .ok_or_else(|| refused(AuctionRefusal::InvalidTerms, "invalid listing arithmetic"))?; let expected_expiry = i64::from(listing.request.terms.duration_minutes) .checked_mul(MICROS_PER_MINUTE) .and_then(|duration| listing.created_micros.checked_add(duration)) - .ok_or_else(|| tagged(ListingRefusal::InvalidTerms, "auction expiry overflow"))?; + .ok_or_else(|| refused(AuctionRefusal::InvalidTerms, "auction expiry overflow"))?; if listing.deposit != expected_deposit || !listing_proceeds_are_representable( listing.request.terms, @@ -1746,8 +1731,8 @@ fn validate_market_listing(ctx: &ReducerContext, listing: &PreparedListing) -> R ) || listing.expires_micros != expected_expiry { - return Err(tagged( - ListingRefusal::InvalidTerms, + return Err(refused( + AuctionRefusal::InvalidTerms, "held listing payload changed", )); } @@ -1852,16 +1837,16 @@ pub fn gw_auction_list_local( let house = match existing_house { Some(policy) if policy.id == requested_house => policy, Some(_) => { - return Err(tagged( - ListingRefusal::InvalidTerms, + return Err(refused( + AuctionRefusal::InvalidTerms, "listing operation id conflict", )); } None => imported_house_policy(ctx, requested_house)?, }; if !replay && auction_house_for_interaction(ctx, seller_guid, auctioneer_guid) != Some(house) { - return Err(tagged( - ListingRefusal::InvalidTerms, + return Err(refused( + AuctionRefusal::InvalidTerms, "auctioneer refused interaction", )); } @@ -1880,7 +1865,7 @@ pub fn gw_auction_list_local( ), ) .map(|_| ()) - .map_err(|refusal| tagged(refusal, "listing rejected")) + .map_err(|refusal| refused(refusal, "listing rejected")) } /// Sharded listing phase 1: atomically move the source value into a caller-identified Hold. @@ -1918,16 +1903,16 @@ pub fn gw_auction_hold_listing( let house = match existing_house { Some(policy) if policy.id == requested_house => policy, Some(_) => { - return Err(tagged( - ListingRefusal::InvalidTerms, + return Err(refused( + AuctionRefusal::InvalidTerms, "listing operation id conflict", )); } None => imported_house_policy(ctx, requested_house)?, }; if !replay && auction_house_for_interaction(ctx, seller_guid, auctioneer_guid) != Some(house) { - return Err(tagged( - ListingRefusal::InvalidTerms, + return Err(refused( + AuctionRefusal::InvalidTerms, "auctioneer refused interaction", )); } @@ -1945,7 +1930,7 @@ pub fn gw_auction_hold_listing( duration_minutes, ), ) - .map_err(|refusal| tagged(refusal, "listing Hold rejected")) + .map_err(|refusal| refused(refusal, "listing Hold rejected")) } /// Sharded listing phase 2: create the realm Auction and idempotency receipt from a held payload. @@ -2001,7 +1986,7 @@ pub fn realm_auction_commit_listing( } commit_held_listing(&mut market, listing) .map(|_| ()) - .map_err(|refusal| tagged(refusal, "listing operation id conflict")) + .map_err(|refusal| refused(refusal, "listing operation id conflict")) } /// Sharded listing phase 3: copy the matching realm receipt onto the source shard. @@ -2019,7 +2004,7 @@ pub fn realm_auction_confirm_listing( as HoldSink>::receipt(&CtxSource { ctx }, operation_id) .map(|receipt| receipt.listing) }) - .ok_or_else(|| tagged(ListingRefusal::InvalidTerms, "listing Hold missing"))?; + .ok_or_else(|| refused(AuctionRefusal::InvalidTerms, "listing Hold missing"))?; confirm_listing( &mut CtxSource { ctx }, ListingReceipt { @@ -2027,7 +2012,7 @@ pub fn realm_auction_confirm_listing( auction_id, }, ) - .map_err(|refusal| tagged(refusal, "listing receipt conflict")) + .map_err(|refusal| refused(refusal, "listing receipt conflict")) } /// Sharded listing phase 4: delete the Hold only after the source has matching receipt evidence. @@ -2035,7 +2020,7 @@ pub fn realm_auction_confirm_listing( pub fn realm_auction_settle_listing(ctx: &ReducerContext, operation_id: u64) -> Result<(), String> { crate::helpers::require_operator(ctx)?; settle_listing(&mut CtxSource { ctx }, operation_id) - .map_err(|refusal| tagged(refusal, "listing Hold is not confirmed")) + .map_err(|refusal| refused(refusal, "listing Hold is not confirmed")) } /// Sharded listing abort: after realm-core refuses phase 2, mail the held item and deposit back @@ -2048,7 +2033,7 @@ pub fn gw_auction_release_listing_hold( ) -> Result<(), String> { crate::helpers::require_operator(ctx)?; release_listing(&mut CtxSource { ctx }, operation_id, seller_guid) - .map_err(|refusal| tagged(refusal, "listing Hold is confirmed")) + .map_err(|refusal| refused(refusal, "listing Hold is confirmed")) } /// Single-database bid: full-offer Hold, realm decision, Auction update or buyout settlement, @@ -2074,8 +2059,8 @@ pub fn gw_auction_bid_local( && auction_house_for_interaction(ctx, bidder_guid, auctioneer_guid) .is_none_or(|policy| policy.id != house) { - return Err(tagged_bid( - BidRefusal::Database, + return Err(refused( + AuctionRefusal::Database, "auctioneer refused interaction", )); } @@ -2085,16 +2070,16 @@ pub fn gw_auction_bid_local( &mut CtxBidMarket { ctx }, request, ) - .map_err(|refusal| tagged_bid(refusal, "local bid rejected"))?; + .map_err(|refusal| refused(refusal, "local bid rejected"))?; let deferred_refund = CtxBidSource { ctx } .hold(operation_id) - .ok_or_else(|| tagged_bid(BidRefusal::Database, "local bid Hold missing"))? + .ok_or_else(|| refused(AuctionRefusal::Database, "local bid Hold missing"))? .deferred_refund; if deferred_refund != 0 { relay_bid_refund(&mut CtxBidMarket { ctx }, request, deferred_refund) - .map_err(|refusal| tagged_bid(refusal, "local bid refund conflict"))?; + .map_err(|refusal| refused(refusal, "local bid refund conflict"))?; confirm_bid_refund(&mut CtxBidSource { ctx }, request, deferred_refund) - .map_err(|refusal| tagged_bid(refusal, "local bid refund confirmation conflict"))?; + .map_err(|refusal| refused(refusal, "local bid refund confirmation conflict"))?; } Ok(()) } @@ -2121,8 +2106,8 @@ pub fn gw_auction_hold_bid( && auction_house_for_interaction(ctx, bidder_guid, auctioneer_guid) .is_none_or(|policy| policy.id != house) { - return Err(tagged_bid( - BidRefusal::Database, + return Err(refused( + AuctionRefusal::Database, "auctioneer refused interaction", )); } @@ -2130,7 +2115,7 @@ pub fn gw_auction_hold_bid( &mut CtxBidSource { ctx }, bid_request(operation_id, bidder_guid, auction_id, house, offer), ) - .map_err(|refusal| tagged_bid(refusal, "bid Hold rejected")) + .map_err(|refusal| refused(refusal, "bid Hold rejected")) } /// Sharded bid phase 2: serialize against the realm Auction and persist one terminal decision. @@ -2149,7 +2134,7 @@ pub fn realm_auction_decide_bid( bid_request(operation_id, bidder_guid, auction_id, house, offer), ) .map(|_| ()) - .map_err(|refusal| tagged_bid(refusal, "bid decision conflict")) + .map_err(|refusal| refused(refusal, "bid decision conflict")) } /// Sharded bid phase 3: consume the normalized accepted price or restore refused value exactly once. @@ -2181,14 +2166,14 @@ pub fn gw_auction_finish_bid( }, offer, ) - .ok_or_else(|| tagged_bid(BidRefusal::Database, "bid decision is pending"))?; + .ok_or_else(|| refused(AuctionRefusal::Database, "bid decision is pending"))?; finish_bid( &mut CtxBidSource { ctx }, bid_request(operation_id, bidder_guid, auction_id, house, offer), decision, ) .map(|_| ()) - .map_err(|refusal| tagged_bid(refusal, "bid outcome conflict")) + .map_err(|refusal| refused(refusal, "bid outcome conflict")) } /// Sharded bid phase 4: place an unrepresentable purse refund in realm-core mail exactly once. @@ -2208,7 +2193,7 @@ pub fn realm_auction_refund_bid( bid_request(operation_id, bidder_guid, auction_id, house, offer), deferred_refund, ) - .map_err(|refusal| tagged_bid(refusal, "bid refund conflict")) + .map_err(|refusal| refused(refusal, "bid refund conflict")) } /// Sharded bid phase 5: record on the source that realm-core durably accepted the refund mail. @@ -2228,7 +2213,7 @@ pub fn gw_auction_confirm_bid_refund( bid_request(operation_id, bidder_guid, auction_id, house, offer), deferred_refund, ) - .map_err(|refusal| tagged_bid(refusal, "bid refund confirmation conflict")) + .map_err(|refusal| refused(refusal, "bid refund confirmation conflict")) } /// Scheduler-only one-shot expiry. Replays see no active Auction and therefore create no mail. @@ -2634,9 +2619,9 @@ fn prepare_listing( seller_money: u32, terms: ListingTerms, house: AuctionHousePolicy, -) -> Result { +) -> Result { let Some(item) = item else { - return Err(ListingRefusal::ItemNotFound); + return Err(AuctionRefusal::ItemNotFound); }; if item.owner_guid != seller_guid || item.slot < FIRST_BACKPACK_SLOT @@ -2645,10 +2630,10 @@ fn prepare_listing( || item.snapshot.stack_count == 0 || item.snapshot.soulbound { - return Err(ListingRefusal::ItemNotFound); + return Err(AuctionRefusal::ItemNotFound); } if terms.start_bid == 0 || (terms.buyout != 0 && terms.buyout < terms.start_bid) { - return Err(ListingRefusal::InvalidTerms); + return Err(AuctionRefusal::InvalidTerms); } let deposit = listing_deposit( item.sell_price, @@ -2656,12 +2641,12 @@ fn prepare_listing( terms.duration_minutes, house.deposit_rate, ) - .ok_or(ListingRefusal::InvalidTerms)?; + .ok_or(AuctionRefusal::InvalidTerms)?; if !listing_proceeds_are_representable(terms, deposit, house.consignment_rate) { - return Err(ListingRefusal::InvalidTerms); + return Err(AuctionRefusal::InvalidTerms); } if seller_money < deposit { - return Err(ListingRefusal::NotEnoughMoney); + return Err(AuctionRefusal::NotEnoughMoney); } Ok(deposit) } @@ -2780,7 +2765,7 @@ mod tests { for slot in [15, 19, 39, 63, 119, 192] { assert_eq!( prepare_listing(Some(&item(slot)), 7, 10, terms(), policy()), - Err(ListingRefusal::ItemNotFound), + Err(AuctionRefusal::ItemNotFound), "slot {slot} must not be auctionable" ); } @@ -2789,30 +2774,30 @@ mod tests { foreign.owner_guid = 8; assert_eq!( prepare_listing(Some(&foreign), 7, 10, terms(), policy()), - Err(ListingRefusal::ItemNotFound) + Err(AuctionRefusal::ItemNotFound) ); let mut soulbound = item(23); soulbound.snapshot.soulbound = true; assert_eq!( prepare_listing(Some(&soulbound), 7, 10, terms(), policy()), - Err(ListingRefusal::ItemNotFound) + Err(AuctionRefusal::ItemNotFound) ); let mut not_mailable = item(120); not_mailable.mailable = false; assert_eq!( prepare_listing(Some(¬_mailable), 7, 10, terms(), policy()), - Err(ListingRefusal::ItemNotFound) + Err(AuctionRefusal::ItemNotFound) ); assert_eq!( prepare_listing(None, 7, 10, terms(), policy()), - Err(ListingRefusal::ItemNotFound) + Err(AuctionRefusal::ItemNotFound) ); assert_eq!( prepare_listing(Some(&item(23)), 7, 9, terms(), policy()), - Err(ListingRefusal::NotEnoughMoney) + Err(AuctionRefusal::NotEnoughMoney) ); for invalid in [ @@ -2832,7 +2817,7 @@ mod tests { ] { assert_eq!( prepare_listing(Some(&item(23)), 7, 10, invalid, policy()), - Err(ListingRefusal::InvalidTerms) + Err(AuctionRefusal::InvalidTerms) ); } } @@ -3076,7 +3061,7 @@ mod tests { assert_eq!( create_local_listing(&mut local, request), - Err(ListingRefusal::InvalidTerms) + Err(AuctionRefusal::InvalidTerms) ); assert_eq!(local.money, before.money); assert_eq!(local.item, before.item); @@ -3089,7 +3074,7 @@ mod tests { assert_eq!( fence_listing(&mut source, request), - Err(ListingRefusal::InvalidTerms) + Err(AuctionRefusal::InvalidTerms) ); assert_eq!(source.money, before.money); assert_eq!(source.item, before.item); @@ -3110,7 +3095,7 @@ mod tests { source: &mut FakeSource, market: &mut FakeMarket, request: ListingRequest, - ) -> Result { + ) -> Result { fence_listing(source, request)?; if let Some(receipt) = source.receipt(request.operation_id) { settle_listing(source, request.operation_id)?; @@ -3245,14 +3230,14 @@ mod tests { changed.terms.buyout += 1; assert_eq!( fence_listing(&mut source, changed), - Err(ListingRefusal::InvalidTerms) + Err(AuctionRefusal::InvalidTerms) ); let mut changed_payload = source.hold(original.operation_id).unwrap().listing; changed_payload.snapshot.durability += 1; assert_eq!( commit_held_listing(&mut market, changed_payload), - Err(ListingRefusal::InvalidTerms) + Err(AuctionRefusal::InvalidTerms) ); assert_eq!(source.money, Some(40)); assert_eq!(market.auction_count, 1); @@ -3272,7 +3257,7 @@ mod tests { assert!(market.receipt.is_none()); assert_eq!( release_listing(&mut source, request.operation_id, request.seller_guid + 1), - Err(ListingRefusal::InvalidTerms), + Err(AuctionRefusal::InvalidTerms), "a wrong seller never releases another character's Hold" ); assert!(source.hold.is_some()); @@ -3320,7 +3305,7 @@ mod tests { assert_eq!( release_listing(&mut source, request.operation_id, request.seller_guid), - Err(ListingRefusal::InvalidTerms) + Err(AuctionRefusal::InvalidTerms) ); assert!(source.hold.is_some()); assert!(source.returned_mail.is_empty()); @@ -3684,7 +3669,7 @@ mod tests { .filter(|hold| hold.request.operation_id == operation_id) } - fn create_hold(&mut self, request: BidRequest) -> Result<(), BidRefusal> { + fn create_hold(&mut self, request: BidRequest) -> Result<(), AuctionRefusal> { self.money -= request.offer; self.hold = Some(HeldBid { request, @@ -3698,9 +3683,9 @@ mod tests { &mut self, request: BidRequest, decision: BidDecision, - ) -> Result<(), BidRefusal> { + ) -> Result<(), AuctionRefusal> { let refund = - refundable_bid_value(decision, request.offer).ok_or(BidRefusal::Database)?; + refundable_bid_value(decision, request.offer).ok_or(AuctionRefusal::Database)?; if refund != 0 { let (money, deferred_refund) = split_bid_refund(self.money, refund); self.money = money; @@ -3714,7 +3699,7 @@ mod tests { Ok(()) } - fn confirm_refund(&mut self, request: BidRequest) -> Result<(), BidRefusal> { + fn confirm_refund(&mut self, request: BidRequest) -> Result<(), AuctionRefusal> { self.deferred_refund = 0; self.hold = self.hold.map(|mut hold| { assert_eq!(hold.request, request); @@ -3814,7 +3799,7 @@ mod tests { }, accepted ), - Err(BidRefusal::Database), + Err(AuctionRefusal::Database), "changed-payload identifier reuse fails closed" ); @@ -3837,14 +3822,17 @@ mod tests { }, ] { let mut source = FakeBidSource::new(200); - assert_eq!(fence_bid(&mut source, malformed), Err(BidRefusal::Database)); + assert_eq!( + fence_bid(&mut source, malformed), + Err(AuctionRefusal::Database) + ); assert_eq!(source.money, 200); assert!(source.hold.is_none()); } let mut poor = FakeBidSource::new(106); assert_eq!( fence_bid(&mut poor, request), - Err(BidRefusal::NotEnoughMoney) + Err(AuctionRefusal::NotEnoughMoney) ); assert_eq!(poor.money, 106); assert!(poor.hold.is_none()); @@ -3890,7 +3878,7 @@ mod tests { request: BidRequest, auction: Option, decision: BidDecision, - ) -> Result<(), BidRefusal> { + ) -> Result<(), AuctionRefusal> { if let BidDecision::Accepted(accepted) = decision { let mut auction = auction.expect("only an active Auction can accept a bid"); self.mail.extend(displaced_bid_refund_mail( @@ -3972,7 +3960,11 @@ mod tests { )) } - fn commit_refund(&mut self, request: BidRequest, amount: u32) -> Result<(), BidRefusal> { + fn commit_refund( + &mut self, + request: BidRequest, + amount: u32, + ) -> Result<(), AuctionRefusal> { self.recorded = amount; self.mails.push((request.bidder_guid, amount)); Ok(()) @@ -4007,11 +3999,11 @@ mod tests { }, 7 ), - Err(BidRefusal::Database) + Err(AuctionRefusal::Database) ); assert_eq!( relay_bid_refund(&mut sink, request, 8), - Err(BidRefusal::Database) + Err(AuctionRefusal::Database) ); sink.recorded = 0; @@ -4021,7 +4013,7 @@ mod tests { assert_eq!(sink.mails, vec![(8, 7), (8, 7)]); assert_eq!( relay_bid_refund(&mut sink, request, 8), - Err(BidRefusal::Database) + Err(AuctionRefusal::Database) ); } @@ -4070,7 +4062,7 @@ mod tests { ..request } ), - Err(BidRefusal::Database) + Err(AuctionRefusal::Database) ); assert_eq!( market.mail,