Skip to content

Commit cf71472

Browse files
committed
add contract/write and create access token routes
1 parent da8d89a commit cf71472

File tree

14 files changed

+332
-19
lines changed

14 files changed

+332
-19
lines changed

src/db/derived-schemas.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { createSelectSchema } from "drizzle-zod";
2-
import { transactions } from "./schema";
2+
import { tokens, transactions } from "./schema";
33

44
export const transactionDbEntrySchema = createSelectSchema(transactions);
5+
export const accessTokenDbEntrySchema = createSelectSchema(tokens);

src/executors/eoa/index.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,7 @@ import {
3737
} from "../../lib/errors";
3838
import { keccak256 } from "ox/Hash";
3939
import type { TransactionReceipt } from "thirdweb/transaction";
40-
import {
41-
checkEoaIssues,
42-
type EoaIssues,
43-
type EoaIssueCode,
44-
setOutOfGasIssue,
45-
} from "./issues";
40+
import { checkEoaIssues, type EoaIssues, setOutOfGasIssue } from "./issues";
4641
import { recordTransactionAttempt } from "./attempts";
4742

4843
const sendLogger = initializeLogger("executor:eoa:send");

src/executors/external-bundler/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export function execute(request: ExecutionRequest) {
137137
createAndSignUserOp({
138138
adminAccount: executionOptions.signer,
139139
client,
140+
waitForDeployment: false,
140141
smartWalletOptions: {
141142
// if we don't provide a factory address, SDK uses thirdweb's default account factory
142143
// user might be using a custom factory, and they might not provide one, so executor entrypoint should try to infer it

src/lib/errors.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,11 @@ export type EngineErr =
212212
| AccountErr;
213213

214214
export function isEngineErr(err: unknown): err is EngineErr {
215-
return "kind" in (err as EngineErr) && "code" in (err as EngineErr);
215+
return (
216+
typeof err === "object" &&
217+
"kind" in (err as EngineErr) &&
218+
"code" in (err as EngineErr)
219+
);
216220
}
217221

218222
export class EngineHttpException extends HTTPException {

src/lib/permissions.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ import {
1111
import { getAddressResult } from "./validation/address";
1212
import { db } from "../db/connection";
1313
import { LRUCache } from "lru-cache";
14+
import { adminAccount } from "./admin-account";
1415

1516
const permissionCache = new LRUCache<string, PermissionDbEntry>({
1617
max: 1024,
1718
});
1819

1920
export function getPermissions(
20-
address: Address,
21+
address: Address
2122
): ResultAsync<PermissionDbEntry, DbErr | PermissionsErr> {
2223
const cached = permissionCache.get(address);
2324
if (cached) {
@@ -28,7 +29,7 @@ export function getPermissions(
2829
db.query.permissions.findFirst({
2930
where: (permissions, { eq }) => eq(permissions.accountAddress, address),
3031
}),
31-
mapDbError,
32+
mapDbError
3233
)
3334
.andTee((permission) => {
3435
permission && permissionCache.set(address, permission);
@@ -40,7 +41,7 @@ export function getPermissions(
4041
kind: "permissions",
4142
code: "no_permissions",
4243
status: 400,
43-
} as PermissionsErr),
44+
} as PermissionsErr)
4445
);
4546
}
4647

@@ -55,7 +56,16 @@ export function checkPermissions({
5556
AuthErr | ValidationErr | DbErr
5657
> {
5758
return getAddressResult("Invalid User Address")(rawAddress)
58-
.asyncAndThen((address) => getPermissions(address))
59+
.asyncAndThen((address) => {
60+
if (address === adminAccount.address) {
61+
return okAsync({
62+
accountAddress: address,
63+
permissions: "ADMIN" as const,
64+
label: "Admin",
65+
});
66+
}
67+
return getPermissions(address);
68+
})
5969
.mapErr((error) => {
6070
if (error.kind === "permissions") {
6171
return {

src/server/engine.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { accountRouter } from "./account";
2828
import { setupQueuesUiRoutes } from "./routes/queues";
2929
import { transactionsRoutes } from "./routes/transactions";
3030
import { correlationId } from "./middleware/correlation-id";
31+
import contractRoutes from "./routes/contract";
32+
import authRoutes from "./routes/auth";
3133

3234
const engineServer = new Hono();
3335
const publicRoutes = new Hono();
@@ -125,10 +127,14 @@ engineServer.use(
125127
engineServer.route("/accounts", accountsRoutes);
126128
engineServer.route("/transactions", transactionsRoutes);
127129
engineServer.route("/account", accountRouter);
130+
engineServer.route("/contract", contractRoutes);
131+
engineServer.route("/auth", authRoutes);
128132

129133
engineServer.onError((err, c) => {
130134
if (err instanceof HTTPException) {
131135
if (err instanceof EngineHttpException) {
136+
defaultLogger.error("[Debug] EngineHttpException", err);
137+
132138
const engineErr = err.engineErr;
133139
return c.json(
134140
{

src/server/middleware/auth/keypair.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { env } from "../../../lib/env";
1111
import { HTTPException } from "hono/http-exception";
1212
import { extractJwt } from "./shared";
1313
import type { KeypairDbEntry } from "../../../db/types";
14-
import { createHash } from "crypto";
14+
import { createHash } from "node:crypto";
1515

1616
function checkKeypairAuth({
1717
jwt,
@@ -23,7 +23,6 @@ function checkKeypairAuth({
2323
// First decode without verification to get the keypair info
2424
const decoded = decode(jwt);
2525

26-
2726
// Get keypair from our DB
2827
return getKeypair({
2928
publicKey: decoded.payload.iss as string,
@@ -34,7 +33,7 @@ function checkKeypairAuth({
3433
const actualBodyHashBytes = Buffer.from(actualBodyHash, "hex");
3534
const expectedBodyHashBytes = Buffer.from(
3635
decoded.payload.bodyHash as string,
37-
"hex",
36+
"hex"
3837
);
3938

4039
if (!actualBodyHashBytes.equals(expectedBodyHashBytes)) {
@@ -59,8 +58,8 @@ function checkKeypairAuth({
5958
kind: "auth",
6059
code: "invalid_signature",
6160
status: 401,
62-
} as const),
63-
),
61+
} as const)
62+
)
6463
)
6564
.mapErr((err) => {
6665
if (err.kind === "keypair") {
@@ -84,11 +83,11 @@ export const keypairAuth = createMiddleware(async (c, next) => {
8483
const actualBodyHash = await c.req
8584
.arrayBuffer()
8685
.then((buffer) =>
87-
createHash("sha256").update(new Uint8Array(buffer)).digest("hex"),
86+
createHash("sha256").update(new Uint8Array(buffer)).digest("hex")
8887
);
8988

9089
const result = await extractJwt(c.req.header("authorization")).asyncAndThen(
91-
(jwt) => checkKeypairAuth({ jwt, actualBodyHash }),
90+
(jwt) => checkKeypairAuth({ jwt, actualBodyHash })
9291
);
9392

9493
if (result.isErr()) {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Permission } from "../../../db/types";
2+
3+
export type AuthContext = {
4+
Variables: {
5+
user?: {
6+
address: string;
7+
permissions: Permission[];
8+
};
9+
keypair?: {
10+
createdAt: Date;
11+
hash: string;
12+
label: string | null;
13+
updatedAt: Date;
14+
deletedAt: Date | null;
15+
publicKey: string;
16+
algorithm: "RS256" | "ES256" | "PS256";
17+
};
18+
};
19+
};
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { describeRoute } from "hono-openapi";
2+
import { authRoutesFactory } from "./factory";
3+
import { resolver, validator } from "hono-openapi/zod";
4+
import {
5+
engineErrToHttpException,
6+
mapDbError,
7+
zErrorMapper,
8+
} from "../../../lib/errors";
9+
import * as z from "zod";
10+
import { adminAccount } from "../../../lib/admin-account";
11+
import { config } from "../../../lib/config";
12+
import { encodeJWT } from "thirdweb/utils";
13+
import { db } from "../../../db/connection";
14+
import { tokens } from "../../../db/schema";
15+
import { ResultAsync } from "neverthrow";
16+
import { accessTokenDbEntrySchema } from "../../../db/derived-schemas";
17+
import { wrapResponseSchema } from "../../schemas/shared-api-schemas";
18+
19+
export const createAccessTokenRoute = authRoutesFactory.createHandlers(
20+
describeRoute({
21+
tags: ["Auth"],
22+
summary: "Create Access Token",
23+
description: "Create an access token for a client",
24+
responses: {
25+
200: {
26+
description: "Access token created successfully",
27+
content: {
28+
"application/json": {
29+
schema: resolver(
30+
wrapResponseSchema(
31+
accessTokenDbEntrySchema.extend({
32+
accessToken: z.string().openapi({
33+
description: "The access token created",
34+
}),
35+
})
36+
)
37+
),
38+
},
39+
},
40+
},
41+
},
42+
}),
43+
validator(
44+
"json",
45+
z.object({
46+
label: z.string(),
47+
}),
48+
zErrorMapper
49+
),
50+
async (c) => {
51+
const { label } = c.req.valid("json");
52+
53+
const user = c.get("user");
54+
55+
const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 365 * 100);
56+
57+
const id = crypto.randomUUID();
58+
const jwt = await encodeJWT({
59+
account: adminAccount,
60+
payload: {
61+
iss: adminAccount.address,
62+
sub: user?.address ?? adminAccount.address,
63+
aud: config.authDomain,
64+
nbf: new Date(),
65+
// Set to expire in 100 years
66+
exp: expiresAt,
67+
iat: new Date(),
68+
ctx: {
69+
permissions: user?.permissions ?? ["ADMIN"],
70+
},
71+
jti: id,
72+
},
73+
});
74+
75+
const dbResult = await ResultAsync.fromPromise(
76+
db
77+
.insert(tokens)
78+
.values({
79+
id,
80+
accountAddress: user?.address ?? adminAccount.address,
81+
isAccessToken: true,
82+
label: label,
83+
expiresAt,
84+
tokenMask: `${jwt.slice(0, 10)}...${jwt.slice(-10)}`,
85+
})
86+
.returning(),
87+
mapDbError
88+
);
89+
90+
if (dbResult.isErr()) {
91+
throw engineErrToHttpException(dbResult.error);
92+
}
93+
94+
const createdToken = dbResult.value[0];
95+
96+
if (!createdToken) {
97+
throw engineErrToHttpException({
98+
kind: "database",
99+
code: "query_failed",
100+
status: 500,
101+
message: "Failed to create access token",
102+
});
103+
}
104+
105+
return c.json({ result: { accessToken: jwt, ...createdToken } });
106+
}
107+
);

src/server/routes/auth/factory.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { createFactory } from "hono/factory";
2+
import type { AuthContext } from "../../middleware/auth/types";
3+
4+
export const authRoutesFactory = createFactory<AuthContext>();

0 commit comments

Comments
 (0)