diff --git a/src/utils/_symbolConverter.ts b/src/utils/_symbolConverter.ts index 764da539..cc5a88ea 100644 --- a/src/utils/_symbolConverter.ts +++ b/src/utils/_symbolConverter.ts @@ -212,6 +212,12 @@ export class SymbolConverter { // Fallback outcomes are not tradable on their own and have no public slug const fallbackOutcomes = new Set(outcomeMetaData.questions.map((question) => question.fallbackOutcome)); + // Markets deployed from a template share their question, outcome and side names, so several + // tradable sides can slugify to the same string. Such a slug is unregistered below, so a lookup + // returns `undefined` instead of whichever market was loaded last. + const seenSlugs = new Set(); + const ambiguousSlugs = new Set(); + outcomeMetaData.outcomes.forEach((outcome) => { if (fallbackOutcomes.has(outcome.outcome)) return; const question = questionByOutcome.get(outcome.outcome); @@ -219,12 +225,19 @@ export class SymbolConverter { outcome.sideSpecs.forEach((sideSpec, sideIdx) => { const slug = this._outcomeSlug(outcome, sideSpec.name, question); if (!slug) return; + if (seenSlugs.has(slug)) ambiguousSlugs.add(slug); + seenSlugs.add(slug); this._nameToAssetId.set(slug, 100000000 + 10 * outcome.outcome + sideIdx); // Outcome markets are absent from szDecimals metadata; they are all 5 this._nameToSzDecimals.set(slug, 5); }); }); + + ambiguousSlugs.forEach((slug) => { + this._nameToAssetId.delete(slug); + this._nameToSzDecimals.delete(slug); + }); } /** diff --git a/tests/utils/symbolConverter.test.ts b/tests/utils/symbolConverter.test.ts index 46a73d7c..7cf3e151 100644 --- a/tests/utils/symbolConverter.test.ts +++ b/tests/utils/symbolConverter.test.ts @@ -148,6 +148,53 @@ const OUTCOME_META: OutcomeMetaResponse = { ], }; +/** + * An `outcomeMeta` response modeled on templated mainnet markets. Their display names are template + * ids rather than the market's own name, so distinct markets slugify to the same string. + */ +const TEMPLATED_OUTCOME_META: OutcomeMetaResponse = { + outcomes: [ + { + outcome: 300, + name: "template:binaryPrice", + description: "perp:BTC|threshold:61720|time:20260608-0600", + sideSpecs: [{ name: "template:Yes" }, { name: "template:No" }], + quoteToken: "USDC", + }, + { + outcome: 301, + name: "template:binaryPrice", + description: "perp:ETH|threshold:2400|time:20260608-0600", + sideSpecs: [{ name: "template:Yes" }, { name: "template:No" }], + quoteToken: "USDC", + }, + { + outcome: 302, + name: "Uniquely Named Market", + description: "", + sideSpecs: [{ name: "Yes" }, { name: "No" }], + quoteToken: "USDC", + }, + { + outcome: 303, + name: "Fallback", + description: "", + sideSpecs: [{ name: "Yes" }, { name: "No" }], + quoteToken: "USDC", + }, + ], + questions: [ + { + question: 40, + name: "Weekly Price Markets", + description: "", + fallbackOutcome: 303, + namedOutcomes: [302], + settledNamedOutcomes: [], + }, + ], +}; + // ============================================================ // Tests // ============================================================ @@ -297,3 +344,21 @@ Deno.test("SymbolConverter outcome markets", async (t) => { assertEquals(converter.getSzDecimals("2026-world-cup-champion-argentina-yes"), 5); }); }); + +Deno.test("SymbolConverter templated outcome markets", async (t) => { + const converter = await SymbolConverter.create({ transport: createOutcomeTransport(TEMPLATED_OUTCOME_META) }); + + await t.step("ambiguous slugs are not registered", () => { + // Outcomes 300 and 301 are different markets that slugify identically, so neither may win the + // lookup: before this was handled, the id of whichever loaded last was returned for both. + assertEquals(converter.getAssetId("templatebinaryprice-templateyes"), undefined); + assertEquals(converter.getAssetId("templatebinaryprice-templateno"), undefined); + assertEquals(converter.getSzDecimals("templatebinaryprice-templateyes"), undefined); + }); + + await t.step("unique slugs are still registered", () => { + assertEquals(converter.getAssetId("weekly-price-markets-uniquely-named-market-yes"), 100003020); + assertEquals(converter.getAssetId("weekly-price-markets-uniquely-named-market-no"), 100003021); + assertEquals(typeof converter.getSzDecimals("weekly-price-markets-uniquely-named-market-yes"), "number"); + }); +});