Skip to content

[wip] feat: crud endpoints for Engine lite #811

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 11, 2024
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,20 @@
-- CreateTable
CREATE TABLE "backend_wallet_lite_access" (
"id" TEXT NOT NULL,
"teamId" TEXT NOT NULL,
"dashboardUserAddress" TEXT NOT NULL,
"accountAddress" TEXT,
"signerAddress" TEXT,
"encryptedJson" TEXT,
"salt" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deletedAt" TIMESTAMP(3),

CONSTRAINT "backend_wallet_lite_access_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "backend_wallet_lite_access_teamId_idx" ON "backend_wallet_lite_access"("teamId");

-- AddForeignKey
ALTER TABLE "backend_wallet_lite_access" ADD CONSTRAINT "backend_wallet_lite_access_accountAddress_fkey" FOREIGN KEY ("accountAddress") REFERENCES "wallet_details"("address") ON DELETE SET NULL ON UPDATE CASCADE;
20 changes: 20 additions & 0 deletions src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,29 @@ model WalletDetails {
accountFactoryAddress String? @map("accountFactoryAddress") /// optional even for smart wallet, if not available default factory will be used
entrypointAddress String? @map("entrypointAddress") /// optional even for smart wallet, if not available SDK will use default entrypoint

BackendWalletLiteAccess BackendWalletLiteAccess[]

@@map("wallet_details")
}

model BackendWalletLiteAccess {
id String @id @default(uuid())
teamId String
dashboardUserAddress String
accountAddress String?
signerAddress String?
encryptedJson String?
salt String

createdAt DateTime @default(now())
deletedAt DateTime?

account WalletDetails? @relation(fields: [accountAddress], references: [address])

@@index([teamId])
@@map("backend_wallet_lite_access")
}

model WalletNonce {
address String @map("address")
chainId String @map("chainId")
Expand Down
117 changes: 68 additions & 49 deletions src/server/middleware/logs.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,90 @@
import type { FastifyInstance } from "fastify";
import type { FastifyInstance, FastifyRequest } from "fastify";
import { stringify } from "thirdweb/utils";
import { logger } from "../../shared/utils/logger";
import { ADMIN_QUEUES_BASEPATH } from "./admin-routes";
import { OPENAPI_ROUTES } from "./open-api";

const SKIP_LOG_PATHS = new Set([
"",
const IGNORE_LOG_PATHS = new Set([
"/",
"/favicon.ico",
"/system/health",
"/static",
...OPENAPI_ROUTES,
// Skip these routes case of importing sensitive details.
]);

const SENSITIVE_LOG_PATHS = new Set([
"/backend-wallet/import",
"/configuration/wallets",
"/backend-wallet/lite",
]);

function shouldLog(request: FastifyRequest) {
if (!request.routeOptions.url) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small changes here to add the POST /backend-wallet/lite path to not log the litePassword

return false;
}
if (request.method === "OPTIONS") {
return false;
}
if (
request.method === "POST" &&
SENSITIVE_LOG_PATHS.has(request.routeOptions.url)
) {
return false;
}
if (IGNORE_LOG_PATHS.has(request.routeOptions.url)) {
return false;
}
if (request.routeOptions.url.startsWith(ADMIN_QUEUES_BASEPATH)) {
return false;
}
return true;
}

export function withRequestLogs(server: FastifyInstance) {
server.addHook("onSend", async (request, reply, payload) => {
if (
request.method === "OPTIONS" ||
!request.routeOptions.url ||
SKIP_LOG_PATHS.has(request.routeOptions.url) ||
request.routeOptions.url.startsWith(ADMIN_QUEUES_BASEPATH)
) {
return payload;
}

const { method, routeOptions, headers, params, query, body } = request;
const { statusCode, elapsedTime } = reply;
const isError = statusCode >= 400;
if (shouldLog(request)) {
const { method, routeOptions, headers, params, query, body } = request;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random whitespace changes in this file. I confirmed its using spaces now, so I must have saved it with tabs in the past. (Grrr Biome)

const { statusCode, elapsedTime } = reply;
const isError = statusCode >= 400;

const extractedHeaders = {
"x-backend-wallet-address": headers["x-backend-wallet-address"],
"x-idempotency-key": headers["x-idempotency-key"],
"x-account-address": headers["x-account-address"],
"x-account-factory-address": headers["x-account-factory-address"],
"x-account-salt": headers["x-account-salt"],
};
const extractedHeaders = {
"x-backend-wallet-address": headers["x-backend-wallet-address"],
"x-idempotency-key": headers["x-idempotency-key"],
"x-account-address": headers["x-account-address"],
"x-account-factory-address": headers["x-account-factory-address"],
"x-account-salt": headers["x-account-salt"],
};

const paramsStr =
params && Object.keys(params).length
? `params=${stringify(params)}`
: undefined;
const queryStr =
query && Object.keys(query).length
? `querystring=${stringify(query)}`
: undefined;
const bodyStr =
body && Object.keys(body).length ? `body=${stringify(body)}` : undefined;
const payloadStr = isError ? `payload=${payload}` : undefined;
const paramsStr =
params && Object.keys(params).length
? `params=${stringify(params)}`
: undefined;
const queryStr =
query && Object.keys(query).length
? `querystring=${stringify(query)}`
: undefined;
const bodyStr =
body && Object.keys(body).length
? `body=${stringify(body)}`
: undefined;
const payloadStr = isError ? `payload=${payload}` : undefined;

logger({
service: "server",
level: isError ? "error" : "info",
message: [
`[Request complete - ${statusCode}]`,
`method=${method}`,
`path=${routeOptions.url}`,
`headers=${stringify(extractedHeaders)}`,
paramsStr,
queryStr,
bodyStr,
`duration=${elapsedTime.toFixed(1)}ms`,
payloadStr,
].join(" "),
});
logger({
service: "server",
level: isError ? "error" : "info",
message: [
`[Request complete - ${statusCode}]`,
`method=${method}`,
`path=${routeOptions.url}`,
`headers=${stringify(extractedHeaders)}`,
paramsStr,
queryStr,
bodyStr,
`duration=${elapsedTime.toFixed(1)}ms`,
payloadStr,
].join(" "),
});
}

return payload;
});
Expand Down
123 changes: 123 additions & 0 deletions src/server/routes/backend-wallet/lite/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { type Static, Type } from "@sinclair/typebox";
import type { FastifyInstance } from "fastify";
import { StatusCodes } from "http-status-codes";
import { checksumAddress } from "thirdweb/utils";
import { getBackendWalletLiteAccess } from "../../../../shared/db/wallets/get-backend-wallet-lite-access";
import { AddressSchema } from "../../../schemas/address";
import { standardResponseSchema } from "../../../schemas/shared-api-schemas";
import { createCustomError } from "../../../middleware/error";
import {
DEFAULT_ACCOUNT_FACTORY_V0_7,
ENTRYPOINT_ADDRESS_v0_7,
} from "thirdweb/wallets/smart";
import { createSmartLocalWalletDetails } from "../../../utils/wallets/create-smart-wallet";
import { updateBackendWalletLiteAccess } from "../../../../shared/db/wallets/update-backend-wallet-lite-access";

const requestSchema = Type.Object({
teamId: Type.String({
description: "Wallets are listed for this team.",
}),
});

const requestBodySchema = Type.Object({
salt: Type.String(),
litePassword: Type.String(),
});

const responseSchema = Type.Object({
result: Type.Object({
walletAddress: Type.Union([AddressSchema, Type.Null()], {
description: "The Engine Lite wallet address, if created.",
}),
salt: Type.String({
description: "The salt used to encrypt the Engine Lite wallet address..",
}),
}),
});

responseSchema.example = {
result: {
walletAddress: "0x....",
salt: "2caaddce3d66ed4bee1a6ba9a29c98eb6d375635f62941655702bdff74939023",
},
};

export const createBackendWalletLiteRoute = async (
fastify: FastifyInstance,
) => {
fastify.withTypeProvider().route<{
Params: Static<typeof requestSchema>;
Body: Static<typeof requestBodySchema>;
Reply: Static<typeof responseSchema>;
}>({
method: "POST",
url: "/backend-wallet/lite/:teamId",
schema: {
summary: "Create backend wallet (Lite)",
description: "Create a backend wallet used for Engine Lite.",
tags: ["Backend Wallet"],
operationId: "createBackendWalletsLite",
params: requestSchema,
body: requestBodySchema,
response: {
...standardResponseSchema,
[StatusCodes.OK]: responseSchema,
},
hide: true,
},
handler: async (req, reply) => {
const dashboardUserAddress = checksumAddress(req.user.address);
if (!dashboardUserAddress) {
throw createCustomError(
"This endpoint must be called from the thirdweb dashboard.",
StatusCodes.FORBIDDEN,
"DASHBOARD_AUTH_REQUIRED",
);
}

const { teamId } = req.params;
const { salt, litePassword } = req.body;

const liteAccess = await getBackendWalletLiteAccess({ teamId });
if (
!liteAccess ||
liteAccess.teamId !== teamId ||
liteAccess.dashboardUserAddress !== dashboardUserAddress ||
liteAccess.salt !== salt
) {
throw createCustomError(
"The salt does not match the authenticated user. Try requesting a backend wallet again.",
StatusCodes.BAD_REQUEST,
"INVALID_LITE_WALLET_SALT",
);
}

// Generate a signer wallet and store the smart:local wallet, encrypted with `litePassword`.
const walletDetails = await createSmartLocalWalletDetails({
label: `${teamId} (${new Date()})`,
accountFactoryAddress: DEFAULT_ACCOUNT_FACTORY_V0_7,
entrypointAddress: ENTRYPOINT_ADDRESS_v0_7,
encryptionPassword: litePassword,
});
if (!walletDetails.accountSignerAddress || !walletDetails.encryptedJson) {
throw new Error(
"Created smart:local wallet is missing required fields.",
);
}

await updateBackendWalletLiteAccess({
id: liteAccess.id,
accountAddress: walletDetails.address,
signerAddress: walletDetails.accountSignerAddress,
encryptedJson: walletDetails.encryptedJson,
});

reply.status(StatusCodes.OK).send({
result: {
walletAddress: walletDetails.address,
salt,
},
});
},
});
};
Loading
Loading