From f5a592c296c53910ea7d7bcb03f56d9d960c87b0 Mon Sep 17 00:00:00 2001 From: felix11 Date: Wed, 29 Jul 2026 12:09:05 +0400 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20GraphQL=20renders=20Move=20TypeName?= =?UTF-8?q?=20as=20plain=20string,=20not=20{name}=20=E2=80=94=20event=20fi?= =?UTF-8?q?lter=20dropped=20all=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/events.ts | 3 ++- src/common/types.ts | 16 ++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/common/events.ts b/src/common/events.ts index c912234..a4852ba 100644 --- a/src/common/events.ts +++ b/src/common/events.ts @@ -68,7 +68,8 @@ export class Events { eventSeq: eve.id.eventSeq, type: eve.type, } as T; - if ("0x" + event.event.typename.name !== params.typeName) { + // GraphQL renders Move's TypeName as a plain string (JSON-RPC used {name}). + if ("0x" + event.event.typename !== params.typeName) { continue; } if (Number(eve.timestampMs!) > endTime) { diff --git a/src/common/types.ts b/src/common/types.ts index 407d6eb..2422c11 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -44,9 +44,7 @@ export type CommonEventParams = { export type EpochChangedEvent = { event: { - typename: { - name: string; - }; + typename: string; old_sui_supply: string; new_sui_supply: string; lst_supply: string; @@ -61,9 +59,7 @@ export type EpochChangedEvent = { export type MintEvent = { event: { - typename: { - name: string; - }; + typename: string; sui_amount_in: string; lst_amount_out: string; fee_amount: string; @@ -77,9 +73,7 @@ export type MintEvent = { export type RedeemEvent = { event: { - typename: { - name: string; - }; + typename: string; lst_amount_in: string; sui_amount_out: string; fee_amount: string; @@ -94,9 +88,7 @@ export type RedeemEvent = { export type FlashStakeEvent = { event: { - typename: { - name: string; - }; + typename: string; sui_amount_in: string; lst_amount_out: string; fee_amount: string; From 425e0b3a4adace5986d1bc11a7c705b9ad514e71 Mon Sep 17 00:00:00 2001 From: Pankaj Jangid Date: Wed, 29 Jul 2026 13:40:28 +0400 Subject: [PATCH 2/2] test: pin event typename filter contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The typename shape change silently dropped every event while the suite stayed green — the live tests only assert toBeDefined() and the callers swallow an empty result as "0". Mock the GraphQL layer and assert the filter keeps matching events, drops non-matching ones, and drops the legacy {name} shape, so the next rendering change fails CI instead of zeroing out APR in production. --- __tests__/eventFilter.test.ts | 123 ++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 __tests__/eventFilter.test.ts diff --git a/__tests__/eventFilter.test.ts b/__tests__/eventFilter.test.ts new file mode 100644 index 0000000..36eadd3 --- /dev/null +++ b/__tests__/eventFilter.test.ts @@ -0,0 +1,123 @@ +/** + * Regression guard for the event `typename` filter. + * + * GraphQL's `contents.json` renders Move's `TypeName` as a plain string, while + * the JSON-RPC era rendered it as `{ name }`. Reading `.typename.name` under + * GraphQL yielded `undefined`, so `Events.getEvents` silently dropped *every* + * event — `fetchStSuiAPR`/`fetchStSuiAPY` returned "0" and `getMintEvents` + * found nothing, all while the suite stayed green (the live tests only assert + * `toBeDefined()`, and the callers swallow the empty result as "0"). + * + * These tests pin the filter contract without touching the network: the + * GraphQL layer is mocked, so a regression in the `typename` shape fails here + * instead of quietly zeroing out APR in production. + */ +import { jest } from "@jest/globals"; +import type { PaginatedMoveEvents } from "../dist/esm/common/blockchain.js"; + +const STSUI = "0xd1b7...::stsui::STSUI"; +const OTHER_LST = "0xdead...::other::OTHER"; + +/** Build one GraphQL-shaped event node as `queryMoveEvents` returns it. */ +function makeEvent(typename: string, timestampMs: number) { + return { + id: { txDigest: "0xdigest", eventSeq: "0" }, + packageId: "0xpkg", + transactionModule: "liquid_staking", + sender: "0xsender", + type: "0xpkg::events::Event<0xpkg::liquid_staking::MintEvent>", + // `typename` carries no 0x prefix on-chain — the filter re-adds it. + parsedJson: { + event: { + typename: typename.replace(/^0x/, ""), + sui_amount_in: "1000", + lst_amount_out: "990", + fee_amount: "10", + }, + }, + bcs: "", + bcsEncoding: "base64", + timestampMs: String(timestampMs), + }; +} + +const queryMoveEvents = jest.fn<() => Promise>(); + +jest.unstable_mockModule("../dist/esm/common/blockchain.js", () => ({ + queryMoveEvents, +})); + +// Imported after the mock is registered — ESM bindings resolve at import time. +const { Events } = await import("../dist/esm/common/events.js"); + +beforeEach(() => { + queryMoveEvents.mockReset(); +}); + +/** One page, no continuation. */ +function singlePage( + nodes: ReturnType[], +): PaginatedMoveEvents { + return { + data: nodes, + hasNextPage: false, + nextCursor: null, + } as unknown as PaginatedMoveEvents; +} + +describe("Events.getEvents typename filter", () => { + it("keeps events whose typename matches the requested coin type", async () => { + const now = Date.now(); + queryMoveEvents.mockResolvedValue( + singlePage([makeEvent(STSUI, now - 1000), makeEvent(STSUI, now - 2000)]), + ); + + const events = await Events.getMintEvents({ + startTime: now - 86400000, + endTime: now, + typeName: STSUI, + }); + + // The bug this pins: a shape change here silently yields [] instead of failing. + expect(events).toHaveLength(2); + expect(events[0].sender).toBe("0xsender"); + }); + + it("drops events of a different coin type", async () => { + const now = Date.now(); + queryMoveEvents.mockResolvedValue( + singlePage([ + makeEvent(OTHER_LST, now - 1000), + makeEvent(STSUI, now - 2000), + ]), + ); + + const events = await Events.getMintEvents({ + startTime: now - 86400000, + endTime: now, + typeName: STSUI, + }); + + expect(events).toHaveLength(1); + expect(events[0].event.typename).toBe(STSUI.replace(/^0x/, "")); + }); + + it("drops every event when typename is the legacy JSON-RPC {name} shape", async () => { + const now = Date.now(); + const legacy = makeEvent(STSUI, now - 1000); + // The pre-fix rendering — proves the filter compares the string form and + // is not accidentally matching on some other field. + (legacy.parsedJson.event as unknown as { typename: unknown }).typename = { + name: STSUI.replace(/^0x/, ""), + }; + queryMoveEvents.mockResolvedValue(singlePage([legacy])); + + const events = await Events.getMintEvents({ + startTime: now - 86400000, + endTime: now, + typeName: STSUI, + }); + + expect(events).toHaveLength(0); + }); +});