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
7 changes: 4 additions & 3 deletions apps/s03-indexer/src/mappings/mappingHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ async function handleMarketCreated(event: DecodedEvent): Promise<void> {
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,
Expand Down Expand Up @@ -775,10 +775,11 @@ async function savePositionChange(
account: string,
changeType: string,
): Promise<void> {
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,
Expand Down
70 changes: 70 additions & 0 deletions apps/s03-indexer/tests/fixtures/reorged-ledger-sequence.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
}
121 changes: 82 additions & 39 deletions apps/s03-indexer/tests/mappingHandlers.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, test } from "bun:test";
import { readFileSync } from "fs";
import {
decodeAddress,
decodeBoolean,
Expand All @@ -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<string, Record<string, unknown>>;
type ReorgFixtureEvent = {
id: string;
eventName: string;
ledger: number;
transactionHash: string;
named: Record<string, string | boolean>;
};
type ReorgFixture = {
orphaned: ReorgFixtureEvent[];
canonical: ReorgFixtureEvent[];
};

const buckets = new Map<string, StoreBucket>();
const logs: string[] = [];
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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<string, string | boolean>;

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<string, string | boolean> = {},
Expand Down