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
11 changes: 9 additions & 2 deletions src/services/OraclePriceBroadcaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,17 @@ export class OraclePriceBroadcaster {
slab: row.slab_address,
priceE6,
});
// oracle_prices carries only a single push price per row — there is
// no separate mark/index price in this event source. Previously this
// hardcoded markPriceE6/indexPriceE6 to the same oracle price, so
// every live WS tick reported markPrice === indexPrice === oracle
// price, contradicting the genuinely distinct values the same
// channel sends from market_stats on initial subscribe (ws.ts
// flushPriceUpdate's own `markPriceE6 ? ... : undefined` check
// already exists to omit these fields when not genuinely known —
// it was just never reachable because this was always truthy).
eventBus.publish("price.updated", row.slab_address, {
priceE6,
markPriceE6: priceE6,
indexPriceE6: priceE6,
source: "oracle_prices",
tx_signature: row.tx_signature ?? undefined,
});
Expand Down
84 changes: 84 additions & 0 deletions tests/services/oracle-price-broadcaster.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Regression for BUG-101: oracle_prices carries only a single push price per
* row — there is no separate mark/index price in this event source. The
* broadcaster must not fabricate markPriceE6/indexPriceE6 values equal to the
* oracle price; doing so caused every live WS price tick to report
* markPrice === indexPrice === oracle price, contradicting the genuinely
* distinct values the same channel sends on initial subscribe.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";

const { publishSpy } = vi.hoisted(() => ({ publishSpy: vi.fn() }));

vi.mock("@percolator/shared", () => ({
eventBus: { publish: publishSpy },
getSupabase: vi.fn(),
getNetwork: vi.fn(() => "devnet"),
createLogger: vi.fn(() => ({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
})),
}));

import { getSupabase } from "@percolator/shared";
import { OraclePriceBroadcaster } from "../../src/services/OraclePriceBroadcaster.js";

describe("OraclePriceBroadcaster", () => {
let insertHandler: (payload: { new: unknown }) => void;
let mockChannel: any;
let mockSupabase: any;

beforeEach(() => {
publishSpy.mockClear();
mockChannel = {
on: vi.fn((_event: string, _filter: unknown, handler: typeof insertHandler) => {
insertHandler = handler;
return mockChannel;
}),
subscribe: vi.fn((cb?: (status: string) => void) => {
cb?.("SUBSCRIBED");
return mockChannel;
}),
};
mockSupabase = {
channel: vi.fn(() => mockChannel),
removeChannel: vi.fn(),
};
vi.mocked(getSupabase).mockReturnValue(mockSupabase);
});

it("publishes price.updated WITHOUT fabricated markPriceE6/indexPriceE6 fields", async () => {
const broadcaster = new OraclePriceBroadcaster();
await broadcaster.start();

insertHandler({
new: {
slab_address: "SLAB1",
price_e6: "1500000",
timestamp: Date.now(),
tx_signature: "sig123",
network: "devnet",
},
});

expect(publishSpy).toHaveBeenCalledTimes(1);
const [event, slab, data] = publishSpy.mock.calls[0];
expect(event).toBe("price.updated");
expect(slab).toBe("SLAB1");
expect(data.priceE6).toBe(1500000);
expect(data).not.toHaveProperty("markPriceE6");
expect(data).not.toHaveProperty("indexPriceE6");
});

it("ignores a row with a non-positive or non-finite price", async () => {
const broadcaster = new OraclePriceBroadcaster();
await broadcaster.start();

insertHandler({ new: { slab_address: "SLAB1", price_e6: "0", timestamp: Date.now(), tx_signature: null, network: "devnet" } });
insertHandler({ new: { slab_address: "SLAB1", price_e6: "not-a-number", timestamp: Date.now(), tx_signature: null, network: "devnet" } });

expect(publishSpy).not.toHaveBeenCalled();
});
});