From 9d93f8dbfff5544b172bf6112d8f7cfb1e7408d8 Mon Sep 17 00:00:00 2001 From: Prithvish Baidya Date: Sat, 8 Feb 2025 10:25:16 +0530 Subject: [PATCH 1/4] remove auto creation and register of circle entity secret --- .../routes/wallet-credentials/create.ts | 13 +++--- .../create-wallet-credential.ts | 46 ++----------------- 2 files changed, 11 insertions(+), 48 deletions(-) diff --git a/src/server/routes/wallet-credentials/create.ts b/src/server/routes/wallet-credentials/create.ts index 53ec65f3..5afbffc9 100644 --- a/src/server/routes/wallet-credentials/create.ts +++ b/src/server/routes/wallet-credentials/create.ts @@ -9,13 +9,11 @@ import { createCustomError } from "../../middleware/error"; const requestBodySchema = Type.Object({ label: Type.String(), type: Type.Literal("circle"), - entitySecret: Type.Optional( - Type.String({ - description: - "32-byte hex string. If not provided, a random one will be generated.", - pattern: "^[0-9a-fA-F]{64}$", - }), - ), + entitySecret: Type.String({ + description: + "32-byte hex string. Consult https://developers.circle.com/w3s/entity-secret-management to create and register an entity secret.", + pattern: "^[0-9a-fA-F]{64}$", + }), isDefault: Type.Optional( Type.Boolean({ description: @@ -98,6 +96,7 @@ export const createWalletCredentialRoute = async (fastify: FastifyInstance) => { "WALLET_CREDENTIAL_ERROR", ); } + throw e; } }, }); diff --git a/src/shared/db/wallet-credentials/create-wallet-credential.ts b/src/shared/db/wallet-credentials/create-wallet-credential.ts index db3bfad2..22e6ec46 100644 --- a/src/shared/db/wallet-credentials/create-wallet-credential.ts +++ b/src/shared/db/wallet-credentials/create-wallet-credential.ts @@ -1,16 +1,13 @@ import { encrypt } from "../../utils/crypto"; -import { registerEntitySecretCiphertext } from "@circle-fin/developer-controlled-wallets"; import { prisma } from "../client"; import { getConfig } from "../../utils/cache/get-config"; import { WalletCredentialsError } from "./get-wallet-credential"; -import { randomBytes } from "node:crypto"; -import { cirlceEntitySecretZodSchema } from "../../schemas/wallet"; // will be expanded to be a discriminated union of all supported wallet types export type CreateWalletCredentialsParams = { type: "circle"; label: string; - entitySecret?: string; + entitySecret: string; isDefault?: boolean; }; @@ -21,59 +18,26 @@ export const createWalletCredential = async ({ isDefault, }: CreateWalletCredentialsParams) => { const { walletConfiguration } = await getConfig(); - switch (type) { case "circle": { const circleApiKey = walletConfiguration.circle?.apiKey; - if (!circleApiKey) { throw new WalletCredentialsError("No Circle API Key Configured"); - } - - if (entitySecret) { - const { error } = cirlceEntitySecretZodSchema.safeParse(entitySecret); - if (error) { - throw new WalletCredentialsError( - "Invalid provided entity secret for Circle", - ); - } - } - - // If entitySecret is not provided, generate a random one - const finalEntitySecret = entitySecret ?? randomBytes(32).toString("hex"); + } // Create the wallet credentials const walletCredentials = await prisma.walletCredentials.create({ data: { type, label, - isDefault: isDefault ?? null, + isDefault: isDefault ? true : null, data: { - entitySecret: encrypt(finalEntitySecret), + entitySecret: encrypt(entitySecret), }, }, }); - - // try registering the entity secret. See: https://developers.circle.com/w3s/developer-controlled-create-your-first-wallet - try { - await registerEntitySecretCiphertext({ - apiKey: circleApiKey, - entitySecret: finalEntitySecret, - recoveryFileDownloadPath: "/dev/null", - }); - } catch (e: unknown) { - // If failed to registeer, permanently delete erroneously created credential - await prisma.walletCredentials.delete({ - where: { - id: walletCredentials.id, - }, - }); - - throw new WalletCredentialsError( - `Could not register Entity Secret with Circle\n${JSON.stringify(e)}`, - ); + return walletCredentials; } - return walletCredentials; } } }; From f00a55ea42ce35c5d865f3233f4f04bc45b1764f Mon Sep 17 00:00:00 2001 From: Prithvish Baidya Date: Sat, 8 Feb 2025 12:10:30 +0530 Subject: [PATCH 2/4] Update wallet credentials --- src/server/routes/index.ts | 2 + src/server/routes/wallet-credentials/get.ts | 9 +- .../routes/wallet-credentials/update.ts | 106 ++++++++++++++++++ .../create-wallet-credential.ts | 8 +- .../get-wallet-credential.ts | 2 +- .../update-wallet-credential.ts | 61 ++++++++++ 6 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 src/server/routes/wallet-credentials/update.ts create mode 100644 src/shared/db/wallet-credentials/update-wallet-credential.ts diff --git a/src/server/routes/index.ts b/src/server/routes/index.ts index 370ab9e6..be174a78 100644 --- a/src/server/routes/index.ts +++ b/src/server/routes/index.ts @@ -116,6 +116,7 @@ import { sendTransactionBatchAtomicRoute } from "./backend-wallet/send-transacti import { createWalletCredentialRoute } from "./wallet-credentials/create"; import { getWalletCredentialRoute } from "./wallet-credentials/get"; import { getAllWalletCredentialsRoute } from "./wallet-credentials/get-all"; +import { updateWalletCredentialRoute } from "./wallet-credentials/update"; export async function withRoutes(fastify: FastifyInstance) { // Backend Wallets @@ -144,6 +145,7 @@ export async function withRoutes(fastify: FastifyInstance) { await fastify.register(createWalletCredentialRoute); await fastify.register(getWalletCredentialRoute); await fastify.register(getAllWalletCredentialsRoute); + await fastify.register(updateWalletCredentialRoute); // Configuration await fastify.register(getWalletsConfiguration); diff --git a/src/server/routes/wallet-credentials/get.ts b/src/server/routes/wallet-credentials/get.ts index c0141efe..f132c08a 100644 --- a/src/server/routes/wallet-credentials/get.ts +++ b/src/server/routes/wallet-credentials/get.ts @@ -1,7 +1,10 @@ import { Type, type Static } from "@sinclair/typebox"; import type { FastifyInstance } from "fastify"; import { StatusCodes } from "http-status-codes"; -import { getWalletCredential, WalletCredentialsError } from "../../../shared/db/wallet-credentials/get-wallet-credential"; +import { + getWalletCredential, + WalletCredentialsError, +} from "../../../shared/db/wallet-credentials/get-wallet-credential"; import { createCustomError } from "../../middleware/error"; import { standardResponseSchema } from "../../schemas/shared-api-schemas"; @@ -16,7 +19,7 @@ const responseSchema = Type.Object({ id: Type.String(), type: Type.String(), label: Type.Union([Type.String(), Type.Null()]), - isDefault: Type.Boolean(), + isDefault: Type.Union([Type.Boolean(), Type.Null()]), createdAt: Type.String(), updatedAt: Type.String(), deletedAt: Type.Union([Type.String(), Type.Null()]), @@ -82,4 +85,4 @@ export async function getWalletCredentialRoute(fastify: FastifyInstance) { } }, }); -} \ No newline at end of file +} diff --git a/src/server/routes/wallet-credentials/update.ts b/src/server/routes/wallet-credentials/update.ts new file mode 100644 index 00000000..e7a4eb50 --- /dev/null +++ b/src/server/routes/wallet-credentials/update.ts @@ -0,0 +1,106 @@ +import { Type, type Static } from "@sinclair/typebox"; +import type { FastifyInstance } from "fastify"; +import { StatusCodes } from "http-status-codes"; +import { updateWalletCredential } from "../../../shared/db/wallet-credentials/update-wallet-credential"; +import { WalletCredentialsError } from "../../../shared/db/wallet-credentials/get-wallet-credential"; +import { createCustomError } from "../../middleware/error"; +import { standardResponseSchema } from "../../schemas/shared-api-schemas"; + +const ParamsSchema = Type.Object({ + id: Type.String({ + description: "The ID of the wallet credential to update.", + }), +}); + +const requestBodySchema = Type.Object({ + label: Type.Optional(Type.String()), + isDefault: Type.Optional( + Type.Boolean({ + description: + "Whether this credential should be set as the default for its type. Only one credential can be default per type.", + }), + ), + entitySecret: Type.Optional( + Type.String({ + description: + "32-byte hex string. Consult https://developers.circle.com/w3s/entity-secret-management to create and register an entity secret.", + pattern: "^[0-9a-fA-F]{64}$", + }), + ), +}); + +const responseSchema = Type.Object({ + result: Type.Object({ + id: Type.String(), + type: Type.String(), + label: Type.Union([Type.String(), Type.Null()]), + isDefault: Type.Union([Type.Boolean(), Type.Null()]), + createdAt: Type.String(), + updatedAt: Type.String(), + }), +}); + +responseSchema.example = { + result: { + id: "123e4567-e89b-12d3-a456-426614174000", + type: "circle", + label: "My Updated Circle Credential", + isDefault: true, + createdAt: "2024-01-01T00:00:00.000Z", + updatedAt: "2024-01-01T00:00:00.000Z", + }, +}; + +export async function updateWalletCredentialRoute(fastify: FastifyInstance) { + fastify.route<{ + Params: Static; + Body: Static; + Reply: Static; + }>({ + method: "PUT", + url: "/wallet-credentials/:id", + schema: { + summary: "Update wallet credential", + description: + "Update a wallet credential's label, default status, and entity secret.", + tags: ["Wallet Credentials"], + operationId: "updateWalletCredential", + params: ParamsSchema, + body: requestBodySchema, + response: { + ...standardResponseSchema, + [StatusCodes.OK]: responseSchema, + }, + }, + handler: async (req, reply) => { + try { + const credential = await updateWalletCredential({ + id: req.params.id, + label: req.body.label, + isDefault: req.body.isDefault, + entitySecret: req.body.entitySecret, + }); + + reply.status(StatusCodes.OK).send({ + result: { + id: credential.id, + type: credential.type, + label: credential.label, + isDefault: credential.isDefault, + createdAt: credential.createdAt.toISOString(), + updatedAt: credential.updatedAt.toISOString(), + }, + }); + } catch (e) { + if (e instanceof WalletCredentialsError) { + throw createCustomError( + e.message, + StatusCodes.NOT_FOUND, + "WALLET_CREDENTIAL_NOT_FOUND", + ); + } + throw e; + } + }, + }); +} diff --git a/src/shared/db/wallet-credentials/create-wallet-credential.ts b/src/shared/db/wallet-credentials/create-wallet-credential.ts index 22e6ec46..51b9cae7 100644 --- a/src/shared/db/wallet-credentials/create-wallet-credential.ts +++ b/src/shared/db/wallet-credentials/create-wallet-credential.ts @@ -23,21 +23,19 @@ export const createWalletCredential = async ({ const circleApiKey = walletConfiguration.circle?.apiKey; if (!circleApiKey) { throw new WalletCredentialsError("No Circle API Key Configured"); - } + } // Create the wallet credentials const walletCredentials = await prisma.walletCredentials.create({ data: { type, label, - isDefault: isDefault ? true : null, + isDefault: isDefault || null, data: { entitySecret: encrypt(entitySecret), }, }, }); - return walletCredentials; - } - + return walletCredentials; } } }; diff --git a/src/shared/db/wallet-credentials/get-wallet-credential.ts b/src/shared/db/wallet-credentials/get-wallet-credential.ts index 21b7b8a2..f1cf5107 100644 --- a/src/shared/db/wallet-credentials/get-wallet-credential.ts +++ b/src/shared/db/wallet-credentials/get-wallet-credential.ts @@ -18,7 +18,7 @@ const walletCredentialsSchema = z.object({ data: z.object({ entitySecret: z.string(), }), - isDefault: z.boolean(), + isDefault: z.boolean().nullable(), createdAt: z.date(), updatedAt: z.date(), deletedAt: z.date().nullable(), diff --git a/src/shared/db/wallet-credentials/update-wallet-credential.ts b/src/shared/db/wallet-credentials/update-wallet-credential.ts new file mode 100644 index 00000000..d56380df --- /dev/null +++ b/src/shared/db/wallet-credentials/update-wallet-credential.ts @@ -0,0 +1,61 @@ +import type { PrismaTransaction } from "../../schemas/prisma"; +import { getWalletCredential } from "./get-wallet-credential"; +import { encrypt } from "../../utils/crypto"; +import { z } from "zod"; +import { prisma } from "../client"; + +const entitySecretSchema = z.string().regex(/^[0-9a-fA-F]{64}$/, { + message: "entitySecret must be a 32-byte hex string", +}); + +interface UpdateWalletCredentialParams { + pgtx?: PrismaTransaction; + id: string; + label?: string; + isDefault?: boolean; + entitySecret?: string; +} + +type UpdateData = { + label?: string; + isDefault: boolean | null; + data?: { + entitySecret: string; + }; +}; + +export const updateWalletCredential = async ({ + id, + label, + isDefault, + entitySecret, +}: UpdateWalletCredentialParams) => { + // First check if credential exists + await getWalletCredential({ id }); + + // If entitySecret is provided, validate and encrypt it + const data: UpdateData = { + label, + isDefault: isDefault || null, + }; + + if (entitySecret) { + // Validate the entity secret + entitySecretSchema.parse(entitySecret); + + // Only update data field if entitySecret is provided + data.data = { + entitySecret: encrypt(entitySecret), + }; + } + + // Update the credential + const updatedCredential = await prisma.walletCredentials.update({ + where: { + id, + }, + data, + }); + + return updatedCredential; +}; From 61eaaedf264001a4c61d18b5aad94de188fbfbd2 Mon Sep 17 00:00:00 2001 From: Prithvish Baidya Date: Sat, 8 Feb 2025 14:40:00 +0530 Subject: [PATCH 3/4] remove uneeded import --- src/shared/db/wallet-credentials/update-wallet-credential.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/shared/db/wallet-credentials/update-wallet-credential.ts b/src/shared/db/wallet-credentials/update-wallet-credential.ts index d56380df..b91a82fa 100644 --- a/src/shared/db/wallet-credentials/update-wallet-credential.ts +++ b/src/shared/db/wallet-credentials/update-wallet-credential.ts @@ -1,4 +1,3 @@ -import type { PrismaTransaction } from "../../schemas/prisma"; import { getWalletCredential } from "./get-wallet-credential"; import { encrypt } from "../../utils/crypto"; import { z } from "zod"; @@ -9,7 +8,6 @@ const entitySecretSchema = z.string().regex(/^[0-9a-fA-F]{64}$/, { }); interface UpdateWalletCredentialParams { - pgtx?: PrismaTransaction; id: string; label?: string; isDefault?: boolean; From 7cfb4d3124c98be2e137fd04bd42e8870154059b Mon Sep 17 00:00:00 2001 From: Prithvish Baidya Date: Sat, 8 Feb 2025 14:41:12 +0530 Subject: [PATCH 4/4] reuse schema --- .../db/wallet-credentials/update-wallet-credential.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/shared/db/wallet-credentials/update-wallet-credential.ts b/src/shared/db/wallet-credentials/update-wallet-credential.ts index b91a82fa..018eb562 100644 --- a/src/shared/db/wallet-credentials/update-wallet-credential.ts +++ b/src/shared/db/wallet-credentials/update-wallet-credential.ts @@ -1,11 +1,7 @@ import { getWalletCredential } from "./get-wallet-credential"; import { encrypt } from "../../utils/crypto"; -import { z } from "zod"; import { prisma } from "../client"; - -const entitySecretSchema = z.string().regex(/^[0-9a-fA-F]{64}$/, { - message: "entitySecret must be a 32-byte hex string", -}); +import { cirlceEntitySecretZodSchema } from "../../schemas/wallet"; interface UpdateWalletCredentialParams { id: string; @@ -39,7 +35,7 @@ export const updateWalletCredential = async ({ if (entitySecret) { // Validate the entity secret - entitySecretSchema.parse(entitySecret); + cirlceEntitySecretZodSchema.parse(entitySecret); // Only update data field if entitySecret is provided data.data = {