diff --git a/apps/s03-indexer/src/mappings/mappingHandlers.ts b/apps/s03-indexer/src/mappings/mappingHandlers.ts index 4907cb5..e34750a 100644 --- a/apps/s03-indexer/src/mappings/mappingHandlers.ts +++ b/apps/s03-indexer/src/mappings/mappingHandlers.ts @@ -437,7 +437,7 @@ async function handleMarketCreated(event: DecodedEvent): Promise { createdTransactionHash: event.transactionHash, }); - const snapshotId = deterministicId("market-config", event.id); + const snapshotId = deterministicId("market-config", market.key, event.ledger.toString()); const snapshot = await upsert(MarketConfigSnapshot, snapshotId, { id: snapshotId, marketId: market.id, @@ -775,10 +775,11 @@ async function savePositionChange( account: string, changeType: string, ): Promise { - const id = deterministicId("position-change", event.id); + const key = fieldString(event, ["position_key", "positionKey", "key"], POSITION_INCREASE_INDEX.key) ?? event.id; + const id = deterministicId("position-change", key, event.ledger.toString(), changeType); const change = await upsert(PositionChange, id, { id, - key: fieldString(event, ["position_key", "positionKey", "key"], POSITION_INCREASE_INDEX.key) ?? event.id, + key, marketId: marketIdValue, positionId, account, diff --git a/apps/s03-indexer/tests/fixtures/reorged-ledger-sequence.json b/apps/s03-indexer/tests/fixtures/reorged-ledger-sequence.json new file mode 100644 index 0000000..64e395b --- /dev/null +++ b/apps/s03-indexer/tests/fixtures/reorged-ledger-sequence.json @@ -0,0 +1,70 @@ +{ + "orphaned": [ + { + "id": "orphan-pos-inc", + "eventName": "pos_inc", + "ledger": 200, + "transactionHash": "tx-orphan-pos", + "named": { + "position_key": "reorg-pos-1", + "market": "MARKET_TOKEN", + "account": "ACCOUNT", + "collateral_token": "MARKET_TOKEN", + "is_long": true, + "size_delta_usd": "100", + "next_size_usd": "100", + "next_collateral_amount": "5", + "execution_price": "10" + } + }, + { + "id": "orphan-ord-crt", + "eventName": "ord_crt", + "ledger": 200, + "transactionHash": "tx-orphan-order", + "named": { + "key": "reorg-order-1", + "market": "MARKET_TOKEN", + "account": "ACCOUNT", + "order_type": "LimitIncrease", + "is_long": true, + "size_delta_usd": "100", + "acceptable_price": "10" + } + } + ], + "canonical": [ + { + "id": "canonical-pos-inc", + "eventName": "pos_inc", + "ledger": 200, + "transactionHash": "tx-canonical-pos", + "named": { + "position_key": "reorg-pos-1", + "market": "MARKET_TOKEN", + "account": "ACCOUNT", + "collateral_token": "MARKET_TOKEN", + "is_long": true, + "size_delta_usd": "250", + "next_size_usd": "250", + "next_collateral_amount": "8", + "execution_price": "12" + } + }, + { + "id": "canonical-ord-crt", + "eventName": "ord_crt", + "ledger": 200, + "transactionHash": "tx-canonical-order", + "named": { + "key": "reorg-order-1", + "market": "MARKET_TOKEN", + "account": "ACCOUNT", + "order_type": "LimitIncrease", + "is_long": true, + "size_delta_usd": "250", + "acceptable_price": "12" + } + } + ] +} diff --git a/apps/s03-indexer/tests/mappingHandlers.test.ts b/apps/s03-indexer/tests/mappingHandlers.test.ts index 9a7cc01..995b630 100644 --- a/apps/s03-indexer/tests/mappingHandlers.test.ts +++ b/apps/s03-indexer/tests/mappingHandlers.test.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, test } from "bun:test"; +import { readFileSync } from "fs"; import { decodeAddress, decodeBoolean, @@ -19,8 +20,22 @@ const handlerContract = "CDWOFIP4YQJGMCYAOWLSRBAWN2OTJUG2I5WOFC32O2TX2SRU56RWBE5 const marketFactoryContract = "CBGX3EJFI3JRHSN5B533O2L5P57JFPTCRS55IPWFS5BNDXLJLXDWA5Z2"; const account = Keypair.random().publicKey(); const receiver = Keypair.random().publicKey(); +const reorgFixture = JSON.parse( + readFileSync(`${import.meta.dir}/fixtures/reorged-ledger-sequence.json`, "utf8"), +) as ReorgFixture; type StoreBucket = Map>; +type ReorgFixtureEvent = { + id: string; + eventName: string; + ledger: number; + transactionHash: string; + named: Record; +}; +type ReorgFixture = { + orphaned: ReorgFixtureEvent[]; + canonical: ReorgFixtureEvent[]; +}; const buckets = new Map(); const logs: string[] = []; @@ -402,48 +417,55 @@ describe("SO4 event dispatch", () => { expect(records("PositionChange")[0].changeType).toBe("INCREASE"); }); - test("indexes position increase with positive funding fee amount", async () => { - const positionKey = "pos-funding-1"; - await dispatchEvent( - so4Event("pos_inc", { - position_key: positionKey, - market: marketToken, - account, - collateral_token: marketToken, - is_long: true, - next_size_usd: "500000000000000000000000000000000", - next_collateral_amount: "105000000", // 100 base + 5 funding - funding_fee_amount: "5000000", // Positive funding added to margin - }), - ); + test("rewrites stale entities when a reorged ledger is replayed", async () => { + for (const fixtureEvent of reorgFixture.orphaned) { + await dispatchEvent(reorgEvent(fixtureEvent)); + } - const position = records("Position")[0]; - const change = records("PositionChange")[0]; - - expect(position.collateralAmount).toBe("105000000"); - expect(change.fundingFeeAmount).toBe("5000000"); - }); + expect(records("Position")).toHaveLength(1); + expect(records("PositionChange")).toHaveLength(1); + expect(records("Order")).toHaveLength(1); + expect(records("Position")[0].sizeUsd).toBe("100"); + expect(records("PositionChange")[0].transactionHash).toBe("tx-orphan-pos"); + expect(records("Order")[0].acceptablePrice).toBe("10"); - test("indexes position decrease with negative funding fee amount", async () => { - const positionKey = "pos-funding-2"; - await dispatchEvent( - so4Event("pos_dec", { - position_key: positionKey, - market: marketToken, - account, - collateral_token: marketToken, - is_long: false, - next_size_usd: "500000000000000000000000000000000", - next_collateral_amount: "95000000", // 100 base - 5 funding - funding_fee_amount: "-5000000", // Negative funding subtracted from margin - }), - ); + for (const fixtureEvent of reorgFixture.canonical) { + await dispatchEvent(reorgEvent(fixtureEvent)); + } + for (const fixtureEvent of reorgFixture.canonical) { + await dispatchEvent(reorgEvent(fixtureEvent)); + } - const position = records("Position")[0]; - const change = records("PositionChange")[0]; - - expect(position.collateralAmount).toBe("95000000"); - expect(change.fundingFeeAmount).toBe("-5000000"); + const [position] = records("Position"); + const [positionChange] = records("PositionChange"); + const [order] = records("Order"); + + expect(records("Position")).toHaveLength(1); + expect(records("PositionChange")).toHaveLength(1); + expect(records("Order")).toHaveLength(1); + expect(position).toMatchObject({ + id: "position:reorg-pos-1", + key: "reorg-pos-1", + sizeUsd: "250", + collateralAmount: "8", + updatedLedger: 200, + updatedTransactionHash: "tx-canonical-pos", + }); + expect(positionChange).toMatchObject({ + id: "position-change:reorg-pos-1:200:INCREASE", + key: "reorg-pos-1", + sizeDeltaUsd: "250", + executionPrice: "12", + transactionHash: "tx-canonical-pos", + }); + expect(order).toMatchObject({ + id: "order:reorg-order-1", + key: "reorg-order-1", + sizeDeltaUsd: "250", + acceptablePrice: "12", + createdLedger: 200, + createdTransactionHash: "tx-canonical-order", + }); }); test("indexes liquidation and ADL events", async () => { @@ -752,6 +774,27 @@ function so4Event( }; } +function reorgEvent(fixtureEvent: ReorgFixtureEvent): DecodedEvent { + const named = Object.fromEntries( + Object.entries(fixtureEvent.named).map(([key, value]) => [ + key, + value === "MARKET_TOKEN" ? marketToken : value === "ACCOUNT" ? account : value, + ]), + ) as Record; + + return { + ...so4Event(fixtureEvent.eventName, named), + id: fixtureEvent.id, + ledger: fixtureEvent.ledger, + timestamp: new Date("2026-06-24T12:01:00Z"), + transactionHash: fixtureEvent.transactionHash, + values: { + list: Object.values(named), + named, + }, + }; +} + function lifecyclePayload( key: string, extra: Record = {},