fix(fifa17): map market resource ids to authoritative Core card ids
Closes the market correctness gap: handle_market_list recorded listing.card_id from the raw FIFA wire resourceId, so a synthetic buy minted a card_id Core could not resolve — it survived the immediate response but Core's content preflight rejected it on reboot. - catalog.rs: keep the by_resource reverse index (was built then discarded) and expose `card_id_for_resource(resource_id) -> Option<&str>` — exact reverse of the card_id->asset catalog, no heuristics, unknown => None. - lib.rs: `impl MarketCardResolver for Fifa17IdentityResolver` delegates to the same catalog /club shaping uses; Core never sees a FIFA resource id. - market_store.rs: listings now carry BOTH `card_id` (authoritative Core content, what a buy MINTS) and `wire_resource_id` (the FIFA wire id, echoed in the auction record). New column; create_listing takes both; row/Listing updated. - market.rs: `MarketCardResolver` trait; handle_market_list resolves resourceId -> Core card_id and fails closed (persists nothing) on an unmappable resource; auction_record emits `resourceId` from wire_resource_id. Dispatch passes the resolver. Tests: list_unknown_resource_fails_closed_no_listing (B), list_persists_core_card_and_wire_resource_across_reopen (C), catalog reverse lookup; and the dispatch E2E now RESTORES the full Core+store restart (economy_full_sequence_through_dispatch_and_restart) — the synthetic buy mints a real reverse-mapped card_id, so Core's content preflight passes on reboot (A+D). market 23 lib + catalog 15 + 2 integration green; clippy -D warnings + fmt clean.
This commit is contained in:
@@ -76,12 +76,17 @@ fn db(e: sqlx::Error) -> MarketError {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Listing {
|
||||
pub listing_id: String,
|
||||
/// Authoritative Core content/card id — what a synthetic buy MINTS. Distinct
|
||||
/// from the FIFA wire `resourceId` (see `wire_resource_id`).
|
||||
pub card_id: String,
|
||||
/// Set for a seller-listed owned item; `None` for a synthetic-seller
|
||||
/// listing (the buy path mints a fresh Core item instead of transferring).
|
||||
pub core_item_id: Option<String>,
|
||||
/// The FIFA wire item id of a seller-listed owned item, if any.
|
||||
pub wire_item_id: Option<i64>,
|
||||
/// The FIFA wire `resourceId` (versioned) the client listed, echoed back in
|
||||
/// the auction record. Never used to mint — the mint uses `card_id`.
|
||||
pub wire_resource_id: Option<i64>,
|
||||
pub start_price: i64,
|
||||
pub buy_now_price: i64,
|
||||
/// Opaque seller identity; `None` for synthetic listings.
|
||||
@@ -97,6 +102,7 @@ const CREATE_LISTINGS: &str = "CREATE TABLE IF NOT EXISTS listings (
|
||||
card_id TEXT NOT NULL,
|
||||
core_item_id TEXT,
|
||||
wire_item_id INTEGER,
|
||||
wire_resource_id INTEGER,
|
||||
start_price INTEGER NOT NULL,
|
||||
buy_now_price INTEGER NOT NULL,
|
||||
owner TEXT,
|
||||
@@ -119,6 +125,7 @@ fn row_to_listing(row: &sqlx::sqlite::SqliteRow) -> Listing {
|
||||
card_id: row.get("card_id"),
|
||||
core_item_id: row.get("core_item_id"),
|
||||
wire_item_id: row.get("wire_item_id"),
|
||||
wire_resource_id: row.get("wire_resource_id"),
|
||||
start_price: row.get("start_price"),
|
||||
buy_now_price: row.get("buy_now_price"),
|
||||
owner: row.get("owner"),
|
||||
@@ -177,6 +184,7 @@ impl MarketStore {
|
||||
card_id: &str,
|
||||
core_item_id: Option<&str>,
|
||||
wire_item_id: Option<i64>,
|
||||
wire_resource_id: Option<i64>,
|
||||
start_price: i64,
|
||||
buy_now_price: i64,
|
||||
owner: Option<&str>,
|
||||
@@ -189,13 +197,14 @@ impl MarketStore {
|
||||
.map_err(db)?;
|
||||
let res = sqlx::query(
|
||||
"INSERT INTO listings (listing_id, card_id, core_item_id, wire_item_id, \
|
||||
start_price, buy_now_price, owner, state, created_at) \
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?)",
|
||||
wire_resource_id, start_price, buy_now_price, owner, state, created_at) \
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'active', ?)",
|
||||
)
|
||||
.bind(listing_id)
|
||||
.bind(card_id)
|
||||
.bind(core_item_id)
|
||||
.bind(wire_item_id)
|
||||
.bind(wire_resource_id)
|
||||
.bind(start_price)
|
||||
.bind(buy_now_price)
|
||||
.bind(owner)
|
||||
@@ -213,6 +222,7 @@ impl MarketStore {
|
||||
card_id: card_id.to_string(),
|
||||
core_item_id: core_item_id.map(str::to_string),
|
||||
wire_item_id,
|
||||
wire_resource_id,
|
||||
start_price,
|
||||
buy_now_price,
|
||||
owner: owner.map(str::to_string),
|
||||
@@ -425,7 +435,7 @@ mod tests {
|
||||
|
||||
async fn seed(store: &MarketStore, id: &str) -> Listing {
|
||||
store
|
||||
.create_listing(id, "card_pl_001", None, None, 900, 2500, None)
|
||||
.create_listing(id, "card_pl_001", None, None, None, 900, 2500, None)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
@@ -456,7 +466,7 @@ mod tests {
|
||||
seed(&store, "900000001").await;
|
||||
assert!(matches!(
|
||||
store
|
||||
.create_listing("900000001", "card_pl_002", None, None, 1, 2, None)
|
||||
.create_listing("900000001", "card_pl_002", None, None, None, 1, 2, None)
|
||||
.await,
|
||||
Err(MarketError::Conflict)
|
||||
));
|
||||
@@ -516,6 +526,7 @@ mod tests {
|
||||
"card_pl_001",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
900,
|
||||
2500,
|
||||
Some("alice"),
|
||||
@@ -586,6 +597,7 @@ mod tests {
|
||||
"card_pl_001",
|
||||
Some("core-7"),
|
||||
Some(100004617),
|
||||
Some(169193),
|
||||
900,
|
||||
2500,
|
||||
Some("alice"),
|
||||
|
||||
Reference in New Issue
Block a user