Skip to content

Commit 465ae06

Browse files
committed
[wip] feat: crud endpoints for Engine lite
1 parent b2a0b1a commit 465ae06

File tree

11 files changed

+392
-58
lines changed

11 files changed

+392
-58
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- CreateTable
2+
CREATE TABLE "backend_wallet_lite_access" (
3+
"id" TEXT NOT NULL,
4+
"teamId" TEXT NOT NULL,
5+
"dashboardUserAddress" TEXT NOT NULL,
6+
"accountAddress" TEXT,
7+
"signerAddress" TEXT,
8+
"encryptedJson" TEXT,
9+
"salt" TEXT NOT NULL,
10+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
11+
"deletedAt" TIMESTAMP(3),
12+
13+
CONSTRAINT "backend_wallet_lite_access_pkey" PRIMARY KEY ("id")
14+
);
15+
16+
-- CreateIndex
17+
CREATE INDEX "backend_wallet_lite_access_teamId_idx" ON "backend_wallet_lite_access"("teamId");
18+
19+
-- AddForeignKey
20+
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;

src/prisma/schema.prisma

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,29 @@ model WalletDetails {
9999
accountFactoryAddress String? @map("accountFactoryAddress") /// optional even for smart wallet, if not available default factory will be used
100100
entrypointAddress String? @map("entrypointAddress") /// optional even for smart wallet, if not available SDK will use default entrypoint
101101
102+
BackendWalletLiteAccess BackendWalletLiteAccess[]
103+
102104
@@map("wallet_details")
103105
}
104106

107+
model BackendWalletLiteAccess {
108+
id String @id @default(uuid())
109+
teamId String
110+
dashboardUserAddress String
111+
accountAddress String?
112+
signerAddress String?
113+
encryptedJson String?
114+
salt String
115+
116+
createdAt DateTime @default(now())
117+
deletedAt DateTime?
118+
119+
account WalletDetails? @relation(fields: [accountAddress], references: [address])
120+
121+
@@index([teamId])
122+
@@map("backend_wallet_lite_access")
123+
}
124+
105125
model WalletNonce {
106126
address String @map("address")
107127
chainId String @map("chainId")

src/server/middleware/logs.ts

Lines changed: 68 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,90 @@
1-
import type { FastifyInstance } from "fastify";
1+
import type { FastifyInstance, FastifyRequest } from "fastify";
22
import { stringify } from "thirdweb/utils";
33
import { logger } from "../../shared/utils/logger";
44
import { ADMIN_QUEUES_BASEPATH } from "./admin-routes";
55
import { OPENAPI_ROUTES } from "./open-api";
66

7-
const SKIP_LOG_PATHS = new Set([
8-
"",
7+
const IGNORE_LOG_PATHS = new Set([
98
"/",
109
"/favicon.ico",
1110
"/system/health",
1211
"/static",
1312
...OPENAPI_ROUTES,
14-
// Skip these routes case of importing sensitive details.
13+
]);
14+
15+
const SENSITIVE_LOG_PATHS = new Set([
1516
"/backend-wallet/import",
1617
"/configuration/wallets",
18+
"/backend-wallet/lite",
1719
]);
1820

21+
function shouldLog(request: FastifyRequest) {
22+
if (!request.routeOptions.url) {
23+
return false;
24+
}
25+
if (request.method === "OPTIONS") {
26+
return false;
27+
}
28+
if (
29+
request.method === "POST" &&
30+
SENSITIVE_LOG_PATHS.has(request.routeOptions.url)
31+
) {
32+
return false;
33+
}
34+
if (IGNORE_LOG_PATHS.has(request.routeOptions.url)) {
35+
return false;
36+
}
37+
if (request.routeOptions.url.startsWith(ADMIN_QUEUES_BASEPATH)) {
38+
return false;
39+
}
40+
return true;
41+
}
42+
1943
export function withRequestLogs(server: FastifyInstance) {
2044
server.addHook("onSend", async (request, reply, payload) => {
21-
if (
22-
request.method === "OPTIONS" ||
23-
!request.routeOptions.url ||
24-
SKIP_LOG_PATHS.has(request.routeOptions.url) ||
25-
request.routeOptions.url.startsWith(ADMIN_QUEUES_BASEPATH)
26-
) {
27-
return payload;
28-
}
29-
30-
const { method, routeOptions, headers, params, query, body } = request;
31-
const { statusCode, elapsedTime } = reply;
32-
const isError = statusCode >= 400;
45+
if (shouldLog(request)) {
46+
const { method, routeOptions, headers, params, query, body } = request;
47+
const { statusCode, elapsedTime } = reply;
48+
const isError = statusCode >= 400;
3349

34-
const extractedHeaders = {
35-
"x-backend-wallet-address": headers["x-backend-wallet-address"],
36-
"x-idempotency-key": headers["x-idempotency-key"],
37-
"x-account-address": headers["x-account-address"],
38-
"x-account-factory-address": headers["x-account-factory-address"],
39-
"x-account-salt": headers["x-account-salt"],
40-
};
50+
const extractedHeaders = {
51+
"x-backend-wallet-address": headers["x-backend-wallet-address"],
52+
"x-idempotency-key": headers["x-idempotency-key"],
53+
"x-account-address": headers["x-account-address"],
54+
"x-account-factory-address": headers["x-account-factory-address"],
55+
"x-account-salt": headers["x-account-salt"],
56+
};
4157

42-
const paramsStr =
43-
params && Object.keys(params).length
44-
? `params=${stringify(params)}`
45-
: undefined;
46-
const queryStr =
47-
query && Object.keys(query).length
48-
? `querystring=${stringify(query)}`
49-
: undefined;
50-
const bodyStr =
51-
body && Object.keys(body).length ? `body=${stringify(body)}` : undefined;
52-
const payloadStr = isError ? `payload=${payload}` : undefined;
58+
const paramsStr =
59+
params && Object.keys(params).length
60+
? `params=${stringify(params)}`
61+
: undefined;
62+
const queryStr =
63+
query && Object.keys(query).length
64+
? `querystring=${stringify(query)}`
65+
: undefined;
66+
const bodyStr =
67+
body && Object.keys(body).length
68+
? `body=${stringify(body)}`
69+
: undefined;
70+
const payloadStr = isError ? `payload=${payload}` : undefined;
5371

54-
logger({
55-
service: "server",
56-
level: isError ? "error" : "info",
57-
message: [
58-
`[Request complete - ${statusCode}]`,
59-
`method=${method}`,
60-
`path=${routeOptions.url}`,
61-
`headers=${stringify(extractedHeaders)}`,
62-
paramsStr,
63-
queryStr,
64-
bodyStr,
65-
`duration=${elapsedTime.toFixed(1)}ms`,
66-
payloadStr,
67-
].join(" "),
68-
});
72+
logger({
73+
service: "server",
74+
level: isError ? "error" : "info",
75+
message: [
76+
`[Request complete - ${statusCode}]`,
77+
`method=${method}`,
78+
`path=${routeOptions.url}`,
79+
`headers=${stringify(extractedHeaders)}`,
80+
paramsStr,
81+
queryStr,
82+
bodyStr,
83+
`duration=${elapsedTime.toFixed(1)}ms`,
84+
payloadStr,
85+
].join(" "),
86+
});
87+
}
6988

7089
return payload;
7190
});
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { type Static, Type } from "@sinclair/typebox";
2+
import type { FastifyInstance } from "fastify";
3+
import { StatusCodes } from "http-status-codes";
4+
import { checksumAddress } from "thirdweb/utils";
5+
import { getBackendWalletLiteAccess } from "../../../../shared/db/wallets/get-backend-wallet-lite-access";
6+
import { AddressSchema } from "../../../schemas/address";
7+
import { standardResponseSchema } from "../../../schemas/shared-api-schemas";
8+
import { createCustomError } from "../../../middleware/error";
9+
import {
10+
DEFAULT_ACCOUNT_FACTORY_V0_7,
11+
ENTRYPOINT_ADDRESS_v0_7,
12+
} from "thirdweb/wallets/smart";
13+
import { createSmartLocalWalletDetails } from "../../../utils/wallets/create-smart-wallet";
14+
import { updateBackendWalletLiteAccess } from "../../../../shared/db/wallets/update-backend-wallet-lite-access";
15+
16+
const requestSchema = Type.Object({
17+
teamId: Type.String({
18+
description: "Wallets are listed for this team.",
19+
}),
20+
});
21+
22+
const requestBodySchema = Type.Object({
23+
salt: Type.String(),
24+
litePassword: Type.String(),
25+
});
26+
27+
const responseSchema = Type.Object({
28+
result: Type.Object({
29+
walletAddress: Type.Union([AddressSchema, Type.Null()], {
30+
description: "The Engine Lite wallet address, if created.",
31+
}),
32+
salt: Type.String({
33+
description: "The salt used to encrypt the Engine Lite wallet address..",
34+
}),
35+
}),
36+
});
37+
38+
responseSchema.example = {
39+
result: {
40+
walletAddress: "0x....",
41+
salt: "2caaddce3d66ed4bee1a6ba9a29c98eb6d375635f62941655702bdff74939023",
42+
},
43+
};
44+
45+
export const createBackendWalletLiteRoute = async (
46+
fastify: FastifyInstance,
47+
) => {
48+
fastify.withTypeProvider().route<{
49+
Params: Static<typeof requestSchema>;
50+
Body: Static<typeof requestBodySchema>;
51+
Reply: Static<typeof responseSchema>;
52+
}>({
53+
method: "POST",
54+
url: "/backend-wallet/lite/:teamId",
55+
schema: {
56+
summary: "Create backend wallet (Lite)",
57+
description: "Create a backend wallet used for Engine Lite.",
58+
tags: ["Backend Wallet"],
59+
operationId: "createBackendWalletsLite",
60+
params: requestSchema,
61+
body: requestBodySchema,
62+
response: {
63+
...standardResponseSchema,
64+
[StatusCodes.OK]: responseSchema,
65+
},
66+
hide: true,
67+
},
68+
handler: async (req, reply) => {
69+
const dashboardUserAddress = checksumAddress(req.user.address);
70+
if (!dashboardUserAddress) {
71+
throw createCustomError(
72+
"This endpoint must be called from the thirdweb dashboard.",
73+
StatusCodes.FORBIDDEN,
74+
"DASHBOARD_AUTH_REQUIRED",
75+
);
76+
}
77+
78+
const { teamId } = req.params;
79+
const { salt, litePassword } = req.body;
80+
81+
const liteAccess = await getBackendWalletLiteAccess({ teamId });
82+
if (
83+
!liteAccess ||
84+
liteAccess.teamId !== teamId ||
85+
liteAccess.dashboardUserAddress !== dashboardUserAddress ||
86+
liteAccess.salt !== salt
87+
) {
88+
throw createCustomError(
89+
"The salt does not match the authenticated user. Try requesting a backend wallet again.",
90+
StatusCodes.BAD_REQUEST,
91+
"INVALID_LITE_WALLET_SALT",
92+
);
93+
}
94+
95+
// Generate a signer wallet and store the smart:local wallet, encrypted with `litePassword`.
96+
const walletDetails = await createSmartLocalWalletDetails({
97+
label: `${teamId} (${new Date()})`,
98+
accountFactoryAddress: DEFAULT_ACCOUNT_FACTORY_V0_7,
99+
entrypointAddress: ENTRYPOINT_ADDRESS_v0_7,
100+
encryptionPassword: litePassword,
101+
});
102+
if (!walletDetails.accountSignerAddress || !walletDetails.encryptedJson) {
103+
throw new Error(
104+
"Created smart:local wallet is missing required fields.",
105+
);
106+
}
107+
108+
await updateBackendWalletLiteAccess({
109+
id: liteAccess.id,
110+
accountAddress: walletDetails.address,
111+
signerAddress: walletDetails.accountSignerAddress,
112+
encryptedJson: walletDetails.encryptedJson,
113+
});
114+
115+
reply.status(StatusCodes.OK).send({
116+
result: {
117+
walletAddress: walletDetails.address,
118+
salt,
119+
},
120+
});
121+
},
122+
});
123+
};

0 commit comments

Comments
 (0)