Skip to content
Merged
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
8 changes: 4 additions & 4 deletions lib/translator/__tests__/persistence.dlq.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import type { RawEvent, TranslatedEvent } from "../types";
import * as Persistence from "../persistence";
import { db } from "@/lib/db/client";
import { translateEvent } from "../registry";
import { translateWithCache } from "../registry";

vi.mock("../registry", async () => {
const actual = await vi.importActual<typeof import("../registry")>("../registry");
return {
...actual,
translateEvent: vi.fn(),
translateWithCache: vi.fn(),
};
});

Expand All @@ -24,7 +24,7 @@ vi.mock("@/lib/ipfs/offloader", () => ({
})),
}));

const mockedTranslateEvent = translateEvent as unknown as vi.MockedFunction<typeof translateEvent>;
const mockedTranslateWithCache = translateWithCache as unknown as vi.MockedFunction<typeof translateWithCache>;

const event: RawEvent = {
id: "dead-letter-1",
Expand All @@ -43,7 +43,7 @@ describe("translateAndPersistEvent DLQ", () => {

it("writes a DeadLetterEvent when translation fails", async () => {
const testError = new Error("Invalid XDR payload");
mockedTranslateEvent.mockRejectedValueOnce(testError as any);
mockedTranslateWithCache.mockRejectedValueOnce(testError as any);

const createSpy = vi.spyOn(db.deadLetterEvent, "create");

Expand Down
4 changes: 3 additions & 1 deletion lib/translator/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ export function escapeHtml(str: string): string {
// ─── Sanitisation ─────────────────────────────────────────────────────────────

const MAX_PARAM_LENGTH = 512;
const XSS_DISARM_RE = /(onerror|onload|onclick|onmouseover|javascript:|vbscript:)/gi;

export function sanitizeTemplateParam(value: string): string {
if (typeof value !== "string") return "";
return escapeHtml(value.trim().slice(0, MAX_PARAM_LENGTH));
const cleaned = value.replace(XSS_DISARM_RE, "");
return escapeHtml(cleaned.trim().slice(0, MAX_PARAM_LENGTH));
}

export interface SanitizeOptions {
Expand Down
6 changes: 3 additions & 3 deletions lib/translator/custom-abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ function translateWithAbi(abi: CustomAbi, event: RawEvent, lang: Language): Tran

const matched = abi.events.find(function (eventDef: CustomAbiEvent): boolean {
return matchesEvent(topic0, decodedName, eventDef.name);
});
}) ?? (abi.events.length === 1 ? abi.events[0] : undefined);

if (!matched) return null;

Expand All @@ -190,7 +190,7 @@ function matchesEvent(topicHex: string, decodedName: string, eventName: string):
/** Renders a matched event into a human-readable sentence. */
function renderEvent(eventDef: CustomAbiEvent, event: RawEvent): string {
// Sanitize the event label — it comes from user-uploaded ABI name field
const label = sanitizeTextField(capitalize(eventDef.name), { maxLength: 64 });
const label = sanitizeTemplateParam(capitalize(eventDef.name)).slice(0, 64);

if (eventDef.fields.length === 0) {
return `${label} event emitted (${truncateHex(event.data, 8)})`;
Expand All @@ -202,7 +202,7 @@ function renderEvent(eventDef: CustomAbiEvent, event: RawEvent): string {
const parts = eventDef.fields.map(function (field: CustomAbiField, index: number): string {
const hex = positions[index] ?? "0x00";
// Sanitize field name from ABI and the rendered value from blockchain data
const safeName = sanitizeTextField(field.name, { maxLength: 64 });
const safeName = sanitizeTemplateParam(field.name).slice(0, 64);
return `${safeName}: ${renderField(field, hex)}`;
});

Expand Down
62 changes: 62 additions & 0 deletions lib/translator/registry.deprecation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { describe, it, expect } from "vitest";
import {
translateEvent,
registerBlueprint,
getContractRegistryEntry,
updateContractMetadata,
} from "./registry";
import type { RawEvent, TranslationBlueprint } from "./types";

const MOCK_CONTRACT = "CDEPRECATED000000000000000000000000000000000000000000000000";

const createMockEvent = (ledger: number): RawEvent => ({
id: `event-${ledger}`,
contractId: MOCK_CONTRACT,
topics: ["0x0000000000000000000000000000000000000000000000000000000074657374"],
data: "0x1234",
ledger,
timestamp: 123456789,
txHash: "tx-hash",
});

describe("Blueprint Deprecation and Ownership Model", () => {
it("registers a blueprint with ownership metadata and active status", () => {
const blueprint: TranslationBlueprint = {
contractId: MOCK_CONTRACT,
contractName: "Test Contract",
owner: "Open Audit Foundation",
maintainers: ["admin@openaudit.org"],
status: "active",
translate: () => ({ description: "Test translated", eventType: "Test" }),
};

registerBlueprint(blueprint);

const entry = getContractRegistryEntry(MOCK_CONTRACT);
expect(entry).toBeDefined();
expect(entry?.owner).toBe("Open Audit Foundation");
expect(entry?.maintainers).toContain("admin@openaudit.org");
expect(entry?.status).toBe("active");

const trans = translateEvent(createMockEvent(100));
expect(trans.blueprintStatus).toBe("active");
expect(trans.blueprintOwner).toBe("Open Audit Foundation");
});

it("updates contract metadata to deprecated status and reflects in translations", () => {
const updated = updateContractMetadata(MOCK_CONTRACT, {
status: "deprecated",
owner: "Legacy Maintainers",
});

expect(updated).toBe(true);

const entry = getContractRegistryEntry(MOCK_CONTRACT);
expect(entry?.status).toBe("deprecated");
expect(entry?.owner).toBe("Legacy Maintainers");

const trans = translateEvent(createMockEvent(200));
expect(trans.blueprintStatus).toBe("deprecated");
expect(trans.blueprintOwner).toBe("Legacy Maintainers");
});
});
Loading