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
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { NextFunction, Request, Response } from "express";

const mocks = vi.hoisted(() => ({
isPhase2EvidenceAdmin: vi.fn(),
exportUserIdentityEvidenceAdmin: vi.fn(),
}));

vi.mock("../../../../shared/services/transaction-evidence.service", () => ({
isPhase2EvidenceAdmin: mocks.isPhase2EvidenceAdmin,
}));

vi.mock("../../../../shared/services/user-identity-evidence.service", () => ({
exportUserIdentityEvidenceAdmin:
mocks.exportUserIdentityEvidenceAdmin,
}));

import * as controller from "../avax-swap.controller";

function createResponse() {
const setHeader = vi.fn();
const json = vi.fn();

return {
response: {
setHeader,
json,
} as unknown as Response,
setHeader,
json,
};
}

async function invokeController(
handler: unknown,
req: Request,
res: Response
): Promise<ReturnType<typeof vi.fn>> {
const next = vi.fn();

(
handler as (
req: Request,
res: Response,
next: NextFunction
) => void
)(req, res, next as unknown as NextFunction);

await vi.waitFor(() => {
expect(
next.mock.calls.length > 0 ||
(res.json as unknown as ReturnType<typeof vi.fn>).mock.calls.length > 0
).toBe(true);
});

return next;
}

describe("user estate administrative evidence export controller", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("exports the complete user estate for an authenticated Phase 2 evidence administrator", async () => {
const evidence = {
schemaVersion: "1.0",
evidenceType: "panoramablock-user-identity-wallet-evidence",
summary: {
profileCount: 3,
walletCount: 4,
userCount: 4,
},
};

mocks.isPhase2EvidenceAdmin.mockReturnValue(true);
mocks.exportUserIdentityEvidenceAdmin.mockResolvedValue(evidence);

const req = {
verifiedAddress:
"0x1111111111111111111111111111111111111111",
} as unknown as Request;

const { response, setHeader, json } = createResponse();

const next = await invokeController(
(controller as Record<string, unknown>)
.exportUserEstateEvidenceAdmin,
req,
response
);

expect(next).not.toHaveBeenCalled();

expect(mocks.isPhase2EvidenceAdmin).toHaveBeenCalledWith(
"0x1111111111111111111111111111111111111111"
);

expect(
mocks.exportUserIdentityEvidenceAdmin
).toHaveBeenCalledTimes(1);

expect(setHeader).toHaveBeenCalledWith(
"Content-Type",
"application/json"
);

expect(setHeader).toHaveBeenCalledWith(
"Content-Disposition",
'attachment; filename="panoramablock-user-estate-evidence.json"'
);

expect(json).toHaveBeenCalledWith(evidence);
});

it("refuses the user estate export when the authenticated wallet is not a Phase 2 evidence administrator", async () => {
mocks.isPhase2EvidenceAdmin.mockReturnValue(false);

const req = {
verifiedAddress:
"0x2222222222222222222222222222222222222222",
} as unknown as Request;

const { response } = createResponse();

const next = await invokeController(
(controller as Record<string, unknown>)
.exportUserEstateEvidenceAdmin,
req,
response
);

expect(mocks.isPhase2EvidenceAdmin).toHaveBeenCalledWith(
"0x2222222222222222222222222222222222222222"
);

expect(
mocks.exportUserIdentityEvidenceAdmin
).not.toHaveBeenCalled();

expect(next).toHaveBeenCalledTimes(1);

const error = next.mock.calls[0][0];

expect(error).toMatchObject({
code: "PHASE2_ADMIN_FORBIDDEN",
status: 403,
});
});
});
46 changes: 46 additions & 0 deletions backend/src/modules/avax-swap/controllers/avax-swap.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {
recordEvidenceExecutionOutcome
} from "../../../shared/services/transaction-evidence.service";
import { AppError } from "../../../shared/errorCodes";
import {
exportUserIdentityEvidenceAdmin,
} from "../../../shared/services/user-identity-evidence.service";
import {
beginAvaxBridgeEvidence,
beginAvaxBridgeDestinationEvidence,
Expand Down Expand Up @@ -443,6 +446,49 @@ export const getEvidenceAdminStatus = asyncHandler(
);


export const exportUserEstateEvidenceAdmin = asyncHandler(
async (req: Request, res: Response) => {
const verifiedAddress =
(req as any).verifiedAddress as string;

if (!isPhase2EvidenceAdmin(verifiedAddress)) {
throw new AppError("PHASE2_ADMIN_FORBIDDEN");
}

try {
const evidenceExport =
await exportUserIdentityEvidenceAdmin();

res.setHeader(
"Content-Type",
"application/json"
);

res.setHeader(
"Content-Disposition",
`attachment; filename="panoramablock-user-estate-evidence.json"`
);

res.json(evidenceExport);
} catch (err) {
if (err instanceof AppError) {
throw err;
}

const message =
err instanceof Error
? err.message
: "User estate evidence export failed";

throw new AppError(
"INTERNAL_ERROR",
message
);
}
}
);


export const exportEvidenceAdmin = asyncHandler(
async (req: Request, res: Response) => {
const verifiedAddress =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { avaxSwapRoutes } from "../avax-swap.routes";

describe("user estate administrative evidence route", () => {
it("registers the authenticated administrative user-estate export endpoint", () => {
const route = (avaxSwapRoutes as any).stack.find(
(layer: any) =>
layer.route?.path ===
"/evidence/admin/user-estate/export/:userAddress" &&
layer.route?.methods?.get === true
);

expect(route).toBeDefined();

const handlerNames = route.route.stack.map(
(layer: any) => layer.handle?.name
);

expect(handlerNames).toContain("requireWalletAuth");
});
});
13 changes: 13 additions & 0 deletions backend/src/modules/avax-swap/routes/avax-swap.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,19 @@ avaxSwapRoutes.get(
ctrl.exportEvidenceAdmin
);

/**
* GET /avax/swap/evidence/admin/user-estate/export/:userAddress
* Returns an administrative evidence export of the PanoramaBlock user estate.
* Requires wallet signature authentication and Phase 2 admin allowlist membership.
*/
avaxSwapRoutes.get(
"/evidence/admin/user-estate/export/:userAddress",
validateAddress("userAddress", "params"),
requireWalletAuth,
executionTimeout(),
ctrl.exportUserEstateEvidenceAdmin
);

/**
* GET /avax/swap/evidence/:correlationId
* Returns the complete durable evidence chain from the DB.
Expand Down
Loading
Loading