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
2 changes: 0 additions & 2 deletions apps/sdp-web/messages/en/dashboard-custody.json
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,6 @@
"rpcProviderSaveMismatch": "RPC provider save mismatch (requested {requested}, persisted {persisted}).",
"rpcSettingsSaved": "RPC settings saved.",
"failedToTestRpcProvider": "Failed to test RPC provider.",
"rpcTestFailed": "RPC test failed.",
"rpcTestFailedStatus": "RPC test failed ({status}).",
"rpcTestMismatch": "RPC test mismatch (requested {requested}, resolved {resolved}).",
"rpcUpstreamReturned": "RPC upstream returned {status} {statusText}.",
"rpcTestPassed": "RPC test passed ({status} {statusText}) in {latency}ms.",
Expand Down
5 changes: 0 additions & 5 deletions apps/sdp-web/messages/en/dashboard-issuance.json
Original file line number Diff line number Diff line change
Expand Up @@ -510,13 +510,8 @@
"noPauseAuthorityConfigured": "No pause authority is configured. Set a pausable authority or mint authority first.",
"freezingDisabled": "Freezing is disabled for this token.",
"noFreezeAuthorityConfigured": "No freeze authority is configured.",
"unknownError": "Unknown error",
"unknown": "unknown",
"ok": "ok",
"executionRouteFailed": "Execution route failed ({status})",
"actionFailed": "{action} failed ({status}): {error}",
"actionSucceeded": "{action} succeeded ({status})",
"requestFailed": "Request failed",
"mintAuthorityHelper": "Can mint new tokens.",
"freezeAuthorityHelper": "Can freeze and unfreeze token accounts.",
"metadataAuthorityHelper": "Can update token metadata.",
Expand Down
26 changes: 9 additions & 17 deletions apps/sdp-web/playwright/tests/issuance.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,24 +147,16 @@ async function waitForActionResponse(
trigger: () => Promise<void>
): Promise<void> {
const responsePromise = page.waitForResponse(
(response) => {
const request = response.request();
const postData = request.postData() ?? "";
return (
response.url().endsWith("/api/playground/execute") &&
request.method() === "POST" &&
postData.includes(`"method":"${options.method}"`) &&
postData.includes(options.pathIncludes)
);
},
(response) =>
response.request().method() === options.method &&
new URL(response.url()).pathname.includes(options.pathIncludes),
{ timeout: 180_000 }
);

await trigger();
const response = await responsePromise;
expect(response.ok()).toBe(true);
const payload = (await response.json().catch(() => null)) as { ok?: boolean } | null;
expect(payload?.ok, JSON.stringify(payload)).toBe(true);
const body = await response.text().catch(() => "");
expect(response.ok(), body).toBe(true);
}

async function confirmAction(page: Page, confirmButtonLabel: string): Promise<void> {
Expand Down Expand Up @@ -468,7 +460,7 @@ test.describe
page,
{
method: "POST",
pathIncludes: `/v1/issuance/tokens/${fixtures.tokens.allowlisted.id}/allowlist`,
pathIncludes: `/api/dashboard/issuance/tokens/${fixtures.tokens.allowlisted.id}/allowlist`,
},
async () => {
await page.getByRole("button", { name: "Add allowlist entry" }).click();
Expand All @@ -487,7 +479,7 @@ test.describe
page,
{
method: "DELETE",
pathIncludes: `/v1/issuance/tokens/${fixtures.tokens.allowlisted.id}/allowlist/`,
pathIncludes: `/api/dashboard/issuance/tokens/${fixtures.tokens.allowlisted.id}/allowlist/`,
},
async () => {
await allowlistEntry.getByRole("button", { name: "Remove entry" }).click();
Expand Down Expand Up @@ -515,7 +507,7 @@ test.describe
page,
{
method: "POST",
pathIncludes: `/v1/issuance/tokens/${authorityTokenId}/authority`,
pathIncludes: `/api/dashboard/issuance/tokens/${authorityTokenId}/authority`,
},
async () => {
await page.getByRole("button", { name: "Save authority" }).click();
Expand All @@ -540,7 +532,7 @@ test.describe
page,
{
method: "POST",
pathIncludes: `/v1/issuance/tokens/${authorityTokenId}/authority`,
pathIncludes: `/api/dashboard/issuance/tokens/${authorityTokenId}/authority`,
},
async () => {
await page.getByRole("button", { name: "Yes, set to None" }).click();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NextResponse } from "next/server";
import { proxyToSdpApi } from "@/lib/sdp-api";

const TOKEN_POST_ACTIONS = {
deploy: "deploy",
mint: "mint",
burn: "burn",
seize: "seize",
"force-burn": "force-burn",
authority: "authority",
freeze: "freeze",
unfreeze: "unfreeze",
pause: "pause",
unpause: "unpause",
"refresh-supply": "supply/refresh",
} as const satisfies Record<string, string>;

type TokenPostAction = keyof typeof TOKEN_POST_ACTIONS;

type RouteContext = {
params: Promise<{ tokenId: string; action: string }>;
};

/**
* Narrows a route param to a known token POST action.
* @param action - The raw `[action]` route segment.
* @returns Whether `action` is a key of `TOKEN_POST_ACTIONS`.
*/
function isTokenPostAction(action: string): action is TokenPostAction {
return Object.hasOwn(TOKEN_POST_ACTIONS, action);
}

export async function POST(request: Request, context: RouteContext) {
const { tokenId, action } = await context.params;
if (!isTokenPostAction(action)) {
return NextResponse.json(
{ error: { message: "Token action is not supported" } },
{ status: 404 }
);
}

return proxyToSdpApi({
request,
traceSource: `route.dashboard.issuance.token.${action}`,
path: `/v1/issuance/tokens/${encodeURIComponent(tokenId)}/${TOKEN_POST_ACTIONS[action]}`,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

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

vi.mock("@/lib/sdp-api", () => ({
proxyToSdpApi: mocks.proxyToSdpApi,
}));

import { POST } from "./route";

function tokenActionRequest(): Request {
return new Request("https://dashboard.example.com/api/dashboard/issuance/tokens/tok_1/mint", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mint: { destination: "dest", amount: "1" } }),
});
}

describe("POST /api/dashboard/issuance/tokens/[tokenId]/[action]", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it.each([
"not-a-real-action",
"__proto__",
"constructor",
"toString",
])("returns 404 for unsupported action %s without proxying", async (action) => {
const response = await POST(tokenActionRequest(), {
params: Promise.resolve({ tokenId: "tok_1", action }),
});

expect(response.status).toBe(404);
await expect(response.json()).resolves.toEqual({
error: { message: "Token action is not supported" },
});
expect(mocks.proxyToSdpApi).not.toHaveBeenCalled();
});

it.each([
["deploy", "deploy"],
["mint", "mint"],
["burn", "burn"],
["seize", "seize"],
["force-burn", "force-burn"],
["authority", "authority"],
["freeze", "freeze"],
["unfreeze", "unfreeze"],
["pause", "pause"],
["unpause", "unpause"],
["refresh-supply", "supply/refresh"],
])("proxies action %s to /v1/issuance/tokens/:tokenId/%s", async (action, sdpApiSegment) => {
mocks.proxyToSdpApi.mockResolvedValue(new Response(null, { status: 200 }));
const request = tokenActionRequest();

await POST(request, { params: Promise.resolve({ tokenId: "tok_1", action }) });

expect(mocks.proxyToSdpApi).toHaveBeenCalledWith({
request,
traceSource: `route.dashboard.issuance.token.${action}`,
path: `/v1/issuance/tokens/tok_1/${sdpApiSegment}`,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { proxyToSdpApi } from "@/lib/sdp-api";

type RouteContext = {
params: Promise<{ tokenId: string; entryId: string }>;
};

export async function DELETE(request: Request, context: RouteContext) {
const { tokenId, entryId } = await context.params;

return proxyToSdpApi({
request,
traceSource: "route.dashboard.issuance.token.allowlist.remove",
path: `/v1/issuance/tokens/${encodeURIComponent(tokenId)}/allowlist/${encodeURIComponent(entryId)}`,
});
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { TokenAllowlistEntry } from "@sdp/types";
import { NextResponse } from "next/server";
import { createTimedTrace } from "@/lib/request-tracing";
import { createSdpApiClient } from "@/lib/sdp-api";
import { createSdpApiClient, proxyToSdpApi } from "@/lib/sdp-api";

function parseErrorMessage(body: string): string {
try {
Expand Down Expand Up @@ -95,3 +95,13 @@ export async function GET(request: Request, { params }: { params: Promise<{ toke
);
}
}

export async function POST(request: Request, { params }: { params: Promise<{ tokenId: string }> }) {
const { tokenId } = await params;

return proxyToSdpApi({
request,
traceSource: "route.dashboard.issuance.token.allowlist.add",
path: `/v1/issuance/tokens/${encodeURIComponent(tokenId)}/allowlist`,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { proxyToSdpApi } from "@/lib/sdp-api";

type RouteContext = {
params: Promise<{ tokenId: string }>;
};

export async function PATCH(request: Request, context: RouteContext) {
const { tokenId } = await context.params;

return proxyToSdpApi({
request,
traceSource: "route.dashboard.issuance.token.update",
path: `/v1/issuance/tokens/${encodeURIComponent(tokenId)}`,
});
}
9 changes: 9 additions & 0 deletions apps/sdp-web/src/app/api/dashboard/settings/rpc-test/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { proxyToSdpApi } from "@/lib/sdp-api";

export async function POST(request: Request) {
return proxyToSdpApi({
request,
traceSource: "route.dashboard.settings.rpc-test",
path: "/v1/rpc/test",
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ export function useTokenOperations({
const frozenAccountsTotal = resolvedSupportingData.frozenAccountsTotal;
const frozenAccountsHasMore = resolvedSupportingData.frozenAccountsHasMore;

const tokenBasePath = `/v1/issuance/tokens/${token.id}`;
const tokenBasePath = `/api/dashboard/issuance/tokens/${token.id}`;
const explorerHref = getExplorerHref(token.mintAddress);
const canDeployToken = token.status === "pending" && !token.mintAddress;
const {
Expand Down Expand Up @@ -548,7 +548,7 @@ export function useTokenOperations({
runAction({
label: t("DashboardIssuance.management.refreshSupply"),
method: "POST",
path: `${tokenBasePath}/supply/refresh`,
path: `${tokenBasePath}/refresh-supply`,
body: {},
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ export function TokenManagementWorkspace({
const frozenAccountsTotal = resolvedSupportingData.frozenAccountsTotal;
const frozenAccountsHasMore = resolvedSupportingData.frozenAccountsHasMore;

const tokenBasePath = `/v1/issuance/tokens/${token.id}`;
const tokenBasePath = `/api/dashboard/issuance/tokens/${token.id}`;
const explorerHref = getExplorerHref(token.mintAddress);
const canDeployToken = token.status === "pending" && !token.mintAddress;
const {
Expand Down Expand Up @@ -754,7 +754,7 @@ export function TokenManagementWorkspace({
runAction({
label: t("DashboardIssuance.management.refreshSupply"),
method: "POST",
path: `${tokenBasePath}/supply/refresh`,
path: `${tokenBasePath}/refresh-supply`,
body: {},
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,6 @@ export interface ActionExecutionInput {
body?: unknown;
}

export interface ExecuteRouteResponse {
ok?: boolean;
status?: number;
body?: unknown;
error?: string;
}

export interface ActionExecutionResult {
ok: boolean;
message: string;
Expand Down
Loading
Loading