Skip to content

Commit f00a55e

Browse files
committed
Update wallet credentials
1 parent 9d93f8d commit f00a55e

File tree

6 files changed

+179
-9
lines changed

6 files changed

+179
-9
lines changed

src/server/routes/index.ts

Lines changed: 2 additions & 0 deletions
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/get.ts

Lines changed: 6 additions & 3 deletions
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+
}
Lines changed: 106 additions & 0 deletions
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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,19 @@ export const createWalletCredential = async ({
2323
const circleApiKey = walletConfiguration.circle?.apiKey;
2424
if (!circleApiKey) {
2525
throw new WalletCredentialsError("No Circle API Key Configured");
26-
}
26+
}
2727
// Create the wallet credentials
2828
const walletCredentials = await prisma.walletCredentials.create({
2929
data: {
3030
type,
3131
label,
32-
isDefault: isDefault ? true : null,
32+
isDefault: isDefault || null,
3333
data: {
3434
entitySecret: encrypt(entitySecret),
3535
},
3636
},
3737
});
38-
return walletCredentials;
39-
}
40-
38+
return walletCredentials;
4139
}
4240
}
4341
};

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

Lines changed: 1 addition & 1 deletion
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(),
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { PrismaTransaction } from "../../schemas/prisma";
2+
import { getWalletCredential } from "./get-wallet-credential";
3+
import { encrypt } from "../../utils/crypto";
4+
import { z } from "zod";
5+
import { prisma } from "../client";
6+
7+
const entitySecretSchema = z.string().regex(/^[0-9a-fA-F]{64}$/, {
8+
message: "entitySecret must be a 32-byte hex string",
9+
});
10+
11+
interface UpdateWalletCredentialParams {
12+
pgtx?: PrismaTransaction;
13+
id: string;
14+
label?: string;
15+
isDefault?: boolean;
16+
entitySecret?: string;
17+
}
18+
19+
type UpdateData = {
20+
label?: string;
21+
isDefault: boolean | null;
22+
data?: {
23+
entitySecret: string;
24+
};
25+
};
26+
27+
export const updateWalletCredential = async ({
28+
id,
29+
label,
30+
isDefault,
31+
entitySecret,
32+
}: UpdateWalletCredentialParams) => {
33+
// First check if credential exists
34+
await getWalletCredential({ id });
35+
36+
// If entitySecret is provided, validate and encrypt it
37+
const data: UpdateData = {
38+
label,
39+
isDefault: isDefault || null,
40+
};
41+
42+
if (entitySecret) {
43+
// Validate the entity secret
44+
entitySecretSchema.parse(entitySecret);
45+
46+
// Only update data field if entitySecret is provided
47+
data.data = {
48+
entitySecret: encrypt(entitySecret),
49+
};
50+
}
51+
52+
// Update the credential
53+
const updatedCredential = await prisma.walletCredentials.update({
54+
where: {
55+
id,
56+
},
57+
data,
58+
});
59+
60+
return updatedCredential;
61+
};

0 commit comments

Comments
 (0)