Skip to content

Commit 07d1d7d

Browse files
authored
[feat] update credentials (#845)
* remove auto creation and register of circle entity secret * Update wallet credentials * remove uneeded import * reuse schema
1 parent f4e728f commit 07d1d7d

File tree

7 files changed

+179
-52
lines changed

7 files changed

+179
-52
lines changed

src/server/routes/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ import { sendTransactionBatchAtomicRoute } from "./backend-wallet/send-transacti
116116
import { createWalletCredentialRoute } from "./wallet-credentials/create";
117117
import { getWalletCredentialRoute } from "./wallet-credentials/get";
118118
import { getAllWalletCredentialsRoute } from "./wallet-credentials/get-all";
119+
import { updateWalletCredentialRoute } from "./wallet-credentials/update";
119120

120121
export async function withRoutes(fastify: FastifyInstance) {
121122
// Backend Wallets
@@ -144,6 +145,7 @@ export async function withRoutes(fastify: FastifyInstance) {
144145
await fastify.register(createWalletCredentialRoute);
145146
await fastify.register(getWalletCredentialRoute);
146147
await fastify.register(getAllWalletCredentialsRoute);
148+
await fastify.register(updateWalletCredentialRoute);
147149

148150
// Configuration
149151
await fastify.register(getWalletsConfiguration);

src/server/routes/wallet-credentials/create.ts

+6-7
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@ import { createCustomError } from "../../middleware/error";
99
const requestBodySchema = Type.Object({
1010
label: Type.String(),
1111
type: Type.Literal("circle"),
12-
entitySecret: Type.Optional(
13-
Type.String({
14-
description:
15-
"32-byte hex string. If not provided, a random one will be generated.",
16-
pattern: "^[0-9a-fA-F]{64}$",
17-
}),
18-
),
12+
entitySecret: Type.String({
13+
description:
14+
"32-byte hex string. Consult https://developers.circle.com/w3s/entity-secret-management to create and register an entity secret.",
15+
pattern: "^[0-9a-fA-F]{64}$",
16+
}),
1917
isDefault: Type.Optional(
2018
Type.Boolean({
2119
description:
@@ -98,6 +96,7 @@ export const createWalletCredentialRoute = async (fastify: FastifyInstance) => {
9896
"WALLET_CREDENTIAL_ERROR",
9997
);
10098
}
99+
throw e;
101100
}
102101
},
103102
});

src/server/routes/wallet-credentials/get.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { Type, type Static } from "@sinclair/typebox";
22
import type { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
4-
import { getWalletCredential, WalletCredentialsError } from "../../../shared/db/wallet-credentials/get-wallet-credential";
4+
import {
5+
getWalletCredential,
6+
WalletCredentialsError,
7+
} from "../../../shared/db/wallet-credentials/get-wallet-credential";
58
import { createCustomError } from "../../middleware/error";
69
import { standardResponseSchema } from "../../schemas/shared-api-schemas";
710

@@ -16,7 +19,7 @@ const responseSchema = Type.Object({
1619
id: Type.String(),
1720
type: Type.String(),
1821
label: Type.Union([Type.String(), Type.Null()]),
19-
isDefault: Type.Boolean(),
22+
isDefault: Type.Union([Type.Boolean(), Type.Null()]),
2023
createdAt: Type.String(),
2124
updatedAt: Type.String(),
2225
deletedAt: Type.Union([Type.String(), Type.Null()]),
@@ -82,4 +85,4 @@ export async function getWalletCredentialRoute(fastify: FastifyInstance) {
8285
}
8386
},
8487
});
85-
}
88+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { Type, type Static } from "@sinclair/typebox";
2+
import type { FastifyInstance } from "fastify";
3+
import { StatusCodes } from "http-status-codes";
4+
import { updateWalletCredential } from "../../../shared/db/wallet-credentials/update-wallet-credential";
5+
import { WalletCredentialsError } from "../../../shared/db/wallet-credentials/get-wallet-credential";
6+
import { createCustomError } from "../../middleware/error";
7+
import { standardResponseSchema } from "../../schemas/shared-api-schemas";
8+
9+
const ParamsSchema = Type.Object({
10+
id: Type.String({
11+
description: "The ID of the wallet credential to update.",
12+
}),
13+
});
14+
15+
const requestBodySchema = Type.Object({
16+
label: Type.Optional(Type.String()),
17+
isDefault: Type.Optional(
18+
Type.Boolean({
19+
description:
20+
"Whether this credential should be set as the default for its type. Only one credential can be default per type.",
21+
}),
22+
),
23+
entitySecret: Type.Optional(
24+
Type.String({
25+
description:
26+
"32-byte hex string. Consult https://developers.circle.com/w3s/entity-secret-management to create and register an entity secret.",
27+
pattern: "^[0-9a-fA-F]{64}$",
28+
}),
29+
),
30+
});
31+
32+
const responseSchema = Type.Object({
33+
result: Type.Object({
34+
id: Type.String(),
35+
type: Type.String(),
36+
label: Type.Union([Type.String(), Type.Null()]),
37+
isDefault: Type.Union([Type.Boolean(), Type.Null()]),
38+
createdAt: Type.String(),
39+
updatedAt: Type.String(),
40+
}),
41+
});
42+
43+
responseSchema.example = {
44+
result: {
45+
id: "123e4567-e89b-12d3-a456-426614174000",
46+
type: "circle",
47+
label: "My Updated Circle Credential",
48+
isDefault: true,
49+
createdAt: "2024-01-01T00:00:00.000Z",
50+
updatedAt: "2024-01-01T00:00:00.000Z",
51+
},
52+
};
53+
54+
export async function updateWalletCredentialRoute(fastify: FastifyInstance) {
55+
fastify.route<{
56+
Params: Static<typeof ParamsSchema>;
57+
Body: Static<typeof requestBodySchema>;
58+
Reply: Static<typeof responseSchema>;
59+
}>({
60+
method: "PUT",
61+
url: "/wallet-credentials/:id",
62+
schema: {
63+
summary: "Update wallet credential",
64+
description:
65+
"Update a wallet credential's label, default status, and entity secret.",
66+
tags: ["Wallet Credentials"],
67+
operationId: "updateWalletCredential",
68+
params: ParamsSchema,
69+
body: requestBodySchema,
70+
response: {
71+
...standardResponseSchema,
72+
[StatusCodes.OK]: responseSchema,
73+
},
74+
},
75+
handler: async (req, reply) => {
76+
try {
77+
const credential = await updateWalletCredential({
78+
id: req.params.id,
79+
label: req.body.label,
80+
isDefault: req.body.isDefault,
81+
entitySecret: req.body.entitySecret,
82+
});
83+
84+
reply.status(StatusCodes.OK).send({
85+
result: {
86+
id: credential.id,
87+
type: credential.type,
88+
label: credential.label,
89+
isDefault: credential.isDefault,
90+
createdAt: credential.createdAt.toISOString(),
91+
updatedAt: credential.updatedAt.toISOString(),
92+
},
93+
});
94+
} catch (e) {
95+
if (e instanceof WalletCredentialsError) {
96+
throw createCustomError(
97+
e.message,
98+
StatusCodes.NOT_FOUND,
99+
"WALLET_CREDENTIAL_NOT_FOUND",
100+
);
101+
}
102+
throw e;
103+
}
104+
},
105+
});
106+
}

src/shared/db/wallet-credentials/create-wallet-credential.ts

+3-41
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import { encrypt } from "../../utils/crypto";
2-
import { registerEntitySecretCiphertext } from "@circle-fin/developer-controlled-wallets";
32
import { prisma } from "../client";
43
import { getConfig } from "../../utils/cache/get-config";
54
import { WalletCredentialsError } from "./get-wallet-credential";
6-
import { randomBytes } from "node:crypto";
7-
import { cirlceEntitySecretZodSchema } from "../../schemas/wallet";
85

96
// will be expanded to be a discriminated union of all supported wallet types
107
export type CreateWalletCredentialsParams = {
118
type: "circle";
129
label: string;
13-
entitySecret?: string;
10+
entitySecret: string;
1411
isDefault?: boolean;
1512
};
1613

@@ -21,58 +18,23 @@ export const createWalletCredential = async ({
2118
isDefault,
2219
}: CreateWalletCredentialsParams) => {
2320
const { walletConfiguration } = await getConfig();
24-
2521
switch (type) {
2622
case "circle": {
2723
const circleApiKey = walletConfiguration.circle?.apiKey;
28-
2924
if (!circleApiKey) {
3025
throw new WalletCredentialsError("No Circle API Key Configured");
3126
}
32-
33-
if (entitySecret) {
34-
const { error } = cirlceEntitySecretZodSchema.safeParse(entitySecret);
35-
if (error) {
36-
throw new WalletCredentialsError(
37-
"Invalid provided entity secret for Circle",
38-
);
39-
}
40-
}
41-
42-
// If entitySecret is not provided, generate a random one
43-
const finalEntitySecret = entitySecret ?? randomBytes(32).toString("hex");
4427
// Create the wallet credentials
4528
const walletCredentials = await prisma.walletCredentials.create({
4629
data: {
4730
type,
4831
label,
49-
isDefault: isDefault ?? null,
32+
isDefault: isDefault || null,
5033
data: {
51-
entitySecret: encrypt(finalEntitySecret),
34+
entitySecret: encrypt(entitySecret),
5235
},
5336
},
5437
});
55-
56-
// try registering the entity secret. See: https://developers.circle.com/w3s/developer-controlled-create-your-first-wallet
57-
try {
58-
await registerEntitySecretCiphertext({
59-
apiKey: circleApiKey,
60-
entitySecret: finalEntitySecret,
61-
recoveryFileDownloadPath: "/dev/null",
62-
});
63-
} catch (e: unknown) {
64-
// If failed to registeer, permanently delete erroneously created credential
65-
await prisma.walletCredentials.delete({
66-
where: {
67-
id: walletCredentials.id,
68-
},
69-
});
70-
71-
throw new WalletCredentialsError(
72-
`Could not register Entity Secret with Circle\n${JSON.stringify(e)}`,
73-
);
74-
}
75-
7638
return walletCredentials;
7739
}
7840
}

src/shared/db/wallet-credentials/get-wallet-credential.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const walletCredentialsSchema = z.object({
1818
data: z.object({
1919
entitySecret: z.string(),
2020
}),
21-
isDefault: z.boolean(),
21+
isDefault: z.boolean().nullable(),
2222
createdAt: z.date(),
2323
updatedAt: z.date(),
2424
deletedAt: z.date().nullable(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { getWalletCredential } from "./get-wallet-credential";
2+
import { encrypt } from "../../utils/crypto";
3+
import { prisma } from "../client";
4+
import { cirlceEntitySecretZodSchema } from "../../schemas/wallet";
5+
6+
interface UpdateWalletCredentialParams {
7+
id: string;
8+
label?: string;
9+
isDefault?: boolean;
10+
entitySecret?: string;
11+
}
12+
13+
type UpdateData = {
14+
label?: string;
15+
isDefault: boolean | null;
16+
data?: {
17+
entitySecret: string;
18+
};
19+
};
20+
21+
export const updateWalletCredential = async ({
22+
id,
23+
label,
24+
isDefault,
25+
entitySecret,
26+
}: UpdateWalletCredentialParams) => {
27+
// First check if credential exists
28+
await getWalletCredential({ id });
29+
30+
// If entitySecret is provided, validate and encrypt it
31+
const data: UpdateData = {
32+
label,
33+
isDefault: isDefault || null,
34+
};
35+
36+
if (entitySecret) {
37+
// Validate the entity secret
38+
cirlceEntitySecretZodSchema.parse(entitySecret);
39+
40+
// Only update data field if entitySecret is provided
41+
data.data = {
42+
entitySecret: encrypt(entitySecret),
43+
};
44+
}
45+
46+
// Update the credential
47+
const updatedCredential = await prisma.walletCredentials.update({
48+
where: {
49+
id,
50+
},
51+
data,
52+
});
53+
54+
return updatedCredential;
55+
};

0 commit comments

Comments
 (0)