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:
OpenFUT Agent
2026-08-13 21:43:48 +00:00
parent 884ecbba64
commit fe72f0def2
5 changed files with 217 additions and 51 deletions
+16 -4
View File
@@ -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"),