Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/utils/_symbolConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,19 +212,32 @@ 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<string>();
const ambiguousSlugs = new Set<string>();

outcomeMetaData.outcomes.forEach((outcome) => {
if (fallbackOutcomes.has(outcome.outcome)) return;
const question = questionByOutcome.get(outcome.outcome);

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);
});
}

/**
Expand Down
65 changes: 65 additions & 0 deletions tests/utils/symbolConverter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ============================================================
Expand Down Expand Up @@ -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");
});
});