From b60f5bbf39bd3a15bca10644e9e329b5a1957c1d Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Fri, 16 May 2025 12:33:43 +0200 Subject: [PATCH 01/19] feat(setting-service): first version --- apps/settings-service/.env.example | 0 apps/settings-service/Makefile | 14 ++ apps/settings-service/build.config.ts | 10 + apps/settings-service/package.json | 28 +++ apps/settings-service/src/db/driver.ts | 13 ++ .../db/migrations/Migration20250515123000.ts | 18 ++ .../src/db/migrations/index.ts | 5 + apps/settings-service/src/db/types.ts | 39 ++++ apps/settings-service/src/dtos.ts | 28 +++ apps/settings-service/src/env.ts | 20 ++ apps/settings-service/src/errors.ts | 11 ++ .../src/handlers/createConfig/createConfig.ts | 13 ++ .../src/handlers/createConfig/index.ts | 2 + .../src/handlers/createConfig/types.ts | 5 + .../src/handlers/createConfig/validation.ts | 30 +++ .../src/handlers/listConfig.ts/index.ts | 2 + .../src/handlers/listConfig.ts/listConfig.ts | 17 ++ .../src/handlers/listConfig.ts/types.ts | 5 + .../src/handlers/listConfig.ts/validation.ts | 50 +++++ apps/settings-service/src/index.ts | 11 ++ apps/settings-service/src/migrate.ts | 38 ++++ .../src/repositories/permissionsRepository.ts | 28 +++ .../src/server/configRoute.ts | 22 +++ apps/settings-service/src/server/index.ts | 101 ++++++++++ .../src/server/makeResponse.ts | 45 +++++ apps/settings-service/src/utils/isAppUrl.ts | 10 + .../src/utils/isProduction.ts | 3 + apps/settings-service/src/utils/isUUID.ts | 6 + apps/settings-service/src/utils/logger.ts | 25 +++ apps/settings-service/tsconfig.build.json | 7 + apps/settings-service/tsconfig.json | 7 + bun.lock | 176 ++++++++++-------- 32 files changed, 715 insertions(+), 74 deletions(-) create mode 100644 apps/settings-service/.env.example create mode 100644 apps/settings-service/Makefile create mode 100644 apps/settings-service/build.config.ts create mode 100644 apps/settings-service/package.json create mode 100644 apps/settings-service/src/db/driver.ts create mode 100644 apps/settings-service/src/db/migrations/Migration20250515123000.ts create mode 100644 apps/settings-service/src/db/migrations/index.ts create mode 100644 apps/settings-service/src/db/types.ts create mode 100644 apps/settings-service/src/dtos.ts create mode 100644 apps/settings-service/src/env.ts create mode 100644 apps/settings-service/src/errors.ts create mode 100644 apps/settings-service/src/handlers/createConfig/createConfig.ts create mode 100644 apps/settings-service/src/handlers/createConfig/index.ts create mode 100644 apps/settings-service/src/handlers/createConfig/types.ts create mode 100644 apps/settings-service/src/handlers/createConfig/validation.ts create mode 100644 apps/settings-service/src/handlers/listConfig.ts/index.ts create mode 100644 apps/settings-service/src/handlers/listConfig.ts/listConfig.ts create mode 100644 apps/settings-service/src/handlers/listConfig.ts/types.ts create mode 100644 apps/settings-service/src/handlers/listConfig.ts/validation.ts create mode 100644 apps/settings-service/src/index.ts create mode 100644 apps/settings-service/src/migrate.ts create mode 100644 apps/settings-service/src/repositories/permissionsRepository.ts create mode 100644 apps/settings-service/src/server/configRoute.ts create mode 100644 apps/settings-service/src/server/index.ts create mode 100644 apps/settings-service/src/server/makeResponse.ts create mode 100644 apps/settings-service/src/utils/isAppUrl.ts create mode 100644 apps/settings-service/src/utils/isProduction.ts create mode 100644 apps/settings-service/src/utils/isUUID.ts create mode 100644 apps/settings-service/src/utils/logger.ts create mode 100644 apps/settings-service/tsconfig.build.json create mode 100644 apps/settings-service/tsconfig.json diff --git a/apps/settings-service/.env.example b/apps/settings-service/.env.example new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/settings-service/Makefile b/apps/settings-service/Makefile new file mode 100644 index 0000000000..610642747d --- /dev/null +++ b/apps/settings-service/Makefile @@ -0,0 +1,14 @@ +SRC_ROOT_DIR := src + +include ../../makefiles/lib.mk +include ../../makefiles/formatting.mk +include ../../makefiles/bundling.mk +include ../../makefiles/help.mk + +start: ## Starts the settings service + bun run --hot src/index.ts +.PHONY: start + +migrate: ## Runs pending migrations + bun run src/migrate.ts +.PHONY: migrate \ No newline at end of file diff --git a/apps/settings-service/build.config.ts b/apps/settings-service/build.config.ts new file mode 100644 index 0000000000..1ee0717c15 --- /dev/null +++ b/apps/settings-service/build.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "@happy.tech/happybuild" + +export default defineConfig({ + exports: [".", "./migrate"], + bunConfig: { + minify: false, + target: "node", + external: ["better-sqlite3"], + }, +}) diff --git a/apps/settings-service/package.json b/apps/settings-service/package.json new file mode 100644 index 0000000000..58bea71b12 --- /dev/null +++ b/apps/settings-service/package.json @@ -0,0 +1,28 @@ +{ + "name": "@happy.tech/settings-service", + "private": true, + "version": "0.1.0", + "type": "module", + "main": "./dist/index.es.js", + "module": "./dist/index.es.js", + "exports": { + ".": "./dist/index.es.js", + "./migrate": "./dist/migrate.es.js" + }, + "dependencies": { + "@happy.tech/common": "workspace:1.0.0", + "@hono/node-server": "^1.13.8", + "@scalar/hono-api-reference": "^0.5.175", + "hono": "^4.7.2", + "hono-openapi": "^0.4.4", + "neverthrow": "^8.1.0", + "uuid": "^11.1.0", + "zod": "^3.23.8", + "zod-openapi": "^4.2.3" + }, + "devDependencies": { + "@happy.tech/happybuild": "workspace:1.0.0", + "typescript": "^5.6.2", + "hono-openapi": "^0.4.4" + } +} diff --git a/apps/settings-service/src/db/driver.ts b/apps/settings-service/src/db/driver.ts new file mode 100644 index 0000000000..72da91b149 --- /dev/null +++ b/apps/settings-service/src/db/driver.ts @@ -0,0 +1,13 @@ +import { Database as BunDatabase } from "bun:sqlite" +import { Kysely, ParseJSONResultsPlugin } from "kysely" +import { BunSqliteDialect } from "kysely-bun-sqlite" +import type { Database } from "./types" + +import { env } from "../env" + +const dbPath = env.SETTINGS_DB_URL || ":memory:" + +export const db = new Kysely({ + dialect: new BunSqliteDialect({ database: new BunDatabase(dbPath) }), + plugins: [new ParseJSONResultsPlugin()], +}) diff --git a/apps/settings-service/src/db/migrations/Migration20250515123000.ts b/apps/settings-service/src/db/migrations/Migration20250515123000.ts new file mode 100644 index 0000000000..acf5e76e14 --- /dev/null +++ b/apps/settings-service/src/db/migrations/Migration20250515123000.ts @@ -0,0 +1,18 @@ +import type { Kysely } from "kysely" +import type { Database } from "../types" + +export async function up(db: Kysely) { + await db.schema + .createTable("walletPermissions") + .addColumn("user", "text") + .addColumn("invoker", "text") + .addColumn("parentCapability", "text") + .addColumn("caveats", "jsonb") + .addColumn("date", "integer") + .addColumn("id", "text", (col) => col.notNull()) + .addColumn("updatedAt", "integer") + .addColumn("deleted", "boolean") + .execute() +} + +export const migration20250515123000 = { up } diff --git a/apps/settings-service/src/db/migrations/index.ts b/apps/settings-service/src/db/migrations/index.ts new file mode 100644 index 0000000000..8349d3b15f --- /dev/null +++ b/apps/settings-service/src/db/migrations/index.ts @@ -0,0 +1,5 @@ +import { migration20250515123000 } from "./Migration20250515123000" + +export const migrations = { + "20250515123000": migration20250515123000, +} diff --git a/apps/settings-service/src/db/types.ts b/apps/settings-service/src/db/types.ts new file mode 100644 index 0000000000..bdf09604e7 --- /dev/null +++ b/apps/settings-service/src/db/types.ts @@ -0,0 +1,39 @@ +import type { Hex } from "@happy.tech/common" +import type { HTTPString, UUID } from "@happy.tech/common" +import type { ColumnType, Selectable } from "kysely" + +export type AppURL = HTTPString & { _brand: "AppHTTPString" } + +/** + * A caveat is a specific specific restrictions applied to the permitted request. + */ +type WalletPermissionCaveat = { + type: string + value: string +} + +/** + * Permission object for a specific permission. + * + * This type is copied from Viem (eip1193.ts) but we add a user field. + */ +export type WalletPermissionTable = { + // The user to which the permission is granted. + user: Hex + // The app to which the permission is granted. + invoker: AppURL + // This is the EIP-1193 request that this permission is mapped to. + parentCapability: "eth_accounts" | string // TODO only string or make specific + caveats: ColumnType + date: number + // Not in the EIP, but Viem wants this. + id: UUID + updatedAt: number + deleted: boolean +} + +export type WalletPermission = Selectable + +export interface Database { + walletPermissions: WalletPermissionTable +} diff --git a/apps/settings-service/src/dtos.ts b/apps/settings-service/src/dtos.ts new file mode 100644 index 0000000000..956408b595 --- /dev/null +++ b/apps/settings-service/src/dtos.ts @@ -0,0 +1,28 @@ +import { isAddress } from "@happy.tech/common" +import { checksum } from "ox/Address" +import { z } from "zod" +import { isAppUrl } from "./utils/isAppUrl" +import { isUUID } from "./utils/isUUID" + +export const walletPermission = z.object({ + type: z.literal("WalletPermissions").openapi({ + example: "WalletPermissions", + type: "string", + }), + user: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + invoker: z.string().refine(isAppUrl).openapi({ example: "https://app.happy.tech" }), + parentCapability: z.string().openapi({ example: "eth_accounts" }), + caveats: z.array( + z.object({ + type: z.string().openapi({ example: "target" }), + value: z.string().openapi({ example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" }), + }), + ), + date: z.number().openapi({ example: 1715702400 }), + id: z.string().refine(isUUID).openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), + updatedAt: z.number().openapi({ example: 1715702400 }), + deleted: z.boolean().openapi({ example: false }), +}) diff --git a/apps/settings-service/src/env.ts b/apps/settings-service/src/env.ts new file mode 100644 index 0000000000..ccb1f7c79e --- /dev/null +++ b/apps/settings-service/src/env.ts @@ -0,0 +1,20 @@ +import { z } from "zod" + +const envSchema = z.object({ + NODE_ENV: z.enum(["development", "production"]), + APP_PORT: z.string().transform((s) => Number(s)), + SETTINGS_DB_URL: z.string().trim(), + LOG_LEVEL: z.preprocess( + (level) => level && String(level).toUpperCase(), + z.enum(["OFF", "TRACE", "INFO", "WARN", "ERROR"]).default("INFO"), + ), +}) + +const parsedEnv = envSchema.safeParse(process.env) + +if (!parsedEnv.success) { + console.log(parsedEnv.error.issues) + throw new Error("There is an error with the server environment variables") +} + +export const env = parsedEnv.data diff --git a/apps/settings-service/src/errors.ts b/apps/settings-service/src/errors.ts new file mode 100644 index 0000000000..11db85717a --- /dev/null +++ b/apps/settings-service/src/errors.ts @@ -0,0 +1,11 @@ +import type { ContentfulStatusCode } from "hono/utils/http-status" + +export abstract class HappySettingsError extends Error { + public readonly statusCode: ContentfulStatusCode + + constructor(statusCode: ContentfulStatusCode, message?: string, options?: ErrorOptions) { + super(message, options) + this.name = this.constructor.name + this.statusCode = statusCode + } +} diff --git a/apps/settings-service/src/handlers/createConfig/createConfig.ts b/apps/settings-service/src/handlers/createConfig/createConfig.ts new file mode 100644 index 0000000000..d8358e47b1 --- /dev/null +++ b/apps/settings-service/src/handlers/createConfig/createConfig.ts @@ -0,0 +1,13 @@ +import { type Result, ok } from "neverthrow" +import { savePermission } from "../../repositories/permissionsRepository" +import type { CreateConfigInput } from "./types" + +export async function createConfig(input: CreateConfigInput): Promise> { + console.log(input) + + if (input.type === "WalletPermissions") { + await savePermission(input) + } + + return ok(undefined) +} diff --git a/apps/settings-service/src/handlers/createConfig/index.ts b/apps/settings-service/src/handlers/createConfig/index.ts new file mode 100644 index 0000000000..cb792f3b13 --- /dev/null +++ b/apps/settings-service/src/handlers/createConfig/index.ts @@ -0,0 +1,2 @@ +export { createConfig } from "./createConfig" +export { createConfigValidation, createConfigDescription } from "./validation" diff --git a/apps/settings-service/src/handlers/createConfig/types.ts b/apps/settings-service/src/handlers/createConfig/types.ts new file mode 100644 index 0000000000..ded661974c --- /dev/null +++ b/apps/settings-service/src/handlers/createConfig/types.ts @@ -0,0 +1,5 @@ +import type { z } from "zod" +import type { inputSchema, outputSchema } from "./validation" + +export type CreateConfigInput = z.infer +export type CreateConfigOutput = z.infer diff --git a/apps/settings-service/src/handlers/createConfig/validation.ts b/apps/settings-service/src/handlers/createConfig/validation.ts new file mode 100644 index 0000000000..138ab915ea --- /dev/null +++ b/apps/settings-service/src/handlers/createConfig/validation.ts @@ -0,0 +1,30 @@ +import { describeRoute } from "hono-openapi" +import { resolver } from "hono-openapi/zod" +import { validator as zv } from "hono-openapi/zod" +import { z } from "zod" +import { walletPermission } from "../../dtos" +import { isProduction } from "../../utils/isProduction" + +export const inputSchema = z.discriminatedUnion("type", [walletPermission]) + +export const outputSchema = z.object({ + success: z.boolean(), + message: z.string().optional(), +}) + +export const createConfigDescription = describeRoute({ + validateResponse: !isProduction, + description: "Create a new config", + responses: { + 201: { + description: "Config created", + content: { + "application/json": { + schema: resolver(outputSchema), + }, + }, + }, + }, +}) + +export const createConfigValidation = zv("json", inputSchema) diff --git a/apps/settings-service/src/handlers/listConfig.ts/index.ts b/apps/settings-service/src/handlers/listConfig.ts/index.ts new file mode 100644 index 0000000000..fc44eb2f67 --- /dev/null +++ b/apps/settings-service/src/handlers/listConfig.ts/index.ts @@ -0,0 +1,2 @@ +export { listConfig } from "./listConfig" +export { listConfigValidation, listConfigDescription } from "./validation" diff --git a/apps/settings-service/src/handlers/listConfig.ts/listConfig.ts b/apps/settings-service/src/handlers/listConfig.ts/listConfig.ts new file mode 100644 index 0000000000..68699f4db5 --- /dev/null +++ b/apps/settings-service/src/handlers/listConfig.ts/listConfig.ts @@ -0,0 +1,17 @@ +import { type Result, ok } from "neverthrow" +import type { WalletPermission } from "../../db/types" +import { listPermissions } from "../../repositories/permissionsRepository" +import type { ListConfigInput } from "./types" + +export async function listConfig(input: ListConfigInput): Promise> { + const permissions = await listPermissions(input.user, input.lastUpdated) + + console.log(permissions.map((p) => p.caveats)) + + return ok( + permissions.map((p) => ({ + type: "WalletPermissions", + ...p, + })), + ) +} diff --git a/apps/settings-service/src/handlers/listConfig.ts/types.ts b/apps/settings-service/src/handlers/listConfig.ts/types.ts new file mode 100644 index 0000000000..c8033128ce --- /dev/null +++ b/apps/settings-service/src/handlers/listConfig.ts/types.ts @@ -0,0 +1,5 @@ +import type { z } from "zod" +import type { inputSchema, outputSchema } from "./validation" + +export type ListConfigInput = z.infer +export type ListConfigOutput = z.infer diff --git a/apps/settings-service/src/handlers/listConfig.ts/validation.ts b/apps/settings-service/src/handlers/listConfig.ts/validation.ts new file mode 100644 index 0000000000..2f437e0f61 --- /dev/null +++ b/apps/settings-service/src/handlers/listConfig.ts/validation.ts @@ -0,0 +1,50 @@ +import { isAddress } from "@happy.tech/common" +import { describeRoute } from "hono-openapi" +import { resolver } from "hono-openapi/zod" +import { validator as zv } from "hono-openapi/zod" +import { checksum } from "ox/Address" +import { z } from "zod" +import { walletPermission } from "../../dtos" +import { isProduction } from "../../utils/isProduction" + +export const listConfigSchema = z + .object({ + user: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + lastUpdated: z + .string() + .optional() + .transform((val) => (val ? Number.parseInt(val) : undefined)) + .openapi({ + example: "1715702400", + type: "number", + }), + }) + .strict() + +export const inputSchema = listConfigSchema + +export const outputSchema = z.object({ + success: z.boolean(), + message: z.string().optional(), + data: z.array(walletPermission), +}) + +export const listConfigDescription = describeRoute({ + validateResponse: !isProduction, + description: "List configs", + responses: { + 200: { + description: "Configs listed", + content: { + "application/json": { + schema: resolver(outputSchema), + }, + }, + }, + }, +}) + +export const listConfigValidation = zv("query", inputSchema) diff --git a/apps/settings-service/src/index.ts b/apps/settings-service/src/index.ts new file mode 100644 index 0000000000..67d7761b49 --- /dev/null +++ b/apps/settings-service/src/index.ts @@ -0,0 +1,11 @@ +import { serve } from "@hono/node-server" +import { env } from "./env" +import { app } from "./server" +import type { AppType } from "./server" + +export type { AppType } + +serve({ + port: env.APP_PORT, + fetch: app.fetch, +}) diff --git a/apps/settings-service/src/migrate.ts b/apps/settings-service/src/migrate.ts new file mode 100644 index 0000000000..b7e7f4444c --- /dev/null +++ b/apps/settings-service/src/migrate.ts @@ -0,0 +1,38 @@ +import { type Migration, type MigrationProvider, Migrator } from "kysely" +import { db } from "./db/driver" +import { migrations } from "./db/migrations" + +class ObjectMigrationProvider implements MigrationProvider { + constructor(private migrations: Record) {} + + async getMigrations(): Promise> { + return this.migrations + } +} + +async function migrateToLatest() { + const migrator = new Migrator({ + db, + provider: new ObjectMigrationProvider(migrations), + }) + + const { error, results } = await migrator.migrateToLatest() + + results?.forEach((it) => { + if (it.status === "Success") { + console.log(`migration "${it.migrationName}" was executed successfully`) + } else if (it.status === "Error") { + console.error(`failed to execute migration "${it.migrationName}"`) + } + }) + + if (error) { + console.error("failed to migrate") + console.error(error) + process.exit(1) + } + + await db.destroy() +} + +migrateToLatest() diff --git a/apps/settings-service/src/repositories/permissionsRepository.ts b/apps/settings-service/src/repositories/permissionsRepository.ts new file mode 100644 index 0000000000..a851ec5d98 --- /dev/null +++ b/apps/settings-service/src/repositories/permissionsRepository.ts @@ -0,0 +1,28 @@ +import type { Hex } from "@happy.tech/common" +import { db } from "../db/driver" +import type { WalletPermission } from "../db/types" + +export function savePermission(permission: WalletPermission) { + return db + .insertInto("walletPermissions") + .values({ + user: permission.user, + invoker: permission.invoker, + parentCapability: permission.parentCapability, + caveats: JSON.stringify(permission.caveats), + date: permission.date, + id: permission.id, + updatedAt: Date.now(), + deleted: permission.deleted, + }) + .execute() +} + +export async function listPermissions(user: Hex, lastUpdated?: number): Promise { + return await db + .selectFrom("walletPermissions") + .where("user", "=", user) + .$if(lastUpdated !== undefined, (qb) => qb.where("updatedAt", ">", lastUpdated as number)) + .selectAll() + .execute() +} diff --git a/apps/settings-service/src/server/configRoute.ts b/apps/settings-service/src/server/configRoute.ts new file mode 100644 index 0000000000..76945c1cfa --- /dev/null +++ b/apps/settings-service/src/server/configRoute.ts @@ -0,0 +1,22 @@ +import { Hono } from "hono" +import { createConfig } from "../handlers/createConfig/createConfig" +import { createConfigDescription, createConfigValidation } from "../handlers/createConfig/validation" +import { listConfig } from "../handlers/listConfig.ts" +import { listConfigDescription, listConfigValidation } from "../handlers/listConfig.ts" +import { makeResponse } from "./makeResponse" + +export default new Hono() + .post("/create", createConfigDescription, createConfigValidation, async (c) => { + const input = c.req.valid("json") + const result = await createConfig(input) + + const [response, code] = makeResponse(result) + return c.json(response, code) + }) + .get("/list", listConfigDescription, listConfigValidation, async (c) => { + const input = c.req.valid("query") + const output = await listConfig(input) + + const [response, code] = makeResponse(output) + return c.json(response, code) + }) diff --git a/apps/settings-service/src/server/index.ts b/apps/settings-service/src/server/index.ts new file mode 100644 index 0000000000..ac973925a6 --- /dev/null +++ b/apps/settings-service/src/server/index.ts @@ -0,0 +1,101 @@ +import "zod-openapi/extend" +import { apiReference } from "@scalar/hono-api-reference" +import { Hono } from "hono" +import { openAPISpecs } from "hono-openapi" +import { cors } from "hono/cors" +import { HTTPException } from "hono/http-exception" +import { logger as loggerMiddleware } from "hono/logger" +import { prettyJSON as prettyJSONMiddleware } from "hono/pretty-json" +import { requestId as requestIdMiddleware } from "hono/request-id" +import { timeout as timeoutMiddleware } from "hono/timeout" +import { timing as timingMiddleware } from "hono/timing" +import { ZodError } from "zod" +import pkg from "../../package.json" assert { type: "json" } +import { env } from "../env" +import { isProduction } from "../utils/isProduction" +import { logJSONResponseMiddleware, logger } from "../utils/logger" +import configRoute from "./configRoute" + +const app = new Hono() + +// Middleware setup +app.use( + "*", + cors({ + origin: "*", + }), +) +app.use("*", timingMiddleware()) +app.use("*", loggerMiddleware()) +app.use("*", logJSONResponseMiddleware) +app.use("*", prettyJSONMiddleware()) +app.use("*", timeoutMiddleware(30_000)) +app.use("*", requestIdMiddleware()) + +// Routes setup +app.get("/", (c) => c.text("Welcome to the Settings Service!")) + +// OpenAPI documentation +app.get( + "/docs/openapi.json", + openAPISpecs(app, { + documentation: { + info: { title: "Settings", version: pkg.version, description: "Settings API" }, + servers: [ + ...(env.NODE_ENV === "development" + ? [ + { + url: `http://localhost:${env.APP_PORT}`, + description: "Local", + }, + ] + : []), + { url: "https://settings.testnet.happy.tech", description: "Testnet" }, + ], + }, + }), +) + +// API Reference UI +app.get( + "/docs", + apiReference({ + pageTitle: "Settings API Reference - HappyChain", + theme: "kepler", + spec: { url: "/docs/openapi.json" }, + showSidebar: true, + hideSearch: false, + }), +) + +app.notFound((c) => c.text("These aren't the droids you're looking for", 404)) +app.onError(async (err, c) => { + // re-format input validation errors + if (err instanceof HTTPException && err.cause instanceof ZodError) { + const error = err.cause.issues.map((i) => ({ path: i.path.join("."), message: i.message })) + return c.json({ error, requestId: c.get("requestId"), url: c.req.url }, 422) + } + + logger.warn({ requestId: c.get("requestId"), url: c.req.url }, err) + + // standard hono exceptions + // https://hono.dev/docs/api/exception#handling-httpexception + if (err instanceof HTTPException) return err.getResponse() + + // Unhandled Exceptions - should not occur + return c.json( + { + error: isProduction + ? `Something Happened, file a report with this key to find out more: ${c.get("requestId")}` + : err.message, + requestId: c.get("requestId"), + url: c.req.url, + }, + 500, + ) +}) + +app.route("/api/v1/settings", configRoute) + +export type AppType = typeof app +export { app } diff --git a/apps/settings-service/src/server/makeResponse.ts b/apps/settings-service/src/server/makeResponse.ts new file mode 100644 index 0000000000..bf7d472209 --- /dev/null +++ b/apps/settings-service/src/server/makeResponse.ts @@ -0,0 +1,45 @@ +import type { ContentfulStatusCode } from "hono/utils/http-status" +import type { Result } from "neverthrow" +import { HappySettingsError } from "../errors" + +type ResponseBodySuccess = { + success: true + message?: string + data?: TOk +} + +type ResponseBodyError = { + success: false + message: string +} + +type ResponseBody = ResponseBodySuccess | ResponseBodyError + +export function makeResponse(output: Result): [ResponseBody, ContentfulStatusCode] { + if (output.isOk()) + return [ + { + success: true, + ...(output.value !== undefined ? { data: output.value } : {}), + }, + 200, + ] as const + + if (output.error instanceof HappySettingsError) { + return [ + { + success: false, + message: output.error.message, + }, + output.error.statusCode, + ] as const + } + + return [ + { + success: false, + message: "Unexpected error", + }, + 500, + ] as const +} diff --git a/apps/settings-service/src/utils/isAppUrl.ts b/apps/settings-service/src/utils/isAppUrl.ts new file mode 100644 index 0000000000..e1182adbf6 --- /dev/null +++ b/apps/settings-service/src/utils/isAppUrl.ts @@ -0,0 +1,10 @@ +import type { AppURL } from "../db/types" + +export function isAppUrl(urlString: string): urlString is AppURL { + try { + const url = new URL(urlString) + return url.protocol === "http:" || url.protocol === "https:" + } catch { + return false + } +} diff --git a/apps/settings-service/src/utils/isProduction.ts b/apps/settings-service/src/utils/isProduction.ts new file mode 100644 index 0000000000..9c9b3ec318 --- /dev/null +++ b/apps/settings-service/src/utils/isProduction.ts @@ -0,0 +1,3 @@ +import { env } from "../env" + +export const isProduction = ["staging", "production"].includes(env.NODE_ENV) diff --git a/apps/settings-service/src/utils/isUUID.ts b/apps/settings-service/src/utils/isUUID.ts new file mode 100644 index 0000000000..7043584976 --- /dev/null +++ b/apps/settings-service/src/utils/isUUID.ts @@ -0,0 +1,6 @@ +import type { UUID } from "@happy.tech/common" +import { validate, version } from "uuid" + +export function isUUID(str: string): str is UUID { + return validate(str) && version(str) === 4 +} diff --git a/apps/settings-service/src/utils/logger.ts b/apps/settings-service/src/utils/logger.ts new file mode 100644 index 0000000000..395d519d4a --- /dev/null +++ b/apps/settings-service/src/utils/logger.ts @@ -0,0 +1,25 @@ +import { LogLevel, Logger, logLevel } from "@happy.tech/common" +import { createMiddleware } from "hono/factory" +import { env } from "../env" + +const defaultLogLevel = logLevel(env.LOG_LEVEL) +Logger.instance.setLogLevel(defaultLogLevel) + +export const logger = Logger.create("SettingsService") + +const responseLogger = Logger.create("Response", LogLevel.TRACE) +export const logJSONResponseMiddleware = createMiddleware(async (c, next) => { + await next() + + if (LogLevel.TRACE > responseLogger.logLevel) return + if (!c.req.path.startsWith("/api")) return + try { + responseLogger.trace(c.res.status, await c.res.clone().json()) + } catch (e) { + responseLogger.error("failed to parse response:", { + error: (e as Error)?.message, + requestId: c.get("requestId"), + url: c.req.url, + }) + } +}) diff --git a/apps/settings-service/tsconfig.build.json b/apps/settings-service/tsconfig.build.json new file mode 100644 index 0000000000..9db3d49060 --- /dev/null +++ b/apps/settings-service/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": ["../../support/configs/tsconfig.base.json", "../../support/configs/tsconfig.types.json"], + "compilerOptions": { + "strict": true + }, + "include": ["src", "./package.json"] +} diff --git a/apps/settings-service/tsconfig.json b/apps/settings-service/tsconfig.json new file mode 100644 index 0000000000..164a3cda4b --- /dev/null +++ b/apps/settings-service/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": ["../../support/configs/tsconfig.base.json"], + "compilerOptions": { + "strict": true + }, + "include": ["*.ts", "src", "./package.json"] +} diff --git a/bun.lock b/bun.lock index 5495b015b4..32479c2f85 100644 --- a/bun.lock +++ b/bun.lock @@ -178,6 +178,26 @@ "typescript": "^5.6.2", }, }, + "apps/settings-service": { + "name": "@happy.tech/settings-service", + "version": "0.1.0", + "dependencies": { + "@happy.tech/common": "workspace:1.0.0", + "@hono/node-server": "^1.13.8", + "@scalar/hono-api-reference": "^0.5.175", + "hono": "^4.7.2", + "hono-openapi": "^0.4.4", + "neverthrow": "^8.1.0", + "uuid": "^11.1.0", + "zod": "^3.23.8", + "zod-openapi": "^4.2.3", + }, + "devDependencies": { + "@happy.tech/happybuild": "workspace:1.0.0", + "hono-openapi": "^0.4.4", + "typescript": "^5.6.2", + }, + }, "apps/submitter": { "name": "@happy.tech/submitter", "version": "0.1.0", @@ -937,6 +957,8 @@ "@happy.tech/react": ["@happy.tech/react@workspace:packages/react"], + "@happy.tech/settings-service": ["@happy.tech/settings-service@workspace:apps/settings-service"], + "@happy.tech/submitter": ["@happy.tech/submitter@workspace:apps/submitter"], "@happy.tech/testing": ["@happy.tech/testing@workspace:support/testing"], @@ -995,8 +1017,6 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], - "@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="], "@kevincharm/bls-bn254": ["@kevincharm/bls-bn254@2.0.0", "", { "peerDependencies": { "ethers": "^6.8.0", "mcl-wasm": "^1.4.0" } }, "sha512-Y6Jk8oE6Re4v3rDkb51mF3rHfDakxCTGptVr/LX2iwtbA7qu3DLNBNgdBU3heYFA8t22JiX3BgnMTbBgm3AZMA=="], @@ -1421,45 +1441,45 @@ "@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.0", "", { "os": "android", "cpu": "arm" }, "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.0", "", { "os": "android", "cpu": "arm64" }, "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.0", "", { "os": "linux", "cpu": "arm" }, "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="], - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.0", "", { "os": "linux", "cpu": "none" }, "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg=="], + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="], - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ=="], + "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.0", "", { "os": "linux", "cpu": "none" }, "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.0", "", { "os": "linux", "cpu": "none" }, "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.0", "", { "os": "linux", "cpu": "x64" }, "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.0", "", { "os": "linux", "cpu": "x64" }, "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="], "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], @@ -1571,27 +1591,27 @@ "@solidity-parser/parser": ["@solidity-parser/parser@0.20.1", "", {}, "sha512-58I2sRpzaQUN+jJmWbHfbWf9AKfzqCI8JAdFB0vbyY+u8tBRcuTt9LxzasvR0LGQpcRv97eyV7l61FQ3Ib7zVw=="], - "@swc/core": ["@swc/core@1.12.6", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.23" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.12.6", "@swc/core-darwin-x64": "1.12.6", "@swc/core-linux-arm-gnueabihf": "1.12.6", "@swc/core-linux-arm64-gnu": "1.12.6", "@swc/core-linux-arm64-musl": "1.12.6", "@swc/core-linux-x64-gnu": "1.12.6", "@swc/core-linux-x64-musl": "1.12.6", "@swc/core-win32-arm64-msvc": "1.12.6", "@swc/core-win32-ia32-msvc": "1.12.6", "@swc/core-win32-x64-msvc": "1.12.6" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-TEpta6Gi02X1b2yDIzBOIr7dFprvq9jD8RbEVI2OcMrwklbCUx0Dz9TrAnklSOwRvYvH5JjCx8ht9E94oWiG7A=="], + "@swc/core": ["@swc/core@1.12.7", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.23" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.12.7", "@swc/core-darwin-x64": "1.12.7", "@swc/core-linux-arm-gnueabihf": "1.12.7", "@swc/core-linux-arm64-gnu": "1.12.7", "@swc/core-linux-arm64-musl": "1.12.7", "@swc/core-linux-x64-gnu": "1.12.7", "@swc/core-linux-x64-musl": "1.12.7", "@swc/core-win32-arm64-msvc": "1.12.7", "@swc/core-win32-ia32-msvc": "1.12.7", "@swc/core-win32-x64-msvc": "1.12.7" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-bcpllEihyUSnqp0UtXTvXc19CT4wp3tGWLENhWnjr4B5iEOkzqMu+xHGz1FI5IBatjfqOQb29tgIfv6IL05QaA=="], - "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.12.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yLiw+XzG+MilfFh0ON7qt67bfIr7UxB9JprhYReVOmLTBDmDVQSC3T4/vIuc+GwlX08ydnHy0ud4lIjTNW4uWg=="], + "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.12.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-w6BBT0hBRS56yS+LbReVym0h+iB7/PpCddqrn1ha94ra4rZ4R/A91A/rkv+LnQlPqU/+fhqdlXtCJU9mrhCBtA=="], - "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.12.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-qwg8ux5x5Gd1LmSUtL4s9mbyfzAjr5M6OtjO281dKHwc/GYiSc4j1urb2jNSo9FcMkfT78oAOW2L6HQiWv+j1A=="], + "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.12.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-jN6LhFfGOpm4DY2mXPgwH4aa9GLOwublwMVFFZ/bGnHYYCRitLZs9+JWBbyWs7MyGcA246Ew+EREx36KVEAxjA=="], - "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.12.6", "", { "os": "linux", "cpu": "arm" }, "sha512-pnkqH59JXBZu+MedaykMAC2or7tlUKeya7GKjzub+hkwxBP0ywWoFd+QYEdzp7QSziOt1VIHc4Wb9iZ2EfnzmA=="], + "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.12.7", "", { "os": "linux", "cpu": "arm" }, "sha512-rHn8XXi7G2StEtZRAeJ6c7nhJPDnqsHXmeNrAaYwk8Tvpa6ZYG2nT9E1OQNXj1/dfbSFTjdiA8M8ZvGYBlpBoA=="], - "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.12.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-h8+Ltx0NSEzIFHetkOYoQ+UQ59unYLuJ4wF6kCpxzS4HskRLjcngr1HgN0F/PRpptnrmJUPVQmfms/vjN8ndAQ=="], + "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.12.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-N15hKizSSh+hkZ2x3TDVrxq0TDcbvDbkQJi2ZrLb9fK+NdFUV/x+XF16ZDPlbxtrGXl1CT7VD439SNaMN9F7qw=="], - "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.12.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-GZu3MnB/5qtBxKEH46hgVDaplEe4mp3ZmQ1O2UpFCv/u/Ji3Gar5w5g2wHCZoT5AOouAhP1bh7IAEqjG/fbVfg=="], + "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.12.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-jxyINtBezpxd3eIUDiDXv7UQ87YWlPsM9KumOwJk09FkFSO4oYxV2RT+Wu+Nt5tVWue4N0MdXT/p7SQsDEk4YA=="], - "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.12.6", "", { "os": "linux", "cpu": "x64" }, "sha512-WwJLQFzMW9ufVjM6k3le4HUgBFNunyt2oghjcgn2YjnKj0Ka2LrrBHCxfS7lgFSCQh/shib2wIlKXUnlTEWQJw=="], + "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.12.7", "", { "os": "linux", "cpu": "x64" }, "sha512-PR4tPVwU1BQBfFDk2XfzXxsEIjF3x/bOV1BzZpYvrlkU0TKUDbR4t2wzvsYwD/coW7/yoQmlL70/qnuPtTp1Zw=="], - "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.12.6", "", { "os": "linux", "cpu": "x64" }, "sha512-rVGPNpI/sm8VVAhnB09Z/23OJP3ymouv6F4z4aYDbq/2JIwxqgpnl8gtMYP+Jw3XqabaFNjQmPiL15TvKCQaxQ=="], + "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.12.7", "", { "os": "linux", "cpu": "x64" }, "sha512-zy7JWfQtQItgMfUjSbbcS3DZqQUn2d9VuV0LSGpJxtTXwgzhRpF1S2Sj7cU9hGpbM27Y8RJ4DeFb3qbAufjbrw=="], - "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.12.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-EKDJ1+8vaIlJGMl2yvd2HklV4GNbpKKwNQcUQid6j91tFYz4/aByw+9vh/sDVG7ZNqdmdywSnLRo317UTt0zFg=="], + "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.12.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-52PeF0tyX04ZFD8nibNhy/GjMFOZWTEWPmIB3wpD1vIJ1po+smtBnEdRRll5WIXITKoiND8AeHlBNBPqcsdcwA=="], - "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.12.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-jnULikZkR2fpZgFUQs7NsNIztavM1JdX+8Y+8FsfChBvMvziKxXtvUPGjeVJ8nzU1wgMnaeilJX9vrwuDGkA0Q=="], + "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.12.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-WzQwkNMuhB1qQShT9uUgz/mX2j7NIEPExEtzvGsBT7TlZ9j1kGZ8NJcZH/fwOFcSJL4W7DnkL7nAhx6DBlSPaA=="], - "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.12.6", "", { "os": "win32", "cpu": "x64" }, "sha512-jL2Dcdcc/QZiQnwByP1uIE4k/mTlapzUng7owtLD2tSBBi1d+jPIdXIefdv+nccYJKRA+lKG3rRB6Tk9GrC7Kg=="], + "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.12.7", "", { "os": "win32", "cpu": "x64" }, "sha512-R52ivBi2lgjl+Bd3XCPum0YfgbZq/W1AUExITysddP9ErsNSwnreYyNB3exEijiazWGcqHEas2ChiuMOP7NYrA=="], "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], @@ -1643,15 +1663,15 @@ "@tanstack/react-store": ["@tanstack/react-store@0.7.1", "", { "dependencies": { "@tanstack/store": "0.7.1", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-qUTEKdId6QPWGiWyKAPf/gkN29scEsz6EUSJ0C3HgLMgaqTAyBsQ2sMCfGVcqb+kkhEXAdjleCgH6LAPD6f2sA=="], - "@tanstack/router-cli": ["@tanstack/router-cli@1.121.34", "", { "dependencies": { "@tanstack/router-generator": "^1.121.34", "chokidar": "^3.6.0", "yargs": "^17.7.2" }, "bin": { "tsr": "bin/tsr.cjs" } }, "sha512-OkBZ+n58wcXU45m1b0f58Uce0mkEoNav+ZeHwrPQWfvw3TQdc9cmH57Kss01YgeOBgCbTaPyAiPJltPl05LjmA=="], + "@tanstack/router-cli": ["@tanstack/router-cli@1.121.37", "", { "dependencies": { "@tanstack/router-generator": "^1.121.37", "chokidar": "^3.6.0", "yargs": "^17.7.2" }, "bin": { "tsr": "bin/tsr.cjs" } }, "sha512-Jc/YIBPBGgKt10wqquWMR3dntbUWSlhXaGCYFRb31SM+zRl+NSyOIhEO5zAm0oP4l605yyejs9gGj8BC9idn0Q=="], "@tanstack/router-core": ["@tanstack/router-core@1.121.34", "", { "dependencies": { "@tanstack/history": "1.121.34", "@tanstack/store": "^0.7.0", "tiny-invariant": "^1.3.3" } }, "sha512-CRH9dC8uLfFOKUGTbtOcMPv+weNVt2xs+me34KLX0Yja2yHG99oAUCBwamXsVQPpfjLFPYeJuKyo98+Mg+Ppeg=="], "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.121.34", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "solid-js": "^1.9.5" }, "peerDependencies": { "@tanstack/router-core": "^1.121.34", "csstype": "^3.0.10", "tiny-invariant": "^1.3.3" }, "optionalPeers": ["csstype"] }, "sha512-WAFYxJ7qViKxqkFmf+VsrtMT4TfYqdfWTBRhVU/6qi0k/+7TO2EHjl8/aGBhg6q0/IwO9wyGvcbDhJxm0DwWag=="], - "@tanstack/router-generator": ["@tanstack/router-generator@1.121.34", "", { "dependencies": { "@tanstack/router-core": "^1.121.34", "@tanstack/router-utils": "^1.121.21", "@tanstack/virtual-file-routes": "^1.121.21", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-JmxlhK8f7LIxHV8BAHikeiYGfwM9p5nxbEMpujNgTmC0dBwSyes+Zm0DzEL0EotVXZy+CyI/9bVa7z+9nWvqlA=="], + "@tanstack/router-generator": ["@tanstack/router-generator@1.121.37", "", { "dependencies": { "@tanstack/router-core": "^1.121.34", "@tanstack/router-utils": "^1.121.21", "@tanstack/virtual-file-routes": "^1.121.21", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-d7IqEDf962uJFNPMWXfPr+kUpS3Cv72azZhBNMMVmZUox/h3VDGgQ6OUnWXHwnno4xqDoS/mx9huTUnItoewaw=="], - "@tanstack/router-plugin": ["@tanstack/router-plugin@1.121.34", "", { "dependencies": { "@babel/core": "^7.26.8", "@babel/plugin-syntax-jsx": "^7.25.9", "@babel/plugin-syntax-typescript": "^7.25.9", "@babel/template": "^7.26.8", "@babel/traverse": "^7.26.8", "@babel/types": "^7.26.8", "@tanstack/router-core": "^1.121.34", "@tanstack/router-generator": "^1.121.34", "@tanstack/router-utils": "^1.121.21", "@tanstack/virtual-file-routes": "^1.121.21", "babel-dead-code-elimination": "^1.0.10", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.121.34", "vite": ">=5.0.0 || >=6.0.0", "vite-plugin-solid": "^2.11.2", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-ZmX/tkdd/ZKLdr17ewKJTTBGkXQDeOfQKSCuuEW5IjiNfWjT5gx8rQDvcYUSRcZdpUZ0LvDBxJUI74oHQ3sAiw=="], + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.121.37", "", { "dependencies": { "@babel/core": "^7.26.8", "@babel/plugin-syntax-jsx": "^7.25.9", "@babel/plugin-syntax-typescript": "^7.25.9", "@babel/template": "^7.26.8", "@babel/traverse": "^7.26.8", "@babel/types": "^7.26.8", "@tanstack/router-core": "^1.121.34", "@tanstack/router-generator": "^1.121.37", "@tanstack/router-utils": "^1.121.21", "@tanstack/virtual-file-routes": "^1.121.21", "babel-dead-code-elimination": "^1.0.10", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.121.34", "vite": ">=5.0.0 || >=6.0.0", "vite-plugin-solid": "^2.11.2", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-zrolQ1J53xDUdxdO6MLfvnpVINnkIfOnEDVeX3kwHKBGQ5zyGdbolVcVVrJIRYQS0SJoWesn8cf8j+z+u8nZtg=="], "@tanstack/router-utils": ["@tanstack/router-utils@1.121.21", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/generator": "^7.27.5", "@babel/parser": "^7.27.5", "@babel/preset-typescript": "^7.27.1", "ansis": "^4.1.0", "diff": "^8.0.2" } }, "sha512-u7ubq1xPBtNiU7Fm+EOWlVWdgFLzuKOa1thhqdscVn8R4dNMUd1VoOjZ6AKmLw201VaUhFtlX+u0pjzI6szX7A=="], @@ -1789,7 +1809,7 @@ "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - "@types/lodash": ["@types/lodash@4.17.18", "", {}, "sha512-KJ65INaxqxmU6EoCiJmRPZC9H9RVWCRd349tXM2M3O5NA7cY6YL7c0bHAHQ93NOfTObEQ004kd2QVHs/r0+m4g=="], + "@types/lodash": ["@types/lodash@4.17.19", "", {}, "sha512-NYqRyg/hIQrYPT9lbOeYc3kIRabJDn/k4qQHIXUpx88CBDww2fD15Sg5kbXlW86zm2XEW4g0QxkTI3/Kfkc7xQ=="], "@types/lru-cache": ["@types/lru-cache@5.1.1", "", {}, "sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw=="], @@ -1803,7 +1823,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@22.15.32", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA=="], + "@types/node": ["@types/node@22.15.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wzoocdnnpSxZ+6CjW4ADCK1jVmd1S/J3ArNWfn8FDDQtRm8dkDg7TA+mvek2wNrfCgwuZxqEOiB9B1XCJ6+dbw=="], "@types/pbkdf2": ["@types/pbkdf2@3.1.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew=="], @@ -1895,11 +1915,11 @@ "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], - "@volar/language-core": ["@volar/language-core@2.4.14", "", { "dependencies": { "@volar/source-map": "2.4.14" } }, "sha512-X6beusV0DvuVseaOEy7GoagS4rYHgDHnTrdOj5jeUb49fW5ceQyP9Ej5rBhqgz2wJggl+2fDbbojq1XKaxDi6w=="], + "@volar/language-core": ["@volar/language-core@2.4.15", "", { "dependencies": { "@volar/source-map": "2.4.15" } }, "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA=="], - "@volar/source-map": ["@volar/source-map@2.4.14", "", {}, "sha512-5TeKKMh7Sfxo8021cJfmBzcjfY1SsXsPMMjMvjY7ivesdnybqqS+GxGAoXHAOUawQTwtdUxgP65Im+dEmvWtYQ=="], + "@volar/source-map": ["@volar/source-map@2.4.15", "", {}, "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg=="], - "@volar/typescript": ["@volar/typescript@2.4.14", "", { "dependencies": { "@volar/language-core": "2.4.14", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-p8Z6f/bZM3/HyCdRNFZOEEzts51uV8WHeN8Tnfnm2EBv6FDB2TQLzfVx7aJvnl8ofKAOnS64B2O8bImBFaauRw=="], + "@volar/typescript": ["@volar/typescript@2.4.15", "", { "dependencies": { "@volar/language-core": "2.4.15", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg=="], "@vue/compiler-core": ["@vue/compiler-core@3.5.17", "", { "dependencies": { "@babel/parser": "^7.27.5", "@vue/shared": "3.5.17", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA=="], @@ -2301,7 +2321,7 @@ "browserify-zlib": ["browserify-zlib@0.2.0", "", { "dependencies": { "pako": "~1.0.5" } }, "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA=="], - "browserslist": ["browserslist@4.25.0", "", { "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA=="], + "browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="], "bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], @@ -2341,7 +2361,7 @@ "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], - "caniuse-lite": ["caniuse-lite@1.0.30001724", "", {}, "sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA=="], + "caniuse-lite": ["caniuse-lite@1.0.30001726", "", {}, "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw=="], "cbor": ["cbor@10.0.3", "", { "dependencies": { "nofilter": "^3.0.2" } }, "sha512-72Jnj81xMsqepqdcSdf2+fflz/UDsThOHy5hj2MW5F5xzHL8Oa0KQ6I6V9CwVUPxg5pf+W9xp6W2KilaRXWWtw=="], @@ -2609,7 +2629,7 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "electron-to-chromium": ["electron-to-chromium@1.5.172", "", {}, "sha512-fnKW9dGgmBfsebbYognQSv0CGGLFH1a5iV9EDYTBwmAQn+whbzHbLFlC+3XbHc8xaNtpO0etm8LOcRXs1qMRkQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.176", "", {}, "sha512-2nDK9orkm7M9ZZkjO3PjbEd3VUulQLyg5T9O3enJdFvUg46Hzd4DUvTvAuEgbdHYXyFsiG4A5sO9IzToMH1cDg=="], "elliptic": ["elliptic@6.6.1", "", { "dependencies": { "bn.js": "^4.11.9", "brorand": "^1.1.0", "hash.js": "^1.0.0", "hmac-drbg": "^1.0.1", "inherits": "^2.0.4", "minimalistic-assert": "^1.0.1", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g=="], @@ -2633,7 +2653,7 @@ "engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="], - "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], + "enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="], "enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="], @@ -2701,7 +2721,7 @@ "eslint-plugin-n": ["eslint-plugin-n@17.20.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.5.0", "@typescript-eslint/utils": "^8.26.1", "enhanced-resolve": "^5.17.1", "eslint-plugin-es-x": "^7.8.0", "get-tsconfig": "^4.8.1", "globals": "^15.11.0", "ignore": "^5.3.2", "minimatch": "^9.0.5", "semver": "^7.6.3", "ts-declaration-location": "^1.0.6" }, "peerDependencies": { "eslint": ">=8.23.0" } }, "sha512-IRSoatgB/NQJZG5EeTbv/iAx1byOGdbbyhQrNvWdCfTnmPxUT0ao9/eGOeG7ljD8wJBsxwE8f6tES5Db0FRKEw=="], - "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.0", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.7" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-8qsOYwkkGrahrgoUv76NZi23koqXOGiiEzXMrT8Q7VcYaUISR+5MorIUxfWqYXN0fN/31WbSrxCxFkVQ43wwrA=="], + "eslint-plugin-prettier": ["eslint-plugin-prettier@5.5.1", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.11.7" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw=="], "eslint-plugin-promise": ["eslint-plugin-promise@7.2.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" } }, "sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA=="], @@ -3019,7 +3039,7 @@ "hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg=="], - "hono": ["hono@4.8.2", "", {}, "sha512-hM+1RIn9PK1I6SiTNS6/y7O1mvg88awYLFEuEtoiMtRyT3SD2iu9pSFgbBXT3b1Ua4IwzvSTLvwO0SEhDxCi4w=="], + "hono": ["hono@4.8.3", "", {}, "sha512-jYZ6ZtfWjzBdh8H/0CIFfCBHaFL75k+KMzaM177hrWWm2TWL39YMYaJgB74uK/niRc866NMlH9B8uCvIo284WQ=="], "hono-openapi": ["hono-openapi@0.4.8", "", { "dependencies": { "json-schema-walker": "^2.0.0" }, "peerDependencies": { "@hono/arktype-validator": "^2.0.0", "@hono/effect-validator": "^1.2.0", "@hono/typebox-validator": "^0.2.0 || ^0.3.0", "@hono/valibot-validator": "^0.5.1", "@hono/zod-validator": "^0.4.1", "@sinclair/typebox": "^0.34.9", "@valibot/to-json-schema": "^1.0.0-beta.3", "arktype": "^2.0.0", "effect": "^3.11.3", "hono": "^4.6.13", "openapi-types": "^12.1.3", "valibot": "^1.0.0-beta.9", "zod": "^3.23.8", "zod-openapi": "^4.0.0" }, "optionalPeers": ["@hono/arktype-validator", "@hono/effect-validator", "@hono/typebox-validator", "@hono/valibot-validator", "@hono/zod-validator", "@sinclair/typebox", "@valibot/to-json-schema", "arktype", "effect", "hono", "valibot", "zod", "zod-openapi"] }, "sha512-LYr5xdtD49M7hEAduV1PftOMzuT8ZNvkyWfh1DThkLsIr4RkvDb12UxgIiFbwrJB6FLtFXLoOZL9x4IeDk2+VA=="], @@ -3581,7 +3601,7 @@ "node-jq": ["node-jq@6.0.1", "", { "dependencies": { "is-valid-path": "^0.1.1", "strip-final-newline": "^2.0.0", "tar": "^7.4.0", "tempy": "^3.1.0", "zod": "^3.23.8" }, "bin": { "node-jq": "bin/jq" } }, "sha512-jt1H7i2c/BZUkid7O8uK4KWw5wDZgpsSHq8WAVv8SpedToUOpA6kAgoLnoAHmorAUGJTfICZlniKkMaEl28Uyw=="], - "node-mock-http": ["node-mock-http@1.0.0", "", {}, "sha512-0uGYQ1WQL1M5kKvGRXWQ3uZCHtLTO8hln3oBjIusM75WoesZ909uQJs/Hb946i2SS+Gsrhkaa6iAO17jRIv6DQ=="], + "node-mock-http": ["node-mock-http@1.0.1", "", {}, "sha512-0gJJgENizp4ghds/Ywu2FCmcRsgBTmRQzYPZm61wy+Em2sBarSka0OhQS5huLBg6od1zkNpnWMCZloQDFVvOMQ=="], "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], @@ -3713,7 +3733,7 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "pathval": ["pathval@2.0.0", "", {}, "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA=="], + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], "pbkdf2": ["pbkdf2@3.1.3", "", { "dependencies": { "create-hash": "~1.1.3", "create-hmac": "^1.1.7", "ripemd160": "=2.0.1", "safe-buffer": "^5.2.1", "sha.js": "^2.4.11", "to-buffer": "^1.2.0" } }, "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA=="], @@ -3985,7 +4005,7 @@ "rlp": ["rlp@2.2.7", "", { "dependencies": { "bn.js": "^5.2.0" }, "bin": { "rlp": "bin/rlp" } }, "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ=="], - "rollup": ["rollup@4.44.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.0", "@rollup/rollup-android-arm64": "4.44.0", "@rollup/rollup-darwin-arm64": "4.44.0", "@rollup/rollup-darwin-x64": "4.44.0", "@rollup/rollup-freebsd-arm64": "4.44.0", "@rollup/rollup-freebsd-x64": "4.44.0", "@rollup/rollup-linux-arm-gnueabihf": "4.44.0", "@rollup/rollup-linux-arm-musleabihf": "4.44.0", "@rollup/rollup-linux-arm64-gnu": "4.44.0", "@rollup/rollup-linux-arm64-musl": "4.44.0", "@rollup/rollup-linux-loongarch64-gnu": "4.44.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0", "@rollup/rollup-linux-riscv64-gnu": "4.44.0", "@rollup/rollup-linux-riscv64-musl": "4.44.0", "@rollup/rollup-linux-s390x-gnu": "4.44.0", "@rollup/rollup-linux-x64-gnu": "4.44.0", "@rollup/rollup-linux-x64-musl": "4.44.0", "@rollup/rollup-win32-arm64-msvc": "4.44.0", "@rollup/rollup-win32-ia32-msvc": "4.44.0", "@rollup/rollup-win32-x64-msvc": "4.44.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA=="], + "rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="], "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], @@ -4403,7 +4423,7 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], "v8-compile-cache-lib": ["v8-compile-cache-lib@3.0.1", "", {}, "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg=="], @@ -4611,6 +4631,10 @@ "@ethersproject/providers/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + "@firebase/component/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@firebase/logger/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@happy.tech/txm/kysely-bun-sqlite": ["kysely-bun-sqlite@0.4.0", "", { "dependencies": { "bun-types": "^1.1.31" }, "peerDependencies": { "kysely": "^0.28.2" } }, "sha512-2EkQE5sT4ewiw7IWfJsAkpxJ/QPVKXKO5sRYI/xjjJIJlECuOdtG+ssYM0twZJySrdrmuildNPFYVreyu1EdZg=="], "@humanwhocodes/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -4655,6 +4679,10 @@ "@metamask/rpc-errors/@metamask/utils": ["@metamask/utils@9.3.0", "", { "dependencies": { "@ethereumjs/tx": "^4.2.0", "@metamask/superstruct": "^3.1.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.3", "@types/debug": "^4.1.7", "debug": "^4.3.4", "pony-cause": "^2.1.10", "semver": "^7.5.4", "uuid": "^9.0.1" } }, "sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g=="], + "@metamask/sdk/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "@metamask/sdk-communication-layer/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "@metamask/utils/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@microsoft/api-extractor/minimatch": ["minimatch@3.0.8", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q=="], @@ -4669,14 +4697,6 @@ "@nomiclabs/hardhat-etherscan/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@opentelemetry/exporter-logs-otlp-grpc/@grpc/grpc-js": ["@grpc/grpc-js@1.13.4", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg=="], - - "@opentelemetry/exporter-metrics-otlp-grpc/@grpc/grpc-js": ["@grpc/grpc-js@1.13.4", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg=="], - - "@opentelemetry/exporter-trace-otlp-grpc/@grpc/grpc-js": ["@grpc/grpc-js@1.13.4", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg=="], - - "@opentelemetry/otlp-grpc-exporter-base/@grpc/grpc-js": ["@grpc/grpc-js@1.13.4", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg=="], - "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], "@radix-ui/react-use-is-hydrated/use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], @@ -4783,7 +4803,7 @@ "@tanstack/react-store/use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], - "@tanstack/router-generator/prettier": ["prettier@3.6.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw=="], + "@tanstack/router-generator/prettier": ["prettier@3.6.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A=="], "@tanstack/router-generator/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], @@ -4799,7 +4819,7 @@ "@tkey/tss/ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="], - "@toruslabs/eslint-config-typescript/prettier": ["prettier@3.6.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw=="], + "@toruslabs/eslint-config-typescript/prettier": ["prettier@3.6.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A=="], "@toruslabs/metadata-helpers/ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="], @@ -4879,6 +4899,8 @@ "ast-types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "async-mutex/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "bl/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "boxen/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], @@ -4951,7 +4973,7 @@ "eslint-plugin-n/globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], - "eslint-plugin-prettier/prettier": ["prettier@3.6.0", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw=="], + "eslint-plugin-prettier/prettier": ["prettier@3.6.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A=="], "eslint-plugin-tsdoc/@microsoft/tsdoc": ["@microsoft/tsdoc@0.15.0", "", {}, "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA=="], @@ -4981,6 +5003,8 @@ "ethereumjs-wallet/aes-js": ["aes-js@3.1.2", "", {}, "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ=="], + "ethereumjs-wallet/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "ethers/@noble/curves": ["@noble/curves@1.2.0", "", { "dependencies": { "@noble/hashes": "1.3.2" } }, "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw=="], "ethers/@noble/hashes": ["@noble/hashes@1.3.2", "", {}, "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ=="], @@ -4995,6 +5019,8 @@ "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "extension-port-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "find-replace/array-back": ["array-back@1.0.4", "", { "dependencies": { "typical": "^2.6.0" } }, "sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw=="], @@ -5033,6 +5059,8 @@ "hardhat/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + "hardhat/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "hardhat/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "hardhat-deploy/ethers": ["ethers@5.8.0", "", { "dependencies": { "@ethersproject/abi": "5.8.0", "@ethersproject/abstract-provider": "5.8.0", "@ethersproject/abstract-signer": "5.8.0", "@ethersproject/address": "5.8.0", "@ethersproject/base64": "5.8.0", "@ethersproject/basex": "5.8.0", "@ethersproject/bignumber": "5.8.0", "@ethersproject/bytes": "5.8.0", "@ethersproject/constants": "5.8.0", "@ethersproject/contracts": "5.8.0", "@ethersproject/hash": "5.8.0", "@ethersproject/hdnode": "5.8.0", "@ethersproject/json-wallets": "5.8.0", "@ethersproject/keccak256": "5.8.0", "@ethersproject/logger": "5.8.0", "@ethersproject/networks": "5.8.0", "@ethersproject/pbkdf2": "5.8.0", "@ethersproject/properties": "5.8.0", "@ethersproject/providers": "5.8.0", "@ethersproject/random": "5.8.0", "@ethersproject/rlp": "5.8.0", "@ethersproject/sha2": "5.8.0", "@ethersproject/signing-key": "5.8.0", "@ethersproject/solidity": "5.8.0", "@ethersproject/strings": "5.8.0", "@ethersproject/transactions": "5.8.0", "@ethersproject/units": "5.8.0", "@ethersproject/wallet": "5.8.0", "@ethersproject/web": "5.8.0", "@ethersproject/wordlists": "5.8.0" } }, "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg=="], @@ -5103,8 +5131,6 @@ "mcl-wasm/@types/node": ["@types/node@20.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA=="], - "md5.js/hash-base": ["hash-base@3.1.0", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", "safe-buffer": "^5.2.0" } }, "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA=="], - "mdast-util-directive/@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "mdast-util-directive/unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], @@ -5181,14 +5207,14 @@ "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], - "radix-vue/@internationalized/date": ["@internationalized/date@3.8.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/wENk7CbvLbkUvX1tu0mwq49CVkkWpkXubGel6birjRPyo6uQ4nQpnq5xZu823zRCwwn82zgHrvgF1vZyvmVgA=="], - - "radix-vue/@internationalized/number": ["@internationalized/number@3.6.3", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-p+Zh1sb6EfrfVaS86jlHGQ9HA66fJhV9x5LiE5vCbZtXEHAuhcmUZUdZ4WrFpUBfNalr2OkAJI5AcKEQF+Lebw=="], - "radix-vue/nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "react-remove-scroll-bar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "react-style-singleton/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "read-cache/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], "read-yaml-file/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], @@ -5197,8 +5223,6 @@ "recast/esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "recast/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "recursive-readdir/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "rehype-autolink-headings/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], @@ -5211,8 +5235,6 @@ "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "ripemd160/hash-base": ["hash-base@3.1.0", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", "safe-buffer": "^5.2.0" } }, "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA=="], - "sc-istanbul/glob": ["glob@5.0.15", "", { "dependencies": { "inflight": "^1.0.4", "inherits": "2", "minimatch": "2 || 3", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA=="], "sc-istanbul/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], @@ -5305,6 +5327,10 @@ "uri-js/punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "use-callback-ref/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "use-sidecar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "valtio/proxy-compare": ["proxy-compare@2.6.0", "", {}, "sha512-8xuCeM3l8yqdmbPoYeLbrAXCBWu19XEYc5/F28f5qOaoAIMyfmBUkl5axiK+x9olUvRlcekvnm98AP9RDngOIw=="], "valtio/use-sync-external-store": ["use-sync-external-store@1.2.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA=="], @@ -5559,8 +5585,12 @@ "eslint/optionator/type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "eth-json-rpc-filters/async-mutex/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + "extension-port-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "ghost-testrpc/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "ghost-testrpc/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], @@ -5879,8 +5909,6 @@ "mocha/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "pbkdf2/create-hash/ripemd160/hash-base": ["hash-base@3.1.0", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", "safe-buffer": "^5.2.0" } }, "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA=="], - "pkg-dir/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], From 484790a8478d91ebd357a6cebff2bea3b6322646 Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Fri, 16 May 2025 16:35:51 +0200 Subject: [PATCH 02/19] feat(settins-service): update and delete methods --- apps/iframe/package.json | 1 + apps/iframe/src/state/permissions.ts | 72 +++++++++++++++++++ apps/settings-service/src/db/types.ts | 6 +- apps/settings-service/src/dtos.ts | 2 + apps/settings-service/src/errors.ts | 6 ++ .../src/handlers/createConfig/validation.ts | 4 +- .../src/handlers/deleteConfig/deleteConfig.ts | 9 +++ .../src/handlers/deleteConfig/index.ts | 2 + .../src/handlers/deleteConfig/types.ts | 5 ++ .../src/handlers/deleteConfig/validation.ts | 36 ++++++++++ .../{listConfig.ts => listConfig}/index.ts | 0 .../listConfig.ts | 11 +-- .../{listConfig.ts => listConfig}/types.ts | 0 .../validation.ts | 0 .../src/handlers/updateConfig/index.ts | 2 + .../src/handlers/updateConfig/types.ts | 5 ++ .../src/handlers/updateConfig/updateConfig.ts | 16 +++++ .../src/handlers/updateConfig/validation.ts | 32 +++++++++ .../src/repositories/permissionsRepository.ts | 66 +++++++++++++---- .../src/server/configRoute.ts | 21 +++++- bun.lock | 11 +-- 21 files changed, 271 insertions(+), 36 deletions(-) create mode 100644 apps/settings-service/src/handlers/deleteConfig/deleteConfig.ts create mode 100644 apps/settings-service/src/handlers/deleteConfig/index.ts create mode 100644 apps/settings-service/src/handlers/deleteConfig/types.ts create mode 100644 apps/settings-service/src/handlers/deleteConfig/validation.ts rename apps/settings-service/src/handlers/{listConfig.ts => listConfig}/index.ts (100%) rename apps/settings-service/src/handlers/{listConfig.ts => listConfig}/listConfig.ts (60%) rename apps/settings-service/src/handlers/{listConfig.ts => listConfig}/types.ts (100%) rename apps/settings-service/src/handlers/{listConfig.ts => listConfig}/validation.ts (100%) create mode 100644 apps/settings-service/src/handlers/updateConfig/index.ts create mode 100644 apps/settings-service/src/handlers/updateConfig/types.ts create mode 100644 apps/settings-service/src/handlers/updateConfig/updateConfig.ts create mode 100644 apps/settings-service/src/handlers/updateConfig/validation.ts diff --git a/apps/iframe/package.json b/apps/iframe/package.json index cf0ad82c37..b347367ab1 100644 --- a/apps/iframe/package.json +++ b/apps/iframe/package.json @@ -12,6 +12,7 @@ "@happy.tech/common": "workspace:*", "@happy.tech/contracts": "workspace:0.2.0", "@happy.tech/wallet-common": "workspace:*", + "@legendapp/state": "^3.0.0-beta.30", "@metamask/safe-event-emitter": "^3.1.1", "@phosphor-icons/react": "^2.1.10", "@tanstack/react-query": "^5.56.2", diff --git a/apps/iframe/src/state/permissions.ts b/apps/iframe/src/state/permissions.ts index 39547f868e..7e2786714f 100644 --- a/apps/iframe/src/state/permissions.ts +++ b/apps/iframe/src/state/permissions.ts @@ -11,6 +11,9 @@ import { checkIfCaveatsMatch } from "../utils/checkIfCaveatsMatch" import { emitUserUpdate } from "../utils/emitUserUpdate" import { revokedSessionKeys } from "./interfaceState" import { getUser, userAtom } from "./user" +import { syncedCrud } from '@legendapp/state/sync-plugins/crud' +import { observable } from "@legendapp/state" +import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage"; // STORE INSTANTIATION const store = getDefaultStore() @@ -87,6 +90,54 @@ export type SessionKeyRequest = { */ export type PermissionsRequest = string | PermissionRequestObject + +const permissionsMapLegend = observable(syncedCrud({ + list: async ({ lastSync }) => { + const response = await fetch(`http://localhost:3000/api/v1/settings/list?user=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266${lastSync ? `&lastUpdated=${lastSync}` : ""}`) + const data = await response.json() + + + + return data.data + }, + create: async (data: PermissionsMap) => { + const response = await fetch("http://localhost:3000/api/v1/settings/create", { + method: "POST", + body: JSON.stringify(data), + }) + return await response.json() + }, + update: async (data: PermissionsMap) => { + console.log("update", data) + + const response = await fetch("http://localhost:3000/api/v1/settings/update", { + method: "POST", + body: JSON.stringify(data), + }) + return await response.json() + }, + subscribe: ({ refresh }) => { + // Set up an interval to refresh messages every 5 seconds + const intervalId = setInterval(() => { + console.log("Refreshing config (5-second interval)"); + refresh(); + }, 5000); + + // Return cleanup function to clear the interval when unsubscribing + return () => { + clearInterval(intervalId); + }; + }, + persist: { + plugin: ObservablePersistLocalStorage, + name: 'config-legend', + retrySync: true // Retry sync after reload + }, + fieldUpdatedAt: 'updatedAt', + fieldDeleted: 'deleted', + changesSince: 'last-sync' +})) + /** * Maps an user + app pair to a {@link AppPermissions}, which is the set of permissions * for that user on that app. @@ -208,6 +259,20 @@ function setAppPermissions(app: AppURL, appPermissions: AppPermissions): void { [user.address]: { ...prev[user.address], [app]: appPermissions }, } }) + + const id = createUUID() + + permissionsMapLegend[id].set({ + id, + type: "WalletPermissions", + user: user.address, + invoker: app, + parentCapability: appPermissions.eth_accounts.parentCapability, + caveats: appPermissions.eth_accounts.caveats, + date: appPermissions.eth_accounts.date, + deleted: false, + updatedAt: Date.now(), + }) } // === CLEAR PERMISSIONS =========================================================================== @@ -222,6 +287,13 @@ export function clearPermissions(): void { const { [user.address]: _, ...rest } = prev return rest }) + + Object.values(permissionsMapLegend).forEach((p) => { + if (p.user === user.address) { + p.deleted = true + } + }) + } /** diff --git a/apps/settings-service/src/db/types.ts b/apps/settings-service/src/db/types.ts index bdf09604e7..2552d10501 100644 --- a/apps/settings-service/src/db/types.ts +++ b/apps/settings-service/src/db/types.ts @@ -1,6 +1,6 @@ import type { Hex } from "@happy.tech/common" import type { HTTPString, UUID } from "@happy.tech/common" -import type { ColumnType, Selectable } from "kysely" +import type { ColumnType } from "kysely" export type AppURL = HTTPString & { _brand: "AppHTTPString" } @@ -29,11 +29,9 @@ export type WalletPermissionTable = { // Not in the EIP, but Viem wants this. id: UUID updatedAt: number - deleted: boolean + deleted: ColumnType } -export type WalletPermission = Selectable - export interface Database { walletPermissions: WalletPermissionTable } diff --git a/apps/settings-service/src/dtos.ts b/apps/settings-service/src/dtos.ts index 956408b595..59beceefee 100644 --- a/apps/settings-service/src/dtos.ts +++ b/apps/settings-service/src/dtos.ts @@ -26,3 +26,5 @@ export const walletPermission = z.object({ updatedAt: z.number().openapi({ example: 1715702400 }), deleted: z.boolean().openapi({ example: false }), }) + +export type WalletPermission = z.infer diff --git a/apps/settings-service/src/errors.ts b/apps/settings-service/src/errors.ts index 11db85717a..e4d8528478 100644 --- a/apps/settings-service/src/errors.ts +++ b/apps/settings-service/src/errors.ts @@ -9,3 +9,9 @@ export abstract class HappySettingsError extends Error { this.statusCode = statusCode } } + +export class PermissionNotFoundError extends HappySettingsError { + constructor(message?: string, options?: ErrorOptions) { + super(404, message || "Permission not found", options) + } +} diff --git a/apps/settings-service/src/handlers/createConfig/validation.ts b/apps/settings-service/src/handlers/createConfig/validation.ts index 138ab915ea..4aff719b9c 100644 --- a/apps/settings-service/src/handlers/createConfig/validation.ts +++ b/apps/settings-service/src/handlers/createConfig/validation.ts @@ -5,7 +5,9 @@ import { z } from "zod" import { walletPermission } from "../../dtos" import { isProduction } from "../../utils/isProduction" -export const inputSchema = z.discriminatedUnion("type", [walletPermission]) +export const inputSchema: z.ZodDiscriminatedUnion<"type", [typeof walletPermission]> = z.discriminatedUnion("type", [ + walletPermission, +]) export const outputSchema = z.object({ success: z.boolean(), diff --git a/apps/settings-service/src/handlers/deleteConfig/deleteConfig.ts b/apps/settings-service/src/handlers/deleteConfig/deleteConfig.ts new file mode 100644 index 0000000000..470194d2a6 --- /dev/null +++ b/apps/settings-service/src/handlers/deleteConfig/deleteConfig.ts @@ -0,0 +1,9 @@ +import { type Result, ok } from "neverthrow" +import { deletePermission } from "../../repositories/permissionsRepository" +import type { DeleteConfigInput } from "./types" + +export async function deleteConfig(input: DeleteConfigInput): Promise> { + await deletePermission(input.id) + + return ok(undefined) +} diff --git a/apps/settings-service/src/handlers/deleteConfig/index.ts b/apps/settings-service/src/handlers/deleteConfig/index.ts new file mode 100644 index 0000000000..70a85d4d95 --- /dev/null +++ b/apps/settings-service/src/handlers/deleteConfig/index.ts @@ -0,0 +1,2 @@ +export { deleteConfig } from "./deleteConfig" +export { deleteConfigValidation, deleteConfigDescription } from "./validation" diff --git a/apps/settings-service/src/handlers/deleteConfig/types.ts b/apps/settings-service/src/handlers/deleteConfig/types.ts new file mode 100644 index 0000000000..08f309b034 --- /dev/null +++ b/apps/settings-service/src/handlers/deleteConfig/types.ts @@ -0,0 +1,5 @@ +import type { z } from "zod" +import type { inputSchema, outputSchema } from "./validation" + +export type DeleteConfigInput = z.infer +export type DeleteConfigOutput = z.infer diff --git a/apps/settings-service/src/handlers/deleteConfig/validation.ts b/apps/settings-service/src/handlers/deleteConfig/validation.ts new file mode 100644 index 0000000000..46673ffd63 --- /dev/null +++ b/apps/settings-service/src/handlers/deleteConfig/validation.ts @@ -0,0 +1,36 @@ +import { describeRoute } from "hono-openapi" +import { resolver } from "hono-openapi/zod" +import { validator as zv } from "hono-openapi/zod" +import { z } from "zod" +import { isProduction } from "../../utils/isProduction" +import { isUUID } from "../../utils/isUUID" + +export const deleteConfigSchema = z + .object({ + id: z.string().refine(isUUID).openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), + }) + .strict() + +export const inputSchema = deleteConfigSchema + +export const outputSchema = z.object({ + success: z.boolean(), + message: z.string().optional(), +}) + +export const deleteConfigDescription = describeRoute({ + validateResponse: !isProduction, + description: "Delete config", + responses: { + 200: { + description: "Config deleted", + content: { + "application/json": { + schema: resolver(outputSchema), + }, + }, + }, + }, +}) + +export const deleteConfigValidation = zv("param", inputSchema) diff --git a/apps/settings-service/src/handlers/listConfig.ts/index.ts b/apps/settings-service/src/handlers/listConfig/index.ts similarity index 100% rename from apps/settings-service/src/handlers/listConfig.ts/index.ts rename to apps/settings-service/src/handlers/listConfig/index.ts diff --git a/apps/settings-service/src/handlers/listConfig.ts/listConfig.ts b/apps/settings-service/src/handlers/listConfig/listConfig.ts similarity index 60% rename from apps/settings-service/src/handlers/listConfig.ts/listConfig.ts rename to apps/settings-service/src/handlers/listConfig/listConfig.ts index 68699f4db5..60046583be 100644 --- a/apps/settings-service/src/handlers/listConfig.ts/listConfig.ts +++ b/apps/settings-service/src/handlers/listConfig/listConfig.ts @@ -1,17 +1,10 @@ import { type Result, ok } from "neverthrow" -import type { WalletPermission } from "../../db/types" +import type { WalletPermission } from "../../dtos" import { listPermissions } from "../../repositories/permissionsRepository" import type { ListConfigInput } from "./types" export async function listConfig(input: ListConfigInput): Promise> { const permissions = await listPermissions(input.user, input.lastUpdated) - console.log(permissions.map((p) => p.caveats)) - - return ok( - permissions.map((p) => ({ - type: "WalletPermissions", - ...p, - })), - ) + return ok(permissions) } diff --git a/apps/settings-service/src/handlers/listConfig.ts/types.ts b/apps/settings-service/src/handlers/listConfig/types.ts similarity index 100% rename from apps/settings-service/src/handlers/listConfig.ts/types.ts rename to apps/settings-service/src/handlers/listConfig/types.ts diff --git a/apps/settings-service/src/handlers/listConfig.ts/validation.ts b/apps/settings-service/src/handlers/listConfig/validation.ts similarity index 100% rename from apps/settings-service/src/handlers/listConfig.ts/validation.ts rename to apps/settings-service/src/handlers/listConfig/validation.ts diff --git a/apps/settings-service/src/handlers/updateConfig/index.ts b/apps/settings-service/src/handlers/updateConfig/index.ts new file mode 100644 index 0000000000..a1ed789e59 --- /dev/null +++ b/apps/settings-service/src/handlers/updateConfig/index.ts @@ -0,0 +1,2 @@ +export { updateConfig } from "./updateConfig" +export { updateConfigValidation, updateConfigDescription } from "./validation" diff --git a/apps/settings-service/src/handlers/updateConfig/types.ts b/apps/settings-service/src/handlers/updateConfig/types.ts new file mode 100644 index 0000000000..1bbb59c205 --- /dev/null +++ b/apps/settings-service/src/handlers/updateConfig/types.ts @@ -0,0 +1,5 @@ +import type { z } from "zod" +import type { inputSchema, outputSchema } from "./validation" + +export type UpdateConfigInput = z.infer +export type UpdateConfigOutput = z.infer diff --git a/apps/settings-service/src/handlers/updateConfig/updateConfig.ts b/apps/settings-service/src/handlers/updateConfig/updateConfig.ts new file mode 100644 index 0000000000..971150275f --- /dev/null +++ b/apps/settings-service/src/handlers/updateConfig/updateConfig.ts @@ -0,0 +1,16 @@ +import { type Result, err, ok } from "neverthrow" +import { PermissionNotFoundError } from "../../errors" +import { getPermission, updatePermission } from "../../repositories/permissionsRepository" +import type { UpdateConfigInput } from "./types" + +export async function updateConfig(input: UpdateConfigInput): Promise> { + const permission = await getPermission(input.id) + + if (!permission) { + return err(new PermissionNotFoundError()) + } + + await updatePermission(input) + + return ok(undefined) +} diff --git a/apps/settings-service/src/handlers/updateConfig/validation.ts b/apps/settings-service/src/handlers/updateConfig/validation.ts new file mode 100644 index 0000000000..55d2625621 --- /dev/null +++ b/apps/settings-service/src/handlers/updateConfig/validation.ts @@ -0,0 +1,32 @@ +import { describeRoute } from "hono-openapi" +import { resolver } from "hono-openapi/zod" +import { validator as zv } from "hono-openapi/zod" +import { z } from "zod" +import { walletPermission } from "../../dtos" +import { isProduction } from "../../utils/isProduction" + +export const inputSchema: z.ZodDiscriminatedUnion<"type", [typeof walletPermission]> = z.discriminatedUnion("type", [ + walletPermission, +]) + +export const outputSchema = z.object({ + success: z.boolean(), + message: z.string().optional(), +}) + +export const updateConfigDescription = describeRoute({ + validateResponse: !isProduction, + description: "Update config", + responses: { + 200: { + description: "Config updated", + content: { + "application/json": { + schema: resolver(outputSchema), + }, + }, + }, + }, +}) + +export const updateConfigValidation = zv("json", inputSchema) diff --git a/apps/settings-service/src/repositories/permissionsRepository.ts b/apps/settings-service/src/repositories/permissionsRepository.ts index a851ec5d98..3c3b93e0e5 100644 --- a/apps/settings-service/src/repositories/permissionsRepository.ts +++ b/apps/settings-service/src/repositories/permissionsRepository.ts @@ -1,28 +1,64 @@ import type { Hex } from "@happy.tech/common" +import type { UUID } from "@happy.tech/common" +import type { Insertable, Selectable } from "kysely" import { db } from "../db/driver" -import type { WalletPermission } from "../db/types" +import type { WalletPermissionTable } from "../db/types" +import type { WalletPermission } from "../dtos" + +function fromDtoToDb(permission: WalletPermission): Insertable { + return { + user: permission.user, + invoker: permission.invoker, + parentCapability: permission.parentCapability, + caveats: JSON.stringify(permission.caveats), + date: permission.date, + id: permission.id, + updatedAt: permission.updatedAt, + deleted: permission.deleted, + } +} + +function fromDbToDto(permission: Selectable): WalletPermission { + return { + type: "WalletPermissions", + user: permission.user, + invoker: permission.invoker, + parentCapability: permission.parentCapability, + caveats: permission.caveats, + date: permission.date, + id: permission.id, + updatedAt: permission.updatedAt, + deleted: permission.deleted === 1, + } +} export function savePermission(permission: WalletPermission) { - return db - .insertInto("walletPermissions") - .values({ - user: permission.user, - invoker: permission.invoker, - parentCapability: permission.parentCapability, - caveats: JSON.stringify(permission.caveats), - date: permission.date, - id: permission.id, - updatedAt: Date.now(), - deleted: permission.deleted, - }) - .execute() + return db.insertInto("walletPermissions").values(fromDtoToDb(permission)).execute() +} + +export function getPermission(id: UUID) { + return db.selectFrom("walletPermissions").where("id", "=", id).selectAll().executeTakeFirst() } export async function listPermissions(user: Hex, lastUpdated?: number): Promise { - return await db + const result = await db .selectFrom("walletPermissions") .where("user", "=", user) .$if(lastUpdated !== undefined, (qb) => qb.where("updatedAt", ">", lastUpdated as number)) .selectAll() .execute() + + return result.map(fromDbToDto) +} + +export async function updatePermission(permission: WalletPermission) { + return await db + .updateTable("walletPermissions") + .set(fromDtoToDb(permission)) + .where("id", "=", permission.id) + .execute() +} + +export async function deletePermission(id: UUID) { + return await db.updateTable("walletPermissions").set({ deleted: true }).where("id", "=", id).execute() } diff --git a/apps/settings-service/src/server/configRoute.ts b/apps/settings-service/src/server/configRoute.ts index 76945c1cfa..c6c4674a6f 100644 --- a/apps/settings-service/src/server/configRoute.ts +++ b/apps/settings-service/src/server/configRoute.ts @@ -1,8 +1,11 @@ import { Hono } from "hono" import { createConfig } from "../handlers/createConfig/createConfig" import { createConfigDescription, createConfigValidation } from "../handlers/createConfig/validation" -import { listConfig } from "../handlers/listConfig.ts" -import { listConfigDescription, listConfigValidation } from "../handlers/listConfig.ts" +import { deleteConfig } from "../handlers/deleteConfig/deleteConfig" +import { deleteConfigDescription, deleteConfigValidation } from "../handlers/deleteConfig/validation" +import { listConfig, listConfigDescription, listConfigValidation } from "../handlers/listConfig" +import { updateConfig } from "../handlers/updateConfig/updateConfig" +import { updateConfigDescription, updateConfigValidation } from "../handlers/updateConfig/validation" import { makeResponse } from "./makeResponse" export default new Hono() @@ -20,3 +23,17 @@ export default new Hono() const [response, code] = makeResponse(output) return c.json(response, code) }) + .put("/update", updateConfigDescription, updateConfigValidation, async (c) => { + const input = c.req.valid("json") + const result = await updateConfig(input) + + const [response, code] = makeResponse(result) + return c.json(response, code) + }) + .delete("/delete/:id", deleteConfigDescription, deleteConfigValidation, async (c) => { + const input = c.req.valid("param") + const result = await deleteConfig(input) + + const [response, code] = makeResponse(result) + return c.json(response, code) + }) diff --git a/bun.lock b/bun.lock index 32479c2f85..768244f7f1 100644 --- a/bun.lock +++ b/bun.lock @@ -79,6 +79,7 @@ "@happy.tech/common": "workspace:*", "@happy.tech/contracts": "workspace:0.2.0", "@happy.tech/wallet-common": "workspace:*", + "@legendapp/state": "^3.0.0-beta.30", "@metamask/safe-event-emitter": "^3.1.1", "@phosphor-icons/react": "^2.1.10", "@tanstack/react-query": "^5.56.2", @@ -1021,6 +1022,8 @@ "@kevincharm/bls-bn254": ["@kevincharm/bls-bn254@2.0.0", "", { "peerDependencies": { "ethers": "^6.8.0", "mcl-wasm": "^1.4.0" } }, "sha512-Y6Jk8oE6Re4v3rDkb51mF3rHfDakxCTGptVr/LX2iwtbA7qu3DLNBNgdBU3heYFA8t22JiX3BgnMTbBgm3AZMA=="], + "@legendapp/state": ["@legendapp/state@3.0.0-beta.31", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "expo-sqlite": "^15.0.0" }, "optionalPeers": ["expo-sqlite"] }, "sha512-ejQHeBk3DEHOeF/j4/nX0W6uXiGQBOqQ5Ftfk10ReDpGzeC/GxF3Or31LG5qPcp3ZZweUBiVyqZqEtQefX4eKA=="], + "@lezer/common": ["@lezer/common@1.2.3", "", {}, "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA=="], "@lezer/css": ["@lezer/css@1.2.1", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-2F5tOqzKEKbCUNraIXc0f6HKeyKlmMWJnBB0i4XW6dJgssrZO/YlZ2pY5xgyqDleqqhiNJ3dQhbrV2aClZQMvg=="], @@ -4413,7 +4416,7 @@ "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], - "use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="], + "use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], "utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="], @@ -4699,8 +4702,6 @@ "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], - "@radix-ui/react-use-is-hydrated/use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], - "@reown/appkit/@walletconnect/types": ["@walletconnect/types@2.21.0", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "events": "3.3.0" } }, "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw=="], "@reown/appkit/@walletconnect/universal-provider": ["@walletconnect/universal-provider@2.21.0", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/jsonrpc-http-connection": "1.0.8", "@walletconnect/jsonrpc-provider": "1.0.14", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "@walletconnect/sign-client": "2.21.0", "@walletconnect/types": "2.21.0", "@walletconnect/utils": "2.21.0", "es-toolkit": "1.33.0", "events": "3.3.0" } }, "sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg=="], @@ -4801,8 +4802,6 @@ "@tailwindcss/vite/tailwindcss": ["tailwindcss@4.0.7", "", {}, "sha512-yH5bPPyapavo7L+547h3c4jcBXcrKwybQRjwdEIVAd9iXRvy/3T1CC6XSQEgZtRySjKfqvo3Cc0ZF1DTheuIdA=="], - "@tanstack/react-store/use-sync-external-store": ["use-sync-external-store@1.5.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A=="], - "@tanstack/router-generator/prettier": ["prettier@3.6.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A=="], "@tanstack/router-generator/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], @@ -5349,6 +5348,8 @@ "vocs/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], + "wagmi/use-sync-external-store": ["use-sync-external-store@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw=="], + "web3-eth-abi/abitype": ["abitype@0.7.1", "", { "peerDependencies": { "typescript": ">=4.9.4", "zod": "^3 >=3.19.1" }, "optionalPeers": ["zod"] }, "sha512-VBkRHTDZf9Myaek/dO3yMmOzB/y2s3Zo6nVU7yaw1G+TvCHAjwaJzNGN9yo4K5D8bU/VZXKP1EJpRhFr862PlQ=="], "web3-eth-accounts/ethereum-cryptography": ["ethereum-cryptography@2.2.1", "", { "dependencies": { "@noble/curves": "1.4.2", "@noble/hashes": "1.4.0", "@scure/bip32": "1.4.0", "@scure/bip39": "1.3.0" } }, "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg=="], From e60087a034c3d0fb30047a35e8ab2a6ac472cac1 Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Mon, 19 May 2025 17:21:44 +0200 Subject: [PATCH 03/19] chore(settings-service): wallet integration --- apps/iframe/src/state/permissions.ts | 128 ++++++++++++++---- .../db/migrations/Migration20250515123000.ts | 1 + apps/settings-service/src/db/types.ts | 1 + apps/settings-service/src/dtos.ts | 1 + .../src/repositories/permissionsRepository.ts | 4 +- 5 files changed, 104 insertions(+), 31 deletions(-) diff --git a/apps/iframe/src/state/permissions.ts b/apps/iframe/src/state/permissions.ts index 7e2786714f..a7d9a0f1dc 100644 --- a/apps/iframe/src/state/permissions.ts +++ b/apps/iframe/src/state/permissions.ts @@ -93,16 +93,20 @@ export type PermissionsRequest = string | PermissionRequestObject const permissionsMapLegend = observable(syncedCrud({ list: async ({ lastSync }) => { - const response = await fetch(`http://localhost:3000/api/v1/settings/list?user=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266${lastSync ? `&lastUpdated=${lastSync}` : ""}`) + const user = getUser() + if (!user) return [] + const response = await fetch(`http://localhost:3000/api/v1/settings/list?user=${user.address}${lastSync ? `&lastUpdated=${lastSync}` : ""}`) const data = await response.json() - - - + return data.data }, create: async (data: PermissionsMap) => { + console.log("create", data) const response = await fetch("http://localhost:3000/api/v1/settings/create", { method: "POST", + headers: { + "Content-Type": "application/json", + }, body: JSON.stringify(data), }) return await response.json() @@ -111,7 +115,10 @@ const permissionsMapLegend = observable(syncedCrud({ console.log("update", data) const response = await fetch("http://localhost:3000/api/v1/settings/update", { - method: "POST", + method: "PUT", + headers: { + "Content-Type": "application/json", + }, body: JSON.stringify(data), }) return await response.json() @@ -122,17 +129,58 @@ const permissionsMapLegend = observable(syncedCrud({ console.log("Refreshing config (5-second interval)"); refresh(); }, 5000); - - // Return cleanup function to clear the interval when unsubscribing - return () => { - clearInterval(intervalId); - }; + }, + delete: async ({id}) => { + console.log("delete", id) + const response = await fetch(`http://localhost:3000/api/v1/settings/delete/${id}`, { + method: "DELETE", + }) + return await response.json() }, persist: { plugin: ObservablePersistLocalStorage, name: 'config-legend', retrySync: true // Retry sync after reload }, + onSaved: ({input}: {input: WalletPermission}) => { + console.log("On saved", input) + const appPermissions = getAppPermissions(input.invoker) + console.log("App permissions", appPermissions) + + const oldPermission = appPermissions[input.parentCapability]; + + console.log("Old permission", oldPermission) + if (oldPermission) { + const differences = { + id: oldPermission.id !== input.id, + invoker: oldPermission.invoker !== input.invoker, + parentCapability: oldPermission.parentCapability !== input.parentCapability, + caveats: JSON.stringify(oldPermission.caveats) !== JSON.stringify(input.caveats), + date: oldPermission.date !== input.date, + }; + + const changedFields = Object.entries(differences) + .filter(([_, changed]) => changed) + .map(([field]) => field); + + console.log("Changed fields", changedFields) + if (changedFields.length > 0) { + console.log('Permission fields changed:', changedFields); + appPermissions[input.parentCapability] = input + setAppPermissions(input.invoker, appPermissions) + } else { + console.log("No changes to permission") + } + } else { + console.log("No old permission found") + console.log("Adding new permission") + appPermissions[input.parentCapability] = input + setAppPermissions(input.invoker, appPermissions) + } + + + }, + fieldCreatedAt: 'created_at', fieldUpdatedAt: 'updatedAt', fieldDeleted: 'deleted', changesSince: 'last-sync' @@ -260,21 +308,24 @@ function setAppPermissions(app: AppURL, appPermissions: AppPermissions): void { } }) - const id = createUUID() - - permissionsMapLegend[id].set({ - id, - type: "WalletPermissions", - user: user.address, - invoker: app, - parentCapability: appPermissions.eth_accounts.parentCapability, - caveats: appPermissions.eth_accounts.caveats, - date: appPermissions.eth_accounts.date, - deleted: false, - updatedAt: Date.now(), - }) + for (const permission of permissionArray ){ + permissionsMapLegend[permission.id].set({ + id: permission.id, + type: "WalletPermissions", + user: user.address, + invoker: app, + parentCapability: permission.parentCapability, + caveats: permission.caveats, + date: permission.date, + deleted: false, + updatedAt: Date.now(), + createdAt: Date.now(), + }) + } } + + // === CLEAR PERMISSIONS =========================================================================== /** @@ -283,16 +334,21 @@ function setAppPermissions(app: AppURL, appPermissions: AppPermissions): void { export function clearPermissions(): void { const user = getUser() if (!user) return + const permissions = store.get(permissionsMapAtom) store.set(permissionsMapAtom, (prev) => { const { [user.address]: _, ...rest } = prev return rest }) - - Object.values(permissionsMapLegend).forEach((p) => { - if (p.user === user.address) { - p.deleted = true - } - }) + for (const permission of Object.values(permissions)) { + const permissionsPerUser = Object.values(permission) + + for (const p of permissionsPerUser) { + const permissionsToDelete = Object.values(p) + for (const p of permissionsToDelete) { + permissionsMapLegend[p.id].delete() + } + } + } } @@ -309,6 +365,8 @@ export function clearAppPermissions(app: AppURL): void { .flatMap((p) => p.caveats) .forEach((c) => revokedSessionKeys.add(c.value as Address)) + const permissions = store.get(permissionsMapAtom) + // Remove app permissions from storage store.set(permissionsMapAtom, (prev) => { const { @@ -317,8 +375,18 @@ export function clearAppPermissions(app: AppURL): void { } = prev return { ...otherUsers, [user.address]: otherApps } }) -} + for (const permission of Object.values(permissions)) { + const permissionsPerUser = Object.values(permission) + + for (const p of permissionsPerUser) { + const permissionsToDelete = Object.values(p) + for (const p of permissionsToDelete) { + permissionsMapLegend[p.id].delete() + } + } + } +} type PermissionRequestEntry = { name: string caveats: WalletPermissionCaveat[] diff --git a/apps/settings-service/src/db/migrations/Migration20250515123000.ts b/apps/settings-service/src/db/migrations/Migration20250515123000.ts index acf5e76e14..88e8c46be6 100644 --- a/apps/settings-service/src/db/migrations/Migration20250515123000.ts +++ b/apps/settings-service/src/db/migrations/Migration20250515123000.ts @@ -11,6 +11,7 @@ export async function up(db: Kysely) { .addColumn("date", "integer") .addColumn("id", "text", (col) => col.notNull()) .addColumn("updatedAt", "integer") + .addColumn("createdAt", "integer") .addColumn("deleted", "boolean") .execute() } diff --git a/apps/settings-service/src/db/types.ts b/apps/settings-service/src/db/types.ts index 2552d10501..e0b619c726 100644 --- a/apps/settings-service/src/db/types.ts +++ b/apps/settings-service/src/db/types.ts @@ -29,6 +29,7 @@ export type WalletPermissionTable = { // Not in the EIP, but Viem wants this. id: UUID updatedAt: number + createdAt: number deleted: ColumnType } diff --git a/apps/settings-service/src/dtos.ts b/apps/settings-service/src/dtos.ts index 59beceefee..a575b9bc8b 100644 --- a/apps/settings-service/src/dtos.ts +++ b/apps/settings-service/src/dtos.ts @@ -24,6 +24,7 @@ export const walletPermission = z.object({ date: z.number().openapi({ example: 1715702400 }), id: z.string().refine(isUUID).openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), updatedAt: z.number().openapi({ example: 1715702400 }), + createdAt: z.number().openapi({ example: 1715702400 }), deleted: z.boolean().openapi({ example: false }), }) diff --git a/apps/settings-service/src/repositories/permissionsRepository.ts b/apps/settings-service/src/repositories/permissionsRepository.ts index 3c3b93e0e5..b9c492d5d1 100644 --- a/apps/settings-service/src/repositories/permissionsRepository.ts +++ b/apps/settings-service/src/repositories/permissionsRepository.ts @@ -14,6 +14,7 @@ function fromDtoToDb(permission: WalletPermission): Insertable): WalletPermi date: permission.date, id: permission.id, updatedAt: permission.updatedAt, + createdAt: permission.createdAt, deleted: permission.deleted === 1, } } @@ -60,5 +62,5 @@ export async function updatePermission(permission: WalletPermission) { } export async function deletePermission(id: UUID) { - return await db.updateTable("walletPermissions").set({ deleted: true }).where("id", "=", id).execute() + return await db.updateTable("walletPermissions").set({ deleted: true, updatedAt: Date.now() }).where("id", "=", id).execute() } From 90745c20ccd89a4b2b741029f1490484c2d28e5f Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Tue, 20 May 2025 13:15:29 +0200 Subject: [PATCH 04/19] chore(settings-service): replace jotai atom for legend state in permission state --- .../permissions/useAppsWithPermissions.ts | 28 ++- apps/iframe/src/hooks/useHasPermissions.ts | 14 +- apps/iframe/src/listeners/atoms.ts | 4 +- apps/iframe/src/state/permissions.ts | 230 +++++++----------- 4 files changed, 110 insertions(+), 166 deletions(-) diff --git a/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts b/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts index 9ca7231d4b..24e896a575 100644 --- a/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts +++ b/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts @@ -1,16 +1,22 @@ -import { entries } from "@happy.tech/common" -import { useAtomValue } from "jotai" -import { useAccount } from "wagmi" -import { type AppPermissions, permissionsMapAtom } from "#src/state/permissions" +import { type AppPermissions, permissionsMapLegend } from "#src/state/permissions" import { type AppURL, isWallet } from "#src/utils/appURL" +import { use$ } from "@legendapp/state/react" export function useAppsWithPermissions(): [AppURL, AppPermissions][] { - const permissionsMap = useAtomValue(permissionsMapAtom) - const account = useAccount() + const appsWithPermissions = () => { + const permissions = permissionsMapLegend.get() + return Object.values(permissions).filter((permission) => !isWallet(permission.invoker)).reduce((acc, permission) => { + const existing = acc.find(([app]) => app === permission.invoker) + if (existing) { + existing[1][permission.parentCapability] = permission + } else { + acc.push([permission.invoker, { + [permission.parentCapability]: permission + }]) + } + return acc + }, [] as [AppURL, AppPermissions][]) + } - // TODO: the default here should include the wallet app, but currently its empty - // adding a permission to an unrelated app will cause the wallet to _also_ be - // granted the default permissions and will then show up here - return entries(permissionsMap[account?.address ?? "0x0"] ?? {}) // - .filter(([app]) => !isWallet(app)) + return use$(() => appsWithPermissions()) } diff --git a/apps/iframe/src/hooks/useHasPermissions.ts b/apps/iframe/src/hooks/useHasPermissions.ts index 9c33a9affc..4660529562 100644 --- a/apps/iframe/src/hooks/useHasPermissions.ts +++ b/apps/iframe/src/hooks/useHasPermissions.ts @@ -1,13 +1,7 @@ -import { useAtomValue } from "jotai" -import { useMemo } from "react" -import { type PermissionsRequest, atomForPermissionsCheck } from "../state/permissions" +import { type PermissionsRequest, hasPermissions } from "../state/permissions" import { type AppURL, getAppURL } from "../utils/appURL" +import { use$ } from "@legendapp/state/react" export function useHasPermissions(permissionsRequest: PermissionsRequest, app: AppURL = getAppURL()) { - // This must be memoized to avoid an infinite render loop. - const permissionsAtom = useMemo( - () => atomForPermissionsCheck(permissionsRequest, app), // - [permissionsRequest, app], - ) - return useAtomValue(permissionsAtom) -} + return use$(() => hasPermissions(app, permissionsRequest)) +} \ No newline at end of file diff --git a/apps/iframe/src/listeners/atoms.ts b/apps/iframe/src/listeners/atoms.ts index 6197b8a507..2e43b085a9 100644 --- a/apps/iframe/src/listeners/atoms.ts +++ b/apps/iframe/src/listeners/atoms.ts @@ -2,7 +2,7 @@ import { Msgs } from "@happy.tech/wallet-common" import { getDefaultStore } from "jotai/vanilla" import { http, createPublicClient } from "viem" import { mainnet } from "viem/chains" -import { permissionsMapAtom } from "#src/state/permissions" +import { permissionsMapLegend } from "#src/state/permissions.ts" import { appMessageBus } from "../services/eventBus" import { authStateAtom } from "../state/authState" import { userAtom } from "../state/user" @@ -64,7 +64,7 @@ if (store.get(userAtom)) emitUserUpdate(store.get(userAtom)) * @emits {@link Msgs.UserChanged} (optional) * @emits {@link Msgs.ProviderEvent} (optional) */ -store.sub(permissionsMapAtom, () => { +permissionsMapLegend.onChange(() => { emitUserUpdate(store.get(userAtom)) }) diff --git a/apps/iframe/src/state/permissions.ts b/apps/iframe/src/state/permissions.ts index a7d9a0f1dc..30bda4f501 100644 --- a/apps/iframe/src/state/permissions.ts +++ b/apps/iframe/src/state/permissions.ts @@ -1,22 +1,17 @@ import { createUUID } from "@happy.tech/common" import type { Address, UUID } from "@happy.tech/common" -import type { HappyUser } from "@happy.tech/wallet-common" -import { type Atom, atom, getDefaultStore } from "jotai" -import { atomFamily, atomWithStorage, createJSONStorage } from "jotai/utils" -import { PermissionName } from "#src/constants/permissions" import { permissionsLogger } from "#src/utils/logger" -import { StorageKey } from "../services/storage" -import { type AppURL, getAppURL, getWalletURL, isApp, isStandaloneWallet } from "../utils/appURL" +import { type AppURL, getWalletURL, isApp, isStandaloneWallet } from "../utils/appURL" import { checkIfCaveatsMatch } from "../utils/checkIfCaveatsMatch" import { emitUserUpdate } from "../utils/emitUserUpdate" import { revokedSessionKeys } from "./interfaceState" -import { getUser, userAtom } from "./user" +import { getUser } from "./user" import { syncedCrud } from '@legendapp/state/sync-plugins/crud' import { observable } from "@legendapp/state" import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage"; +import type { HappyUser } from "@happy.tech/wallet-common" +import { PermissionName } from "#src/constants/permissions.ts" -// STORE INSTANTIATION -const store = getDefaultStore() // In EIP-2255, permissions define whether an app can make certain EIP-1193 requests to the wallets. // These permissions are scoped per app and per account. @@ -52,6 +47,9 @@ export type AppPermissions = Record * This type is copied from Viem (eip1193.ts) */ export type WalletPermission = { + type: "WalletPermissions" + // The user to which the permission is granted. + user: Address // The app to which the permission is granted. invoker: AppURL // This is the EIP-1193 request that this permission is mapped to. @@ -60,6 +58,9 @@ export type WalletPermission = { date: number // Not in the EIP, but Viem wants this. id: UUID + deleted: boolean + updatedAt: number + createdAt: number } /** @@ -90,17 +91,23 @@ export type SessionKeyRequest = { */ export type PermissionsRequest = string | PermissionRequestObject +type PermissionCheckParams = { + permissionsRequest: PermissionsRequest + app: AppURL +} + -const permissionsMapLegend = observable(syncedCrud({ + +export const permissionsMapLegend = observable(syncedCrud({ list: async ({ lastSync }) => { const user = getUser() if (!user) return [] const response = await fetch(`http://localhost:3000/api/v1/settings/list?user=${user.address}${lastSync ? `&lastUpdated=${lastSync}` : ""}`) const data = await response.json() - return data.data + return data.data as WalletPermission[] }, - create: async (data: PermissionsMap) => { + create: async (data: WalletPermission) => { console.log("create", data) const response = await fetch("http://localhost:3000/api/v1/settings/create", { method: "POST", @@ -109,9 +116,9 @@ const permissionsMapLegend = observable(syncedCrud({ }, body: JSON.stringify(data), }) - return await response.json() + await response.json() }, - update: async (data: PermissionsMap) => { + update: async (data: WalletPermission) => { console.log("update", data) const response = await fetch("http://localhost:3000/api/v1/settings/update", { @@ -121,7 +128,7 @@ const permissionsMapLegend = observable(syncedCrud({ }, body: JSON.stringify(data), }) - return await response.json() + await response.json() }, subscribe: ({ refresh }) => { // Set up an interval to refresh messages every 5 seconds @@ -135,7 +142,7 @@ const permissionsMapLegend = observable(syncedCrud({ const response = await fetch(`http://localhost:3000/api/v1/settings/delete/${id}`, { method: "DELETE", }) - return await response.json() + await response.json() }, persist: { plugin: ObservablePersistLocalStorage, @@ -180,50 +187,13 @@ const permissionsMapLegend = observable(syncedCrud({ }, + initial: {}, fieldCreatedAt: 'created_at', fieldUpdatedAt: 'updatedAt', fieldDeleted: 'deleted', changesSince: 'last-sync' })) -/** - * Maps an user + app pair to a {@link AppPermissions}, which is the set of permissions - * for that user on that app. - */ -export const permissionsMapAtom = atomWithStorage(StorageKey.UserPermissions, {}, createJSONStorage(), { - getOnInit: true, -}) - -type PermissionCheckParams = { - permissionsRequest: PermissionsRequest - app: AppURL -} - -const _atomForPermissionsCheck: (params: PermissionCheckParams) => Atom = // - atomFamily(({ permissionsRequest, app }) => { - return atom((get) => { - const user = get(userAtom) - if (!user) return false - // This call *might* be required to record the dependency, which occurs via - // `getDefaultStore().get` during `hasPermissions`. - get(permissionsMapAtom) - return hasPermissions(app, permissionsRequest) - }) - }) - -/** - * A function that returns a new atom that subscribes to a check on the specified permissions. - * - * The atom is cached, but not automatically garbage-collected. If this is called with a changing - * set of permissions, it is necessary to call `atomForPermissionsCheck.remove(oldPermissions)` - * when changing the permissions! - */ -export function atomForPermissionsCheck( - permissionsRequest: PermissionsRequest, // - app: AppURL = getAppURL(), -): Atom { - return _atomForPermissionsCheck({ permissionsRequest, app }) -} // === GET ALL PERMISSIONS ======================================================================================= @@ -232,45 +202,52 @@ export function atomForPermissionsCheck( */ export function getAppPermissions(app: AppURL): AppPermissions { const user = getUser() - const permissionsMap = store.get(permissionsMapAtom) - return getAppPermissionsPure(user, app, permissionsMap) + const permissionsMap = permissionsMapLegend.get() + return getAppPermissionsPure(user, app, Object.values(permissionsMap)) } export function getAppPermissionsPure( user: HappyUser | undefined, app: AppURL, - permissionsMap: PermissionsMap, + permissions: WalletPermission[], ): AppPermissions { if (!user) { // This should never happen and requires investigating if it does! permissionsLogger.warn("No user found, returning empty permissions.") return {} } - const appPermissions = permissionsMap[user.address]?.[app] - if (appPermissions) return appPermissions - - // Permissions don't exist, create them. - - const baseAppPermissions: AppPermissions = - app === getWalletURL() - ? { - // The iframe is always granted the `eth_accounts` permission. - eth_accounts: { - invoker: app, - parentCapability: "eth_accounts", - caveats: [], - date: Date.now(), - id: createUUID(), - }, - } - : {} - - // It's not required to set the permissionsAtom here because the permissions don't actually - // change (so nothing dependent on the atom needs to update). We just write them to avoid - // rerunning the above logic on each lookup. - permissionsMap[user.address] ??= {} - permissionsMap[user.address][app] = baseAppPermissions - - return baseAppPermissions + + const appPermissions = permissions.filter((p) => p.invoker === app && p.user === user.address) + + if (appPermissions.length > 0) { + return appPermissions.reduce((acc, p) => { + acc[p.parentCapability] = p + return acc + }, {} as AppPermissions) + } + + if (app === getWalletURL()) { + // Permissions don't exist, create them. + // The iframe is always granted the `eth_accounts` permission. + const permissionId = createUUID() + const permission: WalletPermission = { + type: "WalletPermissions", + user: user.address, + invoker: app, + parentCapability: "eth_accounts", + caveats: [], + date: Date.now(), + id: permissionId, + deleted: false, + updatedAt: Date.now(), + createdAt: Date.now(), + } + permissionsMapLegend[permissionId].set(permission) + return { + eth_accounts: permission, + } + } + + return {} } // === WRITE ALL PERMISSIONS ======================================================================================= @@ -294,34 +271,16 @@ function setAppPermissions(app: AppURL, appPermissions: AppPermissions): void { return } - store.set(permissionsMapAtom, (prev: PermissionsMap) => { - if (!permissionArray.every((a) => a.invoker === app)) { - // No all permissions supplied are scoped to the app. - // This should never happen! - console.warn("Invalid permission update requested, not setting permissions.") - return prev - } - - return { - ...prev, - [user.address]: { ...prev[user.address], [app]: appPermissions }, - } - }) + if (!permissionArray.every((a) => a.invoker === app)) { + // No all permissions supplied are scoped to the app. + // This should never happen! + console.warn("Invalid permission update requested, not setting permissions.") + return + } - for (const permission of permissionArray ){ - permissionsMapLegend[permission.id].set({ - id: permission.id, - type: "WalletPermissions", - user: user.address, - invoker: app, - parentCapability: permission.parentCapability, - caveats: permission.caveats, - date: permission.date, - deleted: false, - updatedAt: Date.now(), - createdAt: Date.now(), - }) - } + for (const permission of permissionArray) { + permissionsMapLegend[permission.id].set(permission) + } } @@ -334,22 +293,13 @@ function setAppPermissions(app: AppURL, appPermissions: AppPermissions): void { export function clearPermissions(): void { const user = getUser() if (!user) return - const permissions = store.get(permissionsMapAtom) - store.set(permissionsMapAtom, (prev) => { - const { [user.address]: _, ...rest } = prev - return rest - }) + + const permissions = permissionsMapLegend.get() for (const permission of Object.values(permissions)) { - const permissionsPerUser = Object.values(permission) - - for (const p of permissionsPerUser) { - const permissionsToDelete = Object.values(p) - for (const p of permissionsToDelete) { - permissionsMapLegend[p.id].delete() - } - } + if (permission.user === user.address) { + permissionsMapLegend[permission.id].delete() + } } - } /** @@ -365,25 +315,11 @@ export function clearAppPermissions(app: AppURL): void { .flatMap((p) => p.caveats) .forEach((c) => revokedSessionKeys.add(c.value as Address)) - const permissions = store.get(permissionsMapAtom) - - // Remove app permissions from storage - store.set(permissionsMapAtom, (prev) => { - const { - [user.address]: { [app]: _, ...otherApps }, - ...otherUsers - } = prev - return { ...otherUsers, [user.address]: otherApps } - }) + const permissions = permissionsMapLegend.get() for (const permission of Object.values(permissions)) { - const permissionsPerUser = Object.values(permission) - - for (const p of permissionsPerUser) { - const permissionsToDelete = Object.values(p) - for (const p of permissionsToDelete) { - permissionsMapLegend[p.id].delete() - } + if (permission.invoker === app && permission.user === user.address) { + permissionsMapLegend[permission.id].delete() } } } @@ -432,9 +368,12 @@ export function permissionRequestEntries(permissions: PermissionsRequest): Permi * ``` */ export function grantPermissions(app: AppURL, permissionRequest: PermissionsRequest): WalletPermission[] { - const grantedPermissions = [] + const grantedPermissions: WalletPermission[] = [] const appPermissions = getAppPermissions(app) + const user = getUser() + if (!user) return [] + for (const { name, caveats: newCaveats } of permissionRequestEntries(permissionRequest)) { // If permission exists, merge new caveats with existing ones if (appPermissions[name]) { @@ -451,12 +390,17 @@ export function grantPermissions(app: AppURL, permissionRequest: PermissionsRequ grantedPermissions.push(appPermissions[name]) } else { - const grantedPermission = { + const grantedPermission: WalletPermission = { caveats: newCaveats, invoker: app, parentCapability: name, date: Date.now(), id: createUUID(), + deleted: false, + updatedAt: Date.now(), + createdAt: Date.now(), + type: "WalletPermissions", + user: user.address, } grantedPermissions.push(grantedPermission) From 7322108af12f5aff9306185d92f3931e64f30a0a Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Tue, 20 May 2025 16:22:04 +0200 Subject: [PATCH 05/19] chore(settings): when set, clean all the previous elements --- apps/iframe/src/state/permissions.ts | 55 ++++------------------------ 1 file changed, 8 insertions(+), 47 deletions(-) diff --git a/apps/iframe/src/state/permissions.ts b/apps/iframe/src/state/permissions.ts index 30bda4f501..038db77e4b 100644 --- a/apps/iframe/src/state/permissions.ts +++ b/apps/iframe/src/state/permissions.ts @@ -108,7 +108,6 @@ export const permissionsMapLegend = observable(syncedCrud({ return data.data as WalletPermission[] }, create: async (data: WalletPermission) => { - console.log("create", data) const response = await fetch("http://localhost:3000/api/v1/settings/create", { method: "POST", headers: { @@ -119,8 +118,6 @@ export const permissionsMapLegend = observable(syncedCrud({ await response.json() }, update: async (data: WalletPermission) => { - console.log("update", data) - const response = await fetch("http://localhost:3000/api/v1/settings/update", { method: "PUT", headers: { @@ -138,7 +135,6 @@ export const permissionsMapLegend = observable(syncedCrud({ }, 5000); }, delete: async ({id}) => { - console.log("delete", id) const response = await fetch(`http://localhost:3000/api/v1/settings/delete/${id}`, { method: "DELETE", }) @@ -148,44 +144,6 @@ export const permissionsMapLegend = observable(syncedCrud({ plugin: ObservablePersistLocalStorage, name: 'config-legend', retrySync: true // Retry sync after reload - }, - onSaved: ({input}: {input: WalletPermission}) => { - console.log("On saved", input) - const appPermissions = getAppPermissions(input.invoker) - console.log("App permissions", appPermissions) - - const oldPermission = appPermissions[input.parentCapability]; - - console.log("Old permission", oldPermission) - if (oldPermission) { - const differences = { - id: oldPermission.id !== input.id, - invoker: oldPermission.invoker !== input.invoker, - parentCapability: oldPermission.parentCapability !== input.parentCapability, - caveats: JSON.stringify(oldPermission.caveats) !== JSON.stringify(input.caveats), - date: oldPermission.date !== input.date, - }; - - const changedFields = Object.entries(differences) - .filter(([_, changed]) => changed) - .map(([field]) => field); - - console.log("Changed fields", changedFields) - if (changedFields.length > 0) { - console.log('Permission fields changed:', changedFields); - appPermissions[input.parentCapability] = input - setAppPermissions(input.invoker, appPermissions) - } else { - console.log("No changes to permission") - } - } else { - console.log("No old permission found") - console.log("Adding new permission") - appPermissions[input.parentCapability] = input - setAppPermissions(input.invoker, appPermissions) - } - - }, initial: {}, fieldCreatedAt: 'created_at', @@ -219,12 +177,12 @@ export function getAppPermissionsPure( const appPermissions = permissions.filter((p) => p.invoker === app && p.user === user.address) if (appPermissions.length > 0) { - return appPermissions.reduce((acc, p) => { + const appPermissionsObject = appPermissions.reduce((acc, p) => { acc[p.parentCapability] = p return acc }, {} as AppPermissions) + return appPermissionsObject } - if (app === getWalletURL()) { // Permissions don't exist, create them. // The iframe is always granted the `eth_accounts` permission. @@ -265,7 +223,6 @@ function setAppPermissions(app: AppURL, appPermissions: AppPermissions): void { } const permissionArray = Object.values(appPermissions) - if (!permissionArray.length) { clearAppPermissions(app) return @@ -278,6 +235,12 @@ function setAppPermissions(app: AppURL, appPermissions: AppPermissions): void { return } + const currentPermissions = getAppPermissions(app) + + for (const permission of Object.values(currentPermissions)) { + permissionsMapLegend[permission.id].delete() + } + for (const permission of permissionArray) { permissionsMapLegend[permission.id].set(permission) } @@ -308,7 +271,6 @@ export function clearPermissions(): void { export function clearAppPermissions(app: AppURL): void { const user = getUser() if (!user) return - // Register session keys for onchain deregistrations. Object.values(getAppPermissions(app)) .filter((p: WalletPermission) => p.parentCapability === PermissionName.SessionKey) @@ -453,7 +415,6 @@ export function grantPermissions(app: AppURL, permissionRequest: PermissionsRequ */ export function revokePermissions(app: AppURL, permissionsRequest: PermissionsRequest): void { const appPermissions = getAppPermissions(app) - for (const { name, caveats } of permissionRequestEntries(permissionsRequest)) { // Permission is not granted, nothing to do. if (!appPermissions[name]) continue From 915da759b9ad1de24e7fef46a4c6c5fee621345f Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Wed, 21 May 2025 23:28:28 +0200 Subject: [PATCH 06/19] fix(sync-service): fix conditions races --- .../permissions/useAppsWithPermissions.ts | 33 ++-- apps/iframe/src/hooks/useHasPermissions.ts | 4 +- apps/iframe/src/state/permissions.ts | 148 +++++++++--------- apps/settings-service/src/db/types.ts | 3 +- apps/settings-service/src/dtos.ts | 3 +- apps/settings-service/src/errors.ts | 6 + .../src/handlers/createConfig/createConfig.ts | 11 +- .../src/handlers/deleteConfig/validation.ts | 4 +- .../src/repositories/permissionsRepository.ts | 8 +- .../src/server/configRoute.ts | 4 +- 10 files changed, 122 insertions(+), 102 deletions(-) diff --git a/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts b/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts index 24e896a575..4f8cebcf41 100644 --- a/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts +++ b/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts @@ -1,21 +1,30 @@ + +import { use$ } from "@legendapp/state/react" import { type AppPermissions, permissionsMapLegend } from "#src/state/permissions" import { type AppURL, isWallet } from "#src/utils/appURL" -import { use$ } from "@legendapp/state/react" export function useAppsWithPermissions(): [AppURL, AppPermissions][] { const appsWithPermissions = () => { const permissions = permissionsMapLegend.get() - return Object.values(permissions).filter((permission) => !isWallet(permission.invoker)).reduce((acc, permission) => { - const existing = acc.find(([app]) => app === permission.invoker) - if (existing) { - existing[1][permission.parentCapability] = permission - } else { - acc.push([permission.invoker, { - [permission.parentCapability]: permission - }]) - } - return acc - }, [] as [AppURL, AppPermissions][]) + return Object.values(permissions) + .filter((permission) => !isWallet(permission.invoker)) + .reduce( + (acc, permission) => { + const existing = acc.find(([app]) => app === permission.invoker) + if (existing) { + existing[1][permission.parentCapability] = permission + } else { + acc.push([ + permission.invoker, + { + [permission.parentCapability]: permission, + }, + ]) + } + return acc + }, + [] as [AppURL, AppPermissions][], + ) } return use$(() => appsWithPermissions()) diff --git a/apps/iframe/src/hooks/useHasPermissions.ts b/apps/iframe/src/hooks/useHasPermissions.ts index 4660529562..3bc8dadc95 100644 --- a/apps/iframe/src/hooks/useHasPermissions.ts +++ b/apps/iframe/src/hooks/useHasPermissions.ts @@ -1,7 +1,7 @@ +import { use$ } from "@legendapp/state/react" import { type PermissionsRequest, hasPermissions } from "../state/permissions" import { type AppURL, getAppURL } from "../utils/appURL" -import { use$ } from "@legendapp/state/react" export function useHasPermissions(permissionsRequest: PermissionsRequest, app: AppURL = getAppURL()) { return use$(() => hasPermissions(app, permissionsRequest)) -} \ No newline at end of file +} diff --git a/apps/iframe/src/state/permissions.ts b/apps/iframe/src/state/permissions.ts index 038db77e4b..653643045b 100644 --- a/apps/iframe/src/state/permissions.ts +++ b/apps/iframe/src/state/permissions.ts @@ -1,17 +1,15 @@ -import { createUUID } from "@happy.tech/common" -import type { Address, UUID } from "@happy.tech/common" +import type { Address } from "@happy.tech/common" import { permissionsLogger } from "#src/utils/logger" +import { observable } from "@legendapp/state" +import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage" +import { syncedCrud } from "@legendapp/state/sync-plugins/crud" +import { PermissionName } from "#src/constants/permissions" import { type AppURL, getWalletURL, isApp, isStandaloneWallet } from "../utils/appURL" import { checkIfCaveatsMatch } from "../utils/checkIfCaveatsMatch" import { emitUserUpdate } from "../utils/emitUserUpdate" import { revokedSessionKeys } from "./interfaceState" import { getUser } from "./user" -import { syncedCrud } from '@legendapp/state/sync-plugins/crud' -import { observable } from "@legendapp/state" -import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage"; import type { HappyUser } from "@happy.tech/wallet-common" -import { PermissionName } from "#src/constants/permissions.ts" - // In EIP-2255, permissions define whether an app can make certain EIP-1193 requests to the wallets. // These permissions are scoped per app and per account. @@ -57,10 +55,10 @@ export type WalletPermission = { caveats: WalletPermissionCaveat[] date: number // Not in the EIP, but Viem wants this. - id: UUID - deleted: boolean + id: string updatedAt: number createdAt: number + deleted: boolean } /** @@ -96,62 +94,65 @@ type PermissionCheckParams = { app: AppURL } - - -export const permissionsMapLegend = observable(syncedCrud({ - list: async ({ lastSync }) => { - const user = getUser() - if (!user) return [] - const response = await fetch(`http://localhost:3000/api/v1/settings/list?user=${user.address}${lastSync ? `&lastUpdated=${lastSync}` : ""}`) - const data = await response.json() - - return data.data as WalletPermission[] - }, - create: async (data: WalletPermission) => { - const response = await fetch("http://localhost:3000/api/v1/settings/create", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }) - await response.json() - }, - update: async (data: WalletPermission) => { - const response = await fetch("http://localhost:3000/api/v1/settings/update", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }) - await response.json() - }, - subscribe: ({ refresh }) => { - // Set up an interval to refresh messages every 5 seconds - const intervalId = setInterval(() => { - console.log("Refreshing config (5-second interval)"); - refresh(); - }, 5000); - }, - delete: async ({id}) => { - const response = await fetch(`http://localhost:3000/api/v1/settings/delete/${id}`, { - method: "DELETE", - }) - await response.json() - }, - persist: { - plugin: ObservablePersistLocalStorage, - name: 'config-legend', - retrySync: true // Retry sync after reload - }, - initial: {}, - fieldCreatedAt: 'created_at', - fieldUpdatedAt: 'updatedAt', - fieldDeleted: 'deleted', - changesSince: 'last-sync' -})) - +export const permissionsMapLegend = observable( + syncedCrud({ + list: async ({ lastSync }) => { + const user = getUser() + if (!user) return [] + const response = await fetch(`http://localhost:3000/api/v1/settings/list?user=${user.address}`) + const data = await response.json() + + return data.data as WalletPermission[] + }, + create: async (data: WalletPermission) => { + const response = await fetch("http://localhost:3000/api/v1/settings/create", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }) + await response.json() + }, + update: async (data: WalletPermission) => { + const response = await fetch("http://localhost:3000/api/v1/settings/update", { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }) + await response.json() + }, + subscribe: ({ refresh }) => { + // Set up an interval to refresh messages every 5 seconds + const intervalId = setInterval(() => { + console.log("Refreshing config (5-second interval)") + refresh() + }, 5000) + }, + delete: async ({ id }) => { + const response = await fetch("http://localhost:3000/api/v1/settings/delete", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ id }), + }) + await response.json() + }, + persist: { + plugin: ObservablePersistLocalStorage, + name: "config-legend", + retrySync: true, // Retry sync after reload + }, + initial: {}, + fieldCreatedAt: "created_at", + fieldUpdatedAt: "updatedAt", + fieldDeleted: "deleted", + changesSince: "all", + }), +) // === GET ALL PERMISSIONS ======================================================================================= @@ -186,7 +187,7 @@ export function getAppPermissionsPure( if (app === getWalletURL()) { // Permissions don't exist, create them. // The iframe is always granted the `eth_accounts` permission. - const permissionId = createUUID() + const permissionId = `${user.address}-${app}-eth_accounts` const permission: WalletPermission = { type: "WalletPermissions", user: user.address, @@ -195,9 +196,9 @@ export function getAppPermissionsPure( caveats: [], date: Date.now(), id: permissionId, - deleted: false, updatedAt: Date.now(), createdAt: Date.now(), + deleted: false, } permissionsMapLegend[permissionId].set(permission) return { @@ -235,10 +236,12 @@ function setAppPermissions(app: AppURL, appPermissions: AppPermissions): void { return } - const currentPermissions = getAppPermissions(app) + const currentPermissions = getAppPermissions(app) for (const permission of Object.values(currentPermissions)) { - permissionsMapLegend[permission.id].delete() + if (!permissionArray.some((p) => p.id === permission.id)) { + permissionsMapLegend[permission.id].delete() + } } for (const permission of permissionArray) { @@ -246,8 +249,6 @@ function setAppPermissions(app: AppURL, appPermissions: AppPermissions): void { } } - - // === CLEAR PERMISSIONS =========================================================================== /** @@ -256,7 +257,7 @@ function setAppPermissions(app: AppURL, appPermissions: AppPermissions): void { export function clearPermissions(): void { const user = getUser() if (!user) return - + const permissions = permissionsMapLegend.get() for (const permission of Object.values(permissions)) { if (permission.user === user.address) { @@ -352,12 +353,13 @@ export function grantPermissions(app: AppURL, permissionRequest: PermissionsRequ grantedPermissions.push(appPermissions[name]) } else { + const id = `${user.address}-${app}-${name}` const grantedPermission: WalletPermission = { caveats: newCaveats, invoker: app, parentCapability: name, date: Date.now(), - id: createUUID(), + id, deleted: false, updatedAt: Date.now(), createdAt: Date.now(), diff --git a/apps/settings-service/src/db/types.ts b/apps/settings-service/src/db/types.ts index e0b619c726..038da2d122 100644 --- a/apps/settings-service/src/db/types.ts +++ b/apps/settings-service/src/db/types.ts @@ -27,10 +27,9 @@ export type WalletPermissionTable = { caveats: ColumnType date: number // Not in the EIP, but Viem wants this. - id: UUID + id: string updatedAt: number createdAt: number - deleted: ColumnType } export interface Database { diff --git a/apps/settings-service/src/dtos.ts b/apps/settings-service/src/dtos.ts index a575b9bc8b..2368c4e3ad 100644 --- a/apps/settings-service/src/dtos.ts +++ b/apps/settings-service/src/dtos.ts @@ -22,10 +22,9 @@ export const walletPermission = z.object({ }), ), date: z.number().openapi({ example: 1715702400 }), - id: z.string().refine(isUUID).openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), + id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), updatedAt: z.number().openapi({ example: 1715702400 }), createdAt: z.number().openapi({ example: 1715702400 }), - deleted: z.boolean().openapi({ example: false }), }) export type WalletPermission = z.infer diff --git a/apps/settings-service/src/errors.ts b/apps/settings-service/src/errors.ts index e4d8528478..a0f11763aa 100644 --- a/apps/settings-service/src/errors.ts +++ b/apps/settings-service/src/errors.ts @@ -15,3 +15,9 @@ export class PermissionNotFoundError extends HappySettingsError { super(404, message || "Permission not found", options) } } + +export class PermissionAlreadyExistsError extends HappySettingsError { + constructor(message?: string, options?: ErrorOptions) { + super(409, message || "Permission already exists", options) + } +} diff --git a/apps/settings-service/src/handlers/createConfig/createConfig.ts b/apps/settings-service/src/handlers/createConfig/createConfig.ts index d8358e47b1..fe63fb1707 100644 --- a/apps/settings-service/src/handlers/createConfig/createConfig.ts +++ b/apps/settings-service/src/handlers/createConfig/createConfig.ts @@ -1,11 +1,18 @@ -import { type Result, ok } from "neverthrow" -import { savePermission } from "../../repositories/permissionsRepository" +import { type Result, err, ok } from "neverthrow" +import { PermissionAlreadyExistsError } from "../../errors" +import { getPermission, savePermission } from "../../repositories/permissionsRepository" import type { CreateConfigInput } from "./types" export async function createConfig(input: CreateConfigInput): Promise> { console.log(input) if (input.type === "WalletPermissions") { + const permission = await getPermission(input.id) + + if (permission) { + return err(new PermissionAlreadyExistsError()) + } + await savePermission(input) } diff --git a/apps/settings-service/src/handlers/deleteConfig/validation.ts b/apps/settings-service/src/handlers/deleteConfig/validation.ts index 46673ffd63..02c1ede625 100644 --- a/apps/settings-service/src/handlers/deleteConfig/validation.ts +++ b/apps/settings-service/src/handlers/deleteConfig/validation.ts @@ -7,7 +7,7 @@ import { isUUID } from "../../utils/isUUID" export const deleteConfigSchema = z .object({ - id: z.string().refine(isUUID).openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), + id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), }) .strict() @@ -33,4 +33,4 @@ export const deleteConfigDescription = describeRoute({ }, }) -export const deleteConfigValidation = zv("param", inputSchema) +export const deleteConfigValidation = zv("json", inputSchema) diff --git a/apps/settings-service/src/repositories/permissionsRepository.ts b/apps/settings-service/src/repositories/permissionsRepository.ts index b9c492d5d1..d1e60a0cca 100644 --- a/apps/settings-service/src/repositories/permissionsRepository.ts +++ b/apps/settings-service/src/repositories/permissionsRepository.ts @@ -15,7 +15,6 @@ function fromDtoToDb(permission: WalletPermission): Insertable): WalletPermi id: permission.id, updatedAt: permission.updatedAt, createdAt: permission.createdAt, - deleted: permission.deleted === 1, } } @@ -38,7 +36,7 @@ export function savePermission(permission: WalletPermission) { return db.insertInto("walletPermissions").values(fromDtoToDb(permission)).execute() } -export function getPermission(id: UUID) { +export function getPermission(id: string) { return db.selectFrom("walletPermissions").where("id", "=", id).selectAll().executeTakeFirst() } @@ -61,6 +59,6 @@ export async function updatePermission(permission: WalletPermission) { .execute() } -export async function deletePermission(id: UUID) { - return await db.updateTable("walletPermissions").set({ deleted: true, updatedAt: Date.now() }).where("id", "=", id).execute() +export async function deletePermission(id: string) { + return await db.deleteFrom("walletPermissions").where("id", "=", id).execute() } diff --git a/apps/settings-service/src/server/configRoute.ts b/apps/settings-service/src/server/configRoute.ts index c6c4674a6f..8d641de46b 100644 --- a/apps/settings-service/src/server/configRoute.ts +++ b/apps/settings-service/src/server/configRoute.ts @@ -30,8 +30,8 @@ export default new Hono() const [response, code] = makeResponse(result) return c.json(response, code) }) - .delete("/delete/:id", deleteConfigDescription, deleteConfigValidation, async (c) => { - const input = c.req.valid("param") + .delete("/delete", deleteConfigDescription, deleteConfigValidation, async (c) => { + const input = c.req.valid("json") const result = await deleteConfig(input) const [response, code] = makeResponse(result) From d129cf4989eef6f12f3276145a52be6440dedb23 Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Thu, 22 May 2025 16:23:48 +0200 Subject: [PATCH 07/19] chore: pr review --- .../permissions/useAppsWithPermissions.ts | 1 - apps/iframe/src/state/permissions.ts | 10 +-- apps/settings-service/.env.example | 0 apps/sync-service/.env.example | 3 + .../Makefile | 4 +- .../build.config.ts | 0 .../package.json | 3 +- .../src/db/driver.ts | 0 .../db/migrations/Migration20250515123000.ts | 0 .../src/db/migrations/index.ts | 0 .../src/db/types.ts | 8 +- .../src/dtos.ts | 0 .../src/env.ts | 9 +- .../src/errors.ts | 0 .../src/handlers/createConfig/createConfig.ts | 0 .../src/handlers/createConfig/index.ts | 0 .../src/handlers/createConfig/types.ts | 0 .../src/handlers/createConfig/validation.ts | 0 .../src/handlers/deleteConfig/deleteConfig.ts | 0 .../src/handlers/deleteConfig/index.ts | 0 .../src/handlers/deleteConfig/types.ts | 0 .../src/handlers/deleteConfig/validation.ts | 0 .../src/handlers/listConfig/index.ts | 0 .../src/handlers/listConfig/listConfig.ts | 0 .../src/handlers/listConfig/types.ts | 0 .../src/handlers/listConfig/validation.ts | 0 .../src/handlers/updateConfig/index.ts | 0 .../src/handlers/updateConfig/types.ts | 0 .../src/handlers/updateConfig/updateConfig.ts | 0 .../src/handlers/updateConfig/validation.ts | 0 .../src/index.ts | 5 +- .../src/migrate.ts | 0 .../src/repositories/permissionsRepository.ts | 24 ++---- .../src/server/configRoute.ts | 0 .../src/server/index.ts | 0 .../src/server/makeResponse.ts | 0 .../src/utils/isAppUrl.ts | 0 .../src/utils/isProduction.ts | 0 .../src/utils/isUUID.ts | 0 .../src/utils/logger.ts | 2 +- .../tsconfig.build.json | 3 - .../tsconfig.json | 3 - bun.lock | 84 +++++++++---------- support/common/lib/utils/uuid.ts | 6 ++ support/common/package.json | 3 +- 45 files changed, 70 insertions(+), 98 deletions(-) delete mode 100644 apps/settings-service/.env.example create mode 100644 apps/sync-service/.env.example rename apps/{settings-service => sync-service}/Makefile (83%) rename apps/{settings-service => sync-service}/build.config.ts (100%) rename apps/{settings-service => sync-service}/package.json (90%) rename apps/{settings-service => sync-service}/src/db/driver.ts (100%) rename apps/{settings-service => sync-service}/src/db/migrations/Migration20250515123000.ts (100%) rename apps/{settings-service => sync-service}/src/db/migrations/index.ts (100%) rename apps/{settings-service => sync-service}/src/db/types.ts (79%) rename apps/{settings-service => sync-service}/src/dtos.ts (100%) rename apps/{settings-service => sync-service}/src/env.ts (61%) rename apps/{settings-service => sync-service}/src/errors.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/createConfig/createConfig.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/createConfig/index.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/createConfig/types.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/createConfig/validation.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/deleteConfig/deleteConfig.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/deleteConfig/index.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/deleteConfig/types.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/deleteConfig/validation.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/listConfig/index.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/listConfig/listConfig.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/listConfig/types.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/listConfig/validation.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/updateConfig/index.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/updateConfig/types.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/updateConfig/updateConfig.ts (100%) rename apps/{settings-service => sync-service}/src/handlers/updateConfig/validation.ts (100%) rename apps/{settings-service => sync-service}/src/index.ts (76%) rename apps/{settings-service => sync-service}/src/migrate.ts (100%) rename apps/{settings-service => sync-service}/src/repositories/permissionsRepository.ts (64%) rename apps/{settings-service => sync-service}/src/server/configRoute.ts (100%) rename apps/{settings-service => sync-service}/src/server/index.ts (100%) rename apps/{settings-service => sync-service}/src/server/makeResponse.ts (100%) rename apps/{settings-service => sync-service}/src/utils/isAppUrl.ts (100%) rename apps/{settings-service => sync-service}/src/utils/isProduction.ts (100%) rename apps/{settings-service => sync-service}/src/utils/isUUID.ts (100%) rename apps/{settings-service => sync-service}/src/utils/logger.ts (100%) rename apps/{settings-service => sync-service}/tsconfig.build.json (73%) rename apps/{settings-service => sync-service}/tsconfig.json (67%) diff --git a/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts b/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts index 4f8cebcf41..f3e5d9b945 100644 --- a/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts +++ b/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts @@ -1,4 +1,3 @@ - import { use$ } from "@legendapp/state/react" import { type AppPermissions, permissionsMapLegend } from "#src/state/permissions" import { type AppURL, isWallet } from "#src/utils/appURL" diff --git a/apps/iframe/src/state/permissions.ts b/apps/iframe/src/state/permissions.ts index 653643045b..8d32b9ccc6 100644 --- a/apps/iframe/src/state/permissions.ts +++ b/apps/iframe/src/state/permissions.ts @@ -89,14 +89,9 @@ export type SessionKeyRequest = { */ export type PermissionsRequest = string | PermissionRequestObject -type PermissionCheckParams = { - permissionsRequest: PermissionsRequest - app: AppURL -} - export const permissionsMapLegend = observable( syncedCrud({ - list: async ({ lastSync }) => { + list: async () => { const user = getUser() if (!user) return [] const response = await fetch(`http://localhost:3000/api/v1/settings/list?user=${user.address}`) @@ -126,7 +121,7 @@ export const permissionsMapLegend = observable( }, subscribe: ({ refresh }) => { // Set up an interval to refresh messages every 5 seconds - const intervalId = setInterval(() => { + setInterval(() => { console.log("Refreshing config (5-second interval)") refresh() }, 5000) @@ -360,7 +355,6 @@ export function grantPermissions(app: AppURL, permissionRequest: PermissionsRequ parentCapability: name, date: Date.now(), id, - deleted: false, updatedAt: Date.now(), createdAt: Date.now(), type: "WalletPermissions", diff --git a/apps/settings-service/.env.example b/apps/settings-service/.env.example deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/sync-service/.env.example b/apps/sync-service/.env.example new file mode 100644 index 0000000000..c6ab1df781 --- /dev/null +++ b/apps/sync-service/.env.example @@ -0,0 +1,3 @@ +NODE_ENV=development +APP_PORT=3000 +SETTINGS_DB_URL=settings.sqlite \ No newline at end of file diff --git a/apps/settings-service/Makefile b/apps/sync-service/Makefile similarity index 83% rename from apps/settings-service/Makefile rename to apps/sync-service/Makefile index 610642747d..6fe7c145dd 100644 --- a/apps/settings-service/Makefile +++ b/apps/sync-service/Makefile @@ -5,9 +5,9 @@ include ../../makefiles/formatting.mk include ../../makefiles/bundling.mk include ../../makefiles/help.mk -start: ## Starts the settings service +dev: ## Starts the settings service bun run --hot src/index.ts -.PHONY: start +.PHONY: dev migrate: ## Runs pending migrations bun run src/migrate.ts diff --git a/apps/settings-service/build.config.ts b/apps/sync-service/build.config.ts similarity index 100% rename from apps/settings-service/build.config.ts rename to apps/sync-service/build.config.ts diff --git a/apps/settings-service/package.json b/apps/sync-service/package.json similarity index 90% rename from apps/settings-service/package.json rename to apps/sync-service/package.json index 58bea71b12..d56a834698 100644 --- a/apps/settings-service/package.json +++ b/apps/sync-service/package.json @@ -1,5 +1,5 @@ { - "name": "@happy.tech/settings-service", + "name": "@happy.tech/sync-service", "private": true, "version": "0.1.0", "type": "module", @@ -16,7 +16,6 @@ "hono": "^4.7.2", "hono-openapi": "^0.4.4", "neverthrow": "^8.1.0", - "uuid": "^11.1.0", "zod": "^3.23.8", "zod-openapi": "^4.2.3" }, diff --git a/apps/settings-service/src/db/driver.ts b/apps/sync-service/src/db/driver.ts similarity index 100% rename from apps/settings-service/src/db/driver.ts rename to apps/sync-service/src/db/driver.ts diff --git a/apps/settings-service/src/db/migrations/Migration20250515123000.ts b/apps/sync-service/src/db/migrations/Migration20250515123000.ts similarity index 100% rename from apps/settings-service/src/db/migrations/Migration20250515123000.ts rename to apps/sync-service/src/db/migrations/Migration20250515123000.ts diff --git a/apps/settings-service/src/db/migrations/index.ts b/apps/sync-service/src/db/migrations/index.ts similarity index 100% rename from apps/settings-service/src/db/migrations/index.ts rename to apps/sync-service/src/db/migrations/index.ts diff --git a/apps/settings-service/src/db/types.ts b/apps/sync-service/src/db/types.ts similarity index 79% rename from apps/settings-service/src/db/types.ts rename to apps/sync-service/src/db/types.ts index 038da2d122..ccb6fddc16 100644 --- a/apps/settings-service/src/db/types.ts +++ b/apps/sync-service/src/db/types.ts @@ -1,5 +1,5 @@ import type { Hex } from "@happy.tech/common" -import type { HTTPString, UUID } from "@happy.tech/common" +import type { HTTPString } from "@happy.tech/common" import type { ColumnType } from "kysely" export type AppURL = HTTPString & { _brand: "AppHTTPString" } @@ -17,13 +17,13 @@ type WalletPermissionCaveat = { * * This type is copied from Viem (eip1193.ts) but we add a user field. */ -export type WalletPermissionTable = { +export type WalletPermisisonRow = { // The user to which the permission is granted. user: Hex // The app to which the permission is granted. invoker: AppURL // This is the EIP-1193 request that this permission is mapped to. - parentCapability: "eth_accounts" | string // TODO only string or make specific + parentCapability: string caveats: ColumnType date: number // Not in the EIP, but Viem wants this. @@ -33,5 +33,5 @@ export type WalletPermissionTable = { } export interface Database { - walletPermissions: WalletPermissionTable + walletPermissions: WalletPermisisonRow } diff --git a/apps/settings-service/src/dtos.ts b/apps/sync-service/src/dtos.ts similarity index 100% rename from apps/settings-service/src/dtos.ts rename to apps/sync-service/src/dtos.ts diff --git a/apps/settings-service/src/env.ts b/apps/sync-service/src/env.ts similarity index 61% rename from apps/settings-service/src/env.ts rename to apps/sync-service/src/env.ts index ccb1f7c79e..ba78181839 100644 --- a/apps/settings-service/src/env.ts +++ b/apps/sync-service/src/env.ts @@ -10,11 +10,4 @@ const envSchema = z.object({ ), }) -const parsedEnv = envSchema.safeParse(process.env) - -if (!parsedEnv.success) { - console.log(parsedEnv.error.issues) - throw new Error("There is an error with the server environment variables") -} - -export const env = parsedEnv.data +export const env = envSchema.parse(process.env) diff --git a/apps/settings-service/src/errors.ts b/apps/sync-service/src/errors.ts similarity index 100% rename from apps/settings-service/src/errors.ts rename to apps/sync-service/src/errors.ts diff --git a/apps/settings-service/src/handlers/createConfig/createConfig.ts b/apps/sync-service/src/handlers/createConfig/createConfig.ts similarity index 100% rename from apps/settings-service/src/handlers/createConfig/createConfig.ts rename to apps/sync-service/src/handlers/createConfig/createConfig.ts diff --git a/apps/settings-service/src/handlers/createConfig/index.ts b/apps/sync-service/src/handlers/createConfig/index.ts similarity index 100% rename from apps/settings-service/src/handlers/createConfig/index.ts rename to apps/sync-service/src/handlers/createConfig/index.ts diff --git a/apps/settings-service/src/handlers/createConfig/types.ts b/apps/sync-service/src/handlers/createConfig/types.ts similarity index 100% rename from apps/settings-service/src/handlers/createConfig/types.ts rename to apps/sync-service/src/handlers/createConfig/types.ts diff --git a/apps/settings-service/src/handlers/createConfig/validation.ts b/apps/sync-service/src/handlers/createConfig/validation.ts similarity index 100% rename from apps/settings-service/src/handlers/createConfig/validation.ts rename to apps/sync-service/src/handlers/createConfig/validation.ts diff --git a/apps/settings-service/src/handlers/deleteConfig/deleteConfig.ts b/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts similarity index 100% rename from apps/settings-service/src/handlers/deleteConfig/deleteConfig.ts rename to apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts diff --git a/apps/settings-service/src/handlers/deleteConfig/index.ts b/apps/sync-service/src/handlers/deleteConfig/index.ts similarity index 100% rename from apps/settings-service/src/handlers/deleteConfig/index.ts rename to apps/sync-service/src/handlers/deleteConfig/index.ts diff --git a/apps/settings-service/src/handlers/deleteConfig/types.ts b/apps/sync-service/src/handlers/deleteConfig/types.ts similarity index 100% rename from apps/settings-service/src/handlers/deleteConfig/types.ts rename to apps/sync-service/src/handlers/deleteConfig/types.ts diff --git a/apps/settings-service/src/handlers/deleteConfig/validation.ts b/apps/sync-service/src/handlers/deleteConfig/validation.ts similarity index 100% rename from apps/settings-service/src/handlers/deleteConfig/validation.ts rename to apps/sync-service/src/handlers/deleteConfig/validation.ts diff --git a/apps/settings-service/src/handlers/listConfig/index.ts b/apps/sync-service/src/handlers/listConfig/index.ts similarity index 100% rename from apps/settings-service/src/handlers/listConfig/index.ts rename to apps/sync-service/src/handlers/listConfig/index.ts diff --git a/apps/settings-service/src/handlers/listConfig/listConfig.ts b/apps/sync-service/src/handlers/listConfig/listConfig.ts similarity index 100% rename from apps/settings-service/src/handlers/listConfig/listConfig.ts rename to apps/sync-service/src/handlers/listConfig/listConfig.ts diff --git a/apps/settings-service/src/handlers/listConfig/types.ts b/apps/sync-service/src/handlers/listConfig/types.ts similarity index 100% rename from apps/settings-service/src/handlers/listConfig/types.ts rename to apps/sync-service/src/handlers/listConfig/types.ts diff --git a/apps/settings-service/src/handlers/listConfig/validation.ts b/apps/sync-service/src/handlers/listConfig/validation.ts similarity index 100% rename from apps/settings-service/src/handlers/listConfig/validation.ts rename to apps/sync-service/src/handlers/listConfig/validation.ts diff --git a/apps/settings-service/src/handlers/updateConfig/index.ts b/apps/sync-service/src/handlers/updateConfig/index.ts similarity index 100% rename from apps/settings-service/src/handlers/updateConfig/index.ts rename to apps/sync-service/src/handlers/updateConfig/index.ts diff --git a/apps/settings-service/src/handlers/updateConfig/types.ts b/apps/sync-service/src/handlers/updateConfig/types.ts similarity index 100% rename from apps/settings-service/src/handlers/updateConfig/types.ts rename to apps/sync-service/src/handlers/updateConfig/types.ts diff --git a/apps/settings-service/src/handlers/updateConfig/updateConfig.ts b/apps/sync-service/src/handlers/updateConfig/updateConfig.ts similarity index 100% rename from apps/settings-service/src/handlers/updateConfig/updateConfig.ts rename to apps/sync-service/src/handlers/updateConfig/updateConfig.ts diff --git a/apps/settings-service/src/handlers/updateConfig/validation.ts b/apps/sync-service/src/handlers/updateConfig/validation.ts similarity index 100% rename from apps/settings-service/src/handlers/updateConfig/validation.ts rename to apps/sync-service/src/handlers/updateConfig/validation.ts diff --git a/apps/settings-service/src/index.ts b/apps/sync-service/src/index.ts similarity index 76% rename from apps/settings-service/src/index.ts rename to apps/sync-service/src/index.ts index 67d7761b49..2b4c3ba8b2 100644 --- a/apps/settings-service/src/index.ts +++ b/apps/sync-service/src/index.ts @@ -1,11 +1,10 @@ -import { serve } from "@hono/node-server" import { env } from "./env" import { app } from "./server" import type { AppType } from "./server" export type { AppType } -serve({ +export default { port: env.APP_PORT, fetch: app.fetch, -}) +} \ No newline at end of file diff --git a/apps/settings-service/src/migrate.ts b/apps/sync-service/src/migrate.ts similarity index 100% rename from apps/settings-service/src/migrate.ts rename to apps/sync-service/src/migrate.ts diff --git a/apps/settings-service/src/repositories/permissionsRepository.ts b/apps/sync-service/src/repositories/permissionsRepository.ts similarity index 64% rename from apps/settings-service/src/repositories/permissionsRepository.ts rename to apps/sync-service/src/repositories/permissionsRepository.ts index d1e60a0cca..2a3d73be9c 100644 --- a/apps/settings-service/src/repositories/permissionsRepository.ts +++ b/apps/sync-service/src/repositories/permissionsRepository.ts @@ -1,34 +1,20 @@ import type { Hex } from "@happy.tech/common" -import type { UUID } from "@happy.tech/common" import type { Insertable, Selectable } from "kysely" import { db } from "../db/driver" -import type { WalletPermissionTable } from "../db/types" +import type { WalletPermisisonRow } from "../db/types" import type { WalletPermission } from "../dtos" -function fromDtoToDb(permission: WalletPermission): Insertable { +function fromDtoToDb(permission: WalletPermission): Insertable { return { - user: permission.user, - invoker: permission.invoker, - parentCapability: permission.parentCapability, + ...permission, caveats: JSON.stringify(permission.caveats), - date: permission.date, - id: permission.id, - updatedAt: permission.updatedAt, - createdAt: permission.createdAt, } } -function fromDbToDto(permission: Selectable): WalletPermission { +function fromDbToDto(permission: Selectable): WalletPermission { return { type: "WalletPermissions", - user: permission.user, - invoker: permission.invoker, - parentCapability: permission.parentCapability, - caveats: permission.caveats, - date: permission.date, - id: permission.id, - updatedAt: permission.updatedAt, - createdAt: permission.createdAt, + ...permission, } } diff --git a/apps/settings-service/src/server/configRoute.ts b/apps/sync-service/src/server/configRoute.ts similarity index 100% rename from apps/settings-service/src/server/configRoute.ts rename to apps/sync-service/src/server/configRoute.ts diff --git a/apps/settings-service/src/server/index.ts b/apps/sync-service/src/server/index.ts similarity index 100% rename from apps/settings-service/src/server/index.ts rename to apps/sync-service/src/server/index.ts diff --git a/apps/settings-service/src/server/makeResponse.ts b/apps/sync-service/src/server/makeResponse.ts similarity index 100% rename from apps/settings-service/src/server/makeResponse.ts rename to apps/sync-service/src/server/makeResponse.ts diff --git a/apps/settings-service/src/utils/isAppUrl.ts b/apps/sync-service/src/utils/isAppUrl.ts similarity index 100% rename from apps/settings-service/src/utils/isAppUrl.ts rename to apps/sync-service/src/utils/isAppUrl.ts diff --git a/apps/settings-service/src/utils/isProduction.ts b/apps/sync-service/src/utils/isProduction.ts similarity index 100% rename from apps/settings-service/src/utils/isProduction.ts rename to apps/sync-service/src/utils/isProduction.ts diff --git a/apps/settings-service/src/utils/isUUID.ts b/apps/sync-service/src/utils/isUUID.ts similarity index 100% rename from apps/settings-service/src/utils/isUUID.ts rename to apps/sync-service/src/utils/isUUID.ts diff --git a/apps/settings-service/src/utils/logger.ts b/apps/sync-service/src/utils/logger.ts similarity index 100% rename from apps/settings-service/src/utils/logger.ts rename to apps/sync-service/src/utils/logger.ts index 395d519d4a..6c62cd82fd 100644 --- a/apps/settings-service/src/utils/logger.ts +++ b/apps/sync-service/src/utils/logger.ts @@ -6,8 +6,8 @@ const defaultLogLevel = logLevel(env.LOG_LEVEL) Logger.instance.setLogLevel(defaultLogLevel) export const logger = Logger.create("SettingsService") - const responseLogger = Logger.create("Response", LogLevel.TRACE) + export const logJSONResponseMiddleware = createMiddleware(async (c, next) => { await next() diff --git a/apps/settings-service/tsconfig.build.json b/apps/sync-service/tsconfig.build.json similarity index 73% rename from apps/settings-service/tsconfig.build.json rename to apps/sync-service/tsconfig.build.json index 9db3d49060..7a86a94de4 100644 --- a/apps/settings-service/tsconfig.build.json +++ b/apps/sync-service/tsconfig.build.json @@ -1,7 +1,4 @@ { "extends": ["../../support/configs/tsconfig.base.json", "../../support/configs/tsconfig.types.json"], - "compilerOptions": { - "strict": true - }, "include": ["src", "./package.json"] } diff --git a/apps/settings-service/tsconfig.json b/apps/sync-service/tsconfig.json similarity index 67% rename from apps/settings-service/tsconfig.json rename to apps/sync-service/tsconfig.json index 164a3cda4b..fe56c15ad1 100644 --- a/apps/settings-service/tsconfig.json +++ b/apps/sync-service/tsconfig.json @@ -1,7 +1,4 @@ { "extends": ["../../support/configs/tsconfig.base.json"], - "compilerOptions": { - "strict": true - }, "include": ["*.ts", "src", "./package.json"] } diff --git a/bun.lock b/bun.lock index 768244f7f1..f0744b4eae 100644 --- a/bun.lock +++ b/bun.lock @@ -179,26 +179,6 @@ "typescript": "^5.6.2", }, }, - "apps/settings-service": { - "name": "@happy.tech/settings-service", - "version": "0.1.0", - "dependencies": { - "@happy.tech/common": "workspace:1.0.0", - "@hono/node-server": "^1.13.8", - "@scalar/hono-api-reference": "^0.5.175", - "hono": "^4.7.2", - "hono-openapi": "^0.4.4", - "neverthrow": "^8.1.0", - "uuid": "^11.1.0", - "zod": "^3.23.8", - "zod-openapi": "^4.2.3", - }, - "devDependencies": { - "@happy.tech/happybuild": "workspace:1.0.0", - "hono-openapi": "^0.4.4", - "typescript": "^5.6.2", - }, - }, "apps/submitter": { "name": "@happy.tech/submitter", "version": "0.1.0", @@ -234,6 +214,25 @@ "typescript": "^5.6.2", }, }, + "apps/sync-service": { + "name": "@happy.tech/sync-service", + "version": "0.1.0", + "dependencies": { + "@happy.tech/common": "workspace:1.0.0", + "@hono/node-server": "^1.13.8", + "@scalar/hono-api-reference": "^0.5.175", + "hono": "^4.7.2", + "hono-openapi": "^0.4.4", + "neverthrow": "^8.1.0", + "zod": "^3.23.8", + "zod-openapi": "^4.2.3", + }, + "devDependencies": { + "@happy.tech/happybuild": "workspace:1.0.0", + "hono-openapi": "^0.4.4", + "typescript": "^5.6.2", + }, + }, "contracts": { "name": "@happy.tech/contracts", "version": "0.2.0", @@ -431,6 +430,7 @@ "version": "1.0.0", "dependencies": { "@opentelemetry/api": "^1.9.0", + "uuid": "^11.1.0", }, "devDependencies": { "@happy.tech/configs": "workspace:*", @@ -914,7 +914,7 @@ "@floating-ui/core": ["@floating-ui/core@1.7.1", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw=="], - "@floating-ui/dom": ["@floating-ui/dom@1.7.1", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/utils": "^0.2.9" } }, "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ=="], + "@floating-ui/dom": ["@floating-ui/dom@1.6.13", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w=="], "@floating-ui/react": ["@floating-ui/react@0.27.12", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.3", "@floating-ui/utils": "^0.2.9", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-kKlWNrpIQxF1B/a2MZvE0/uyKby4960yjO91W7nVyNKmmfNi62xU9HCjL1M1eWzx/LFj/VPSwJVbwQk9Pq/68A=="], @@ -958,10 +958,10 @@ "@happy.tech/react": ["@happy.tech/react@workspace:packages/react"], - "@happy.tech/settings-service": ["@happy.tech/settings-service@workspace:apps/settings-service"], - "@happy.tech/submitter": ["@happy.tech/submitter@workspace:apps/submitter"], + "@happy.tech/sync-service": ["@happy.tech/sync-service@workspace:apps/sync-service"], + "@happy.tech/testing": ["@happy.tech/testing@workspace:support/testing"], "@happy.tech/txm": ["@happy.tech/txm@workspace:packages/txm"], @@ -4004,7 +4004,7 @@ "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], - "ripemd160": ["ripemd160@2.0.2", "", { "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" } }, "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA=="], + "ripemd160": ["ripemd160@2.0.1", "", { "dependencies": { "hash-base": "^2.0.0", "inherits": "^2.0.1" } }, "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w=="], "rlp": ["rlp@2.2.7", "", { "dependencies": { "bn.js": "^5.2.0" }, "bin": { "rlp": "bin/rlp" } }, "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ=="], @@ -4634,10 +4634,6 @@ "@ethersproject/providers/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], - "@firebase/component/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@firebase/logger/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@happy.tech/txm/kysely-bun-sqlite": ["kysely-bun-sqlite@0.4.0", "", { "dependencies": { "bun-types": "^1.1.31" }, "peerDependencies": { "kysely": "^0.28.2" } }, "sha512-2EkQE5sT4ewiw7IWfJsAkpxJ/QPVKXKO5sRYI/xjjJIJlECuOdtG+ssYM0twZJySrdrmuildNPFYVreyu1EdZg=="], "@humanwhocodes/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -4696,6 +4692,12 @@ "@microsoft/tsdoc-config/ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="], + "@motionone/easing/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@motionone/generators/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@motionone/utils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@nomiclabs/hardhat-etherscan/cbor": ["cbor@5.2.0", "", { "dependencies": { "bignumber.js": "^9.0.1", "nofilter": "^1.0.4" } }, "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A=="], "@nomiclabs/hardhat-etherscan/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -4888,12 +4890,12 @@ "@web3auth/auth/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "@zag-js/popper/@floating-ui/dom": ["@floating-ui/dom@1.6.13", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w=="], - "account-abstraction/typescript": ["typescript@4.9.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "aria-hidden/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "asn1.js/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], "ast-types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -4924,13 +4926,13 @@ "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "create-ecdh/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], + "create-ecdh/bn.js": ["bn.js@4.11.6", "", {}, "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA=="], "create-vocs/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], "crypto-random-string/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], - "diffie-hellman/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], + "diffie-hellman/bn.js": ["bn.js@4.11.6", "", {}, "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA=="], "elliptic/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], @@ -5018,8 +5020,6 @@ "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "extension-port-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "find-replace/array-back": ["array-back@1.0.4", "", { "dependencies": { "typical": "^2.6.0" } }, "sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw=="], @@ -5192,8 +5192,6 @@ "pbkdf2/create-hash": ["create-hash@1.1.3", "", { "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", "ripemd160": "^2.0.0", "sha.js": "^2.4.0" } }, "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA=="], - "pbkdf2/ripemd160": ["ripemd160@2.0.1", "", { "dependencies": { "hash-base": "^2.0.0", "inherits": "^2.0.1" } }, "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w=="], - "pkg-dir/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], "prebuild-install/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], @@ -5202,7 +5200,7 @@ "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "public-encrypt/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], + "public-encrypt/bn.js": ["bn.js@4.11.6", "", {}, "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA=="], "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], @@ -5210,6 +5208,8 @@ "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "react-remove-scroll/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "react-remove-scroll-bar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "react-style-singleton/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -5222,6 +5222,8 @@ "recast/esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + "recast/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "recursive-readdir/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "rehype-autolink-headings/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], @@ -5234,6 +5236,8 @@ "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "ripemd160/hash-base": ["hash-base@2.0.2", "", { "dependencies": { "inherits": "^2.0.1" } }, "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw=="], + "sc-istanbul/glob": ["glob@5.0.15", "", { "dependencies": { "inflight": "^1.0.4", "inherits": "2", "minimatch": "2 || 3", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA=="], "sc-istanbul/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], @@ -5590,8 +5594,6 @@ "ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], - "extension-port-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "ghost-testrpc/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "ghost-testrpc/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], @@ -5676,10 +5678,6 @@ "ora/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], - "pbkdf2/create-hash/ripemd160": ["ripemd160@2.0.2", "", { "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" } }, "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA=="], - - "pbkdf2/ripemd160/hash-base": ["hash-base@2.0.2", "", { "dependencies": { "inherits": "^2.0.1" } }, "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw=="], - "pkg-dir/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], diff --git a/support/common/lib/utils/uuid.ts b/support/common/lib/utils/uuid.ts index 6424ba02c9..80849d80f6 100644 --- a/support/common/lib/utils/uuid.ts +++ b/support/common/lib/utils/uuid.ts @@ -1,5 +1,11 @@ +import { validate, version } from "uuid" + export type UUID = ReturnType & { _brand: "uuid" } export function createUUID(): UUID { return crypto.randomUUID() as UUID } + +export function isUUID(str: string): str is UUID { + return validate(str) && version(str) === 4 +} diff --git a/support/common/package.json b/support/common/package.json index c12d92fbdf..4ff2ebb4b1 100644 --- a/support/common/package.json +++ b/support/common/package.json @@ -34,6 +34,7 @@ "vite-plugin-node-polyfills": "^0.22.0" }, "dependencies": { - "@opentelemetry/api": "^1.9.0" + "@opentelemetry/api": "^1.9.0", + "uuid": "^11.1.0" } } From d9ef015e2862331f791cb7bb3073c10f516d4fe1 Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Tue, 3 Jun 2025 16:08:44 +0200 Subject: [PATCH 08/19] chore: sse events sync service --- apps/iframe/src/state/permissions.ts | 65 ++++++++++++++++--- .../db/migrations/Migration20250515123000.ts | 18 ++--- apps/sync-service/src/db/types.ts | 1 + apps/sync-service/src/dtos.ts | 31 ++++++++- apps/sync-service/src/errors.ts | 12 ---- .../src/handlers/createConfig/createConfig.ts | 23 ++++--- .../src/handlers/deleteConfig/deleteConfig.ts | 20 +++++- .../src/handlers/deleteConfig/validation.ts | 1 - .../src/handlers/subscribe/index.ts | 2 + .../src/handlers/subscribe/subscribe.ts | 16 +++++ .../src/handlers/subscribe/types.ts | 4 ++ .../src/handlers/subscribe/validation.ts | 24 +++++++ .../src/handlers/updateConfig/updateConfig.ts | 25 ++++--- .../src/handlers/updateConfig/validation.ts | 9 +-- apps/sync-service/src/index.ts | 2 +- .../src/repositories/permissionsRepository.ts | 37 +++++++---- apps/sync-service/src/server/configRoute.ts | 10 +++ apps/sync-service/src/server/index.ts | 6 +- .../src/services/notifyUpdates.ts | 33 ++++++++++ apps/sync-service/src/utils/logger.ts | 1 + 20 files changed, 267 insertions(+), 73 deletions(-) create mode 100644 apps/sync-service/src/handlers/subscribe/index.ts create mode 100644 apps/sync-service/src/handlers/subscribe/subscribe.ts create mode 100644 apps/sync-service/src/handlers/subscribe/types.ts create mode 100644 apps/sync-service/src/handlers/subscribe/validation.ts create mode 100644 apps/sync-service/src/services/notifyUpdates.ts diff --git a/apps/iframe/src/state/permissions.ts b/apps/iframe/src/state/permissions.ts index 8d32b9ccc6..9cac8acac5 100644 --- a/apps/iframe/src/state/permissions.ts +++ b/apps/iframe/src/state/permissions.ts @@ -91,10 +91,13 @@ export type PermissionsRequest = string | PermissionRequestObject export const permissionsMapLegend = observable( syncedCrud({ - list: async () => { + list: async ({ lastSync }) => { const user = getUser() if (!user) return [] - const response = await fetch(`http://localhost:3000/api/v1/settings/list?user=${user.address}`) + + const response = await fetch( + `http://localhost:3000/api/v1/settings/list?user=${user.address}${lastSync ? `&lastUpdated=${lastSync}` : ""}`, + ) const data = await response.json() return data.data as WalletPermission[] @@ -110,21 +113,35 @@ export const permissionsMapLegend = observable( await response.json() }, update: async (data: WalletPermission) => { + const user = getUser() + if (!user) return + const response = await fetch("http://localhost:3000/api/v1/settings/update", { method: "PUT", headers: { "Content-Type": "application/json", }, - body: JSON.stringify(data), + body: JSON.stringify({ + ...data, + type: "WalletPermissions", + user: user.address, + }), }) await response.json() }, subscribe: ({ refresh }) => { - // Set up an interval to refresh messages every 5 seconds - setInterval(() => { - console.log("Refreshing config (5-second interval)") + const user = getUser() + if (!user) return () => {} + + console.log("Subscribing to updates for user", user.address) + + const eventSource = new EventSource(`http://localhost:3000/api/v1/settings/subscribe?user=${user.address}`) + eventSource.addEventListener("update", (event) => { + const data = JSON.parse(event.data) + console.log("Received update", data) refresh() - }, 5000) + }) + return () => eventSource.close() }, delete: async ({ id }) => { const response = await fetch("http://localhost:3000/api/v1/settings/delete", { @@ -142,13 +159,42 @@ export const permissionsMapLegend = observable( retrySync: true, // Retry sync after reload }, initial: {}, - fieldCreatedAt: "created_at", + fieldCreatedAt: "createdAt", fieldUpdatedAt: "updatedAt", fieldDeleted: "deleted", - changesSince: "all", + changesSince: "last-sync", + updatePartial: true, }), ) +// // === RANDOM PERMISSION UPDATE SIMULATION ======================================================================== + +const mockPermissionUpdates = () => { + const permissions = permissionsMapLegend.get() + const permissionArray = Object.values(permissions) + + if (permissionArray.length === 0) return + + // Randomly select a permission to update + const randomIndex = Math.floor(Math.random() * permissionArray.length) + const permissionToUpdate = permissionArray[randomIndex] + + // Update the timestamp + const updatedPermission = { + ...permissionToUpdate, + caveats: [...permissionToUpdate.caveats, { type: "random", value: String(Math.random()) }], + updatedAt: Date.now() + } + + + // Update in the legend + permissionsMapLegend[permissionToUpdate.id].set(updatedPermission) +} + +// Set up interval for random updates every 5 seconds +setInterval(mockPermissionUpdates, 5000) + + // === GET ALL PERMISSIONS ======================================================================================= /** @@ -359,6 +405,7 @@ export function grantPermissions(app: AppURL, permissionRequest: PermissionsRequ createdAt: Date.now(), type: "WalletPermissions", user: user.address, + deleted: false, } grantedPermissions.push(grantedPermission) diff --git a/apps/sync-service/src/db/migrations/Migration20250515123000.ts b/apps/sync-service/src/db/migrations/Migration20250515123000.ts index 88e8c46be6..8b4fc215e8 100644 --- a/apps/sync-service/src/db/migrations/Migration20250515123000.ts +++ b/apps/sync-service/src/db/migrations/Migration20250515123000.ts @@ -4,15 +4,15 @@ import type { Database } from "../types" export async function up(db: Kysely) { await db.schema .createTable("walletPermissions") - .addColumn("user", "text") - .addColumn("invoker", "text") - .addColumn("parentCapability", "text") - .addColumn("caveats", "jsonb") - .addColumn("date", "integer") - .addColumn("id", "text", (col) => col.notNull()) - .addColumn("updatedAt", "integer") - .addColumn("createdAt", "integer") - .addColumn("deleted", "boolean") + .addColumn("user", "text", (col) => col.notNull()) + .addColumn("invoker", "text", (col) => col.notNull()) + .addColumn("parentCapability", "text", (col) => col.notNull()) + .addColumn("caveats", "jsonb", (col) => col.notNull()) + .addColumn("date", "integer", (col) => col.notNull()) + .addColumn("id", "text", (col) => col.notNull().primaryKey()) + .addColumn("updatedAt", "integer", (col) => col.notNull()) + .addColumn("createdAt", "integer", (col) => col.notNull()) + .addColumn("deleted", "boolean", (col) => col.notNull()) .execute() } diff --git a/apps/sync-service/src/db/types.ts b/apps/sync-service/src/db/types.ts index ccb6fddc16..a9692cbe3a 100644 --- a/apps/sync-service/src/db/types.ts +++ b/apps/sync-service/src/db/types.ts @@ -30,6 +30,7 @@ export type WalletPermisisonRow = { id: string updatedAt: number createdAt: number + deleted: ColumnType } export interface Database { diff --git a/apps/sync-service/src/dtos.ts b/apps/sync-service/src/dtos.ts index 2368c4e3ad..9f6eff470f 100644 --- a/apps/sync-service/src/dtos.ts +++ b/apps/sync-service/src/dtos.ts @@ -2,7 +2,6 @@ import { isAddress } from "@happy.tech/common" import { checksum } from "ox/Address" import { z } from "zod" import { isAppUrl } from "./utils/isAppUrl" -import { isUUID } from "./utils/isUUID" export const walletPermission = z.object({ type: z.literal("WalletPermissions").openapi({ @@ -25,6 +24,36 @@ export const walletPermission = z.object({ id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), updatedAt: z.number().openapi({ example: 1715702400 }), createdAt: z.number().openapi({ example: 1715702400 }), + deleted: z.boolean().openapi({ example: false }), }) export type WalletPermission = z.infer + +export const walletPermissionUpdate = walletPermission.partial().extend({ + type: z.literal("WalletPermissions").openapi({ + example: "WalletPermissions", + type: "string", + }), + user: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), +}) + +export type WalletPermissionUpdate = z.infer + +export const updateEvent = z.object({ + event: z.enum(["WalletPermissions.updated", "WalletPermissions.deleted", "WalletPermissions.created"]), + data: z.object({ + destination: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + resourceId: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), + updatedAt: z.number().openapi({ example: 1715702400 }), + }), + id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), +}) + +export type UpdateEvent = z.infer diff --git a/apps/sync-service/src/errors.ts b/apps/sync-service/src/errors.ts index a0f11763aa..11db85717a 100644 --- a/apps/sync-service/src/errors.ts +++ b/apps/sync-service/src/errors.ts @@ -9,15 +9,3 @@ export abstract class HappySettingsError extends Error { this.statusCode = statusCode } } - -export class PermissionNotFoundError extends HappySettingsError { - constructor(message?: string, options?: ErrorOptions) { - super(404, message || "Permission not found", options) - } -} - -export class PermissionAlreadyExistsError extends HappySettingsError { - constructor(message?: string, options?: ErrorOptions) { - super(409, message || "Permission already exists", options) - } -} diff --git a/apps/sync-service/src/handlers/createConfig/createConfig.ts b/apps/sync-service/src/handlers/createConfig/createConfig.ts index fe63fb1707..1b0fb8e10c 100644 --- a/apps/sync-service/src/handlers/createConfig/createConfig.ts +++ b/apps/sync-service/src/handlers/createConfig/createConfig.ts @@ -1,19 +1,24 @@ -import { type Result, err, ok } from "neverthrow" -import { PermissionAlreadyExistsError } from "../../errors" -import { getPermission, savePermission } from "../../repositories/permissionsRepository" +import { type Result, ok } from "neverthrow" +import { savePermission } from "../../repositories/permissionsRepository" import type { CreateConfigInput } from "./types" +import { notifyUpdates } from "../../services/notifyUpdates" +import { createUUID } from "../../../../../support/common/dist/index.es" export async function createConfig(input: CreateConfigInput): Promise> { console.log(input) if (input.type === "WalletPermissions") { - const permission = await getPermission(input.id) - - if (permission) { - return err(new PermissionAlreadyExistsError()) - } - await savePermission(input) + + notifyUpdates({ + event: "WalletPermissions.created", + data: { + destination: input.user, + resourceId: input.id, + updatedAt: input.updatedAt, + }, + id: createUUID(), + }) } return ok(undefined) diff --git a/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts b/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts index 470194d2a6..b9c08adf13 100644 --- a/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts +++ b/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts @@ -1,9 +1,27 @@ import { type Result, ok } from "neverthrow" -import { deletePermission } from "../../repositories/permissionsRepository" +import { deletePermission, getPermission } from "../../repositories/permissionsRepository" import type { DeleteConfigInput } from "./types" +import { createUUID } from "@happy.tech/common" +import { notifyUpdates } from "../../services/notifyUpdates" export async function deleteConfig(input: DeleteConfigInput): Promise> { + const permission = await getPermission(input.id) + + if (!permission) { + return ok(undefined) + } + await deletePermission(input.id) + notifyUpdates({ + event: "WalletPermissions.deleted", + data: { + destination: permission.user, + resourceId: input.id, + updatedAt: permission.updatedAt, + }, + id: createUUID(), + }) + return ok(undefined) } diff --git a/apps/sync-service/src/handlers/deleteConfig/validation.ts b/apps/sync-service/src/handlers/deleteConfig/validation.ts index 02c1ede625..e3341f73b6 100644 --- a/apps/sync-service/src/handlers/deleteConfig/validation.ts +++ b/apps/sync-service/src/handlers/deleteConfig/validation.ts @@ -3,7 +3,6 @@ import { resolver } from "hono-openapi/zod" import { validator as zv } from "hono-openapi/zod" import { z } from "zod" import { isProduction } from "../../utils/isProduction" -import { isUUID } from "../../utils/isUUID" export const deleteConfigSchema = z .object({ diff --git a/apps/sync-service/src/handlers/subscribe/index.ts b/apps/sync-service/src/handlers/subscribe/index.ts new file mode 100644 index 0000000000..f92f39a368 --- /dev/null +++ b/apps/sync-service/src/handlers/subscribe/index.ts @@ -0,0 +1,2 @@ +export { subscribe } from "./subscribe" +export { subscribeValidation, subscribeDescription } from "./validation" \ No newline at end of file diff --git a/apps/sync-service/src/handlers/subscribe/subscribe.ts b/apps/sync-service/src/handlers/subscribe/subscribe.ts new file mode 100644 index 0000000000..c4b2e13d54 --- /dev/null +++ b/apps/sync-service/src/handlers/subscribe/subscribe.ts @@ -0,0 +1,16 @@ +import type { SSEStreamingApi } from "hono/streaming" +import { promiseWithResolvers } from "@happy.tech/common" +import { saveStream } from "../../services/notifyUpdates" +import type { SubscribeInput } from "./types" + +export async function subscribe(input: SubscribeInput, stream: SSEStreamingApi) { + const {promise, reject } = promiseWithResolvers() + + stream.onAbort(() => { + reject(undefined) + }) + + saveStream(input.user, stream) + + await promise +} diff --git a/apps/sync-service/src/handlers/subscribe/types.ts b/apps/sync-service/src/handlers/subscribe/types.ts new file mode 100644 index 0000000000..f198260bae --- /dev/null +++ b/apps/sync-service/src/handlers/subscribe/types.ts @@ -0,0 +1,4 @@ +import type { z } from "zod" +import type { inputSchema } from "./validation" + +export type SubscribeInput = z.infer diff --git a/apps/sync-service/src/handlers/subscribe/validation.ts b/apps/sync-service/src/handlers/subscribe/validation.ts new file mode 100644 index 0000000000..46ffe0f981 --- /dev/null +++ b/apps/sync-service/src/handlers/subscribe/validation.ts @@ -0,0 +1,24 @@ +import { isAddress } from "@happy.tech/common" +import { describeRoute } from "hono-openapi" +import { validator as zv } from "hono-openapi/zod" +import { checksum } from "ox/Address" +import { z } from "zod" +import { isProduction } from "../../utils/isProduction" + +export const subscribeSchema = z + .object({ + user: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + }) + .strict() + +export const inputSchema = subscribeSchema + +export const subscribeDescription = describeRoute({ + validateResponse: !isProduction, + description: "Subscribe to config updates", +}) + +export const subscribeValidation = zv("query", inputSchema) diff --git a/apps/sync-service/src/handlers/updateConfig/updateConfig.ts b/apps/sync-service/src/handlers/updateConfig/updateConfig.ts index 971150275f..ff33e1d326 100644 --- a/apps/sync-service/src/handlers/updateConfig/updateConfig.ts +++ b/apps/sync-service/src/handlers/updateConfig/updateConfig.ts @@ -1,16 +1,21 @@ -import { type Result, err, ok } from "neverthrow" -import { PermissionNotFoundError } from "../../errors" -import { getPermission, updatePermission } from "../../repositories/permissionsRepository" +import { createUUID } from "@happy.tech/common" +import { type Result, ok } from "neverthrow" +import { savePermission } from "../../repositories/permissionsRepository" +import { notifyUpdates } from "../../services/notifyUpdates" import type { UpdateConfigInput } from "./types" -export async function updateConfig(input: UpdateConfigInput): Promise> { - const permission = await getPermission(input.id) +export async function updateConfig(input: UpdateConfigInput): Promise> { + await savePermission(input) - if (!permission) { - return err(new PermissionNotFoundError()) - } - - await updatePermission(input) + notifyUpdates({ + event: "WalletPermissions.updated", + data: { + destination: input.user, + resourceId: createUUID(), + updatedAt: input.updatedAt ?? Date.now(), + }, + id: input.id, + }) return ok(undefined) } diff --git a/apps/sync-service/src/handlers/updateConfig/validation.ts b/apps/sync-service/src/handlers/updateConfig/validation.ts index 55d2625621..a6b98c7a2b 100644 --- a/apps/sync-service/src/handlers/updateConfig/validation.ts +++ b/apps/sync-service/src/handlers/updateConfig/validation.ts @@ -2,12 +2,13 @@ import { describeRoute } from "hono-openapi" import { resolver } from "hono-openapi/zod" import { validator as zv } from "hono-openapi/zod" import { z } from "zod" -import { walletPermission } from "../../dtos" +import { walletPermissionUpdate } from "../../dtos" import { isProduction } from "../../utils/isProduction" -export const inputSchema: z.ZodDiscriminatedUnion<"type", [typeof walletPermission]> = z.discriminatedUnion("type", [ - walletPermission, -]) +export const inputSchema: z.ZodDiscriminatedUnion<"type", [typeof walletPermissionUpdate]> = z.discriminatedUnion( + "type", + [walletPermissionUpdate], +) export const outputSchema = z.object({ success: z.boolean(), diff --git a/apps/sync-service/src/index.ts b/apps/sync-service/src/index.ts index 2b4c3ba8b2..ddfdd4cb7e 100644 --- a/apps/sync-service/src/index.ts +++ b/apps/sync-service/src/index.ts @@ -7,4 +7,4 @@ export type { AppType } export default { port: env.APP_PORT, fetch: app.fetch, -} \ No newline at end of file +} diff --git a/apps/sync-service/src/repositories/permissionsRepository.ts b/apps/sync-service/src/repositories/permissionsRepository.ts index 2a3d73be9c..8abafb3933 100644 --- a/apps/sync-service/src/repositories/permissionsRepository.ts +++ b/apps/sync-service/src/repositories/permissionsRepository.ts @@ -2,12 +2,14 @@ import type { Hex } from "@happy.tech/common" import type { Insertable, Selectable } from "kysely" import { db } from "../db/driver" import type { WalletPermisisonRow } from "../db/types" -import type { WalletPermission } from "../dtos" +import type { WalletPermission, WalletPermissionUpdate } from "../dtos" -function fromDtoToDb(permission: WalletPermission): Insertable { +function fromDtoToDbUpdate(permission: WalletPermissionUpdate): Partial> { + const { type, caveats, ...rest } = permission return { - ...permission, - caveats: JSON.stringify(permission.caveats), + ...rest, + ...(caveats && { caveats: JSON.stringify(caveats) }), + updatedAt: Date.now(), } } @@ -15,13 +17,10 @@ function fromDbToDto(permission: Selectable): WalletPermiss return { type: "WalletPermissions", ...permission, + deleted: permission.deleted === 1, } } -export function savePermission(permission: WalletPermission) { - return db.insertInto("walletPermissions").values(fromDtoToDb(permission)).execute() -} - export function getPermission(id: string) { return db.selectFrom("walletPermissions").where("id", "=", id).selectAll().executeTakeFirst() } @@ -37,14 +36,26 @@ export async function listPermissions(user: Hex, lastUpdated?: number): Promise< return result.map(fromDbToDto) } -export async function updatePermission(permission: WalletPermission) { +export async function savePermission(permission: WalletPermissionUpdate) { + const existing = await getPermission(permission.id) + if (existing) { + return await db + .updateTable("walletPermissions") + .set(fromDtoToDbUpdate(permission)) + .where("id", "=", permission.id) + .execute() + } + return await db - .updateTable("walletPermissions") - .set(fromDtoToDb(permission)) - .where("id", "=", permission.id) + .insertInto("walletPermissions") + .values(fromDtoToDbUpdate(permission) as Insertable) .execute() } export async function deletePermission(id: string) { - return await db.deleteFrom("walletPermissions").where("id", "=", id).execute() + return await db + .updateTable("walletPermissions") + .set({ deleted: true, updatedAt: Date.now() }) + .where("id", "=", id) + .execute() } diff --git a/apps/sync-service/src/server/configRoute.ts b/apps/sync-service/src/server/configRoute.ts index 8d641de46b..ff4205e63b 100644 --- a/apps/sync-service/src/server/configRoute.ts +++ b/apps/sync-service/src/server/configRoute.ts @@ -1,9 +1,12 @@ import { Hono } from "hono" +import { stream, streamSSE } from "hono/streaming" import { createConfig } from "../handlers/createConfig/createConfig" import { createConfigDescription, createConfigValidation } from "../handlers/createConfig/validation" import { deleteConfig } from "../handlers/deleteConfig/deleteConfig" import { deleteConfigDescription, deleteConfigValidation } from "../handlers/deleteConfig/validation" import { listConfig, listConfigDescription, listConfigValidation } from "../handlers/listConfig" +import { subscribe } from "../handlers/subscribe/subscribe" +import { subscribeDescription, subscribeValidation } from "../handlers/subscribe/validation" import { updateConfig } from "../handlers/updateConfig/updateConfig" import { updateConfigDescription, updateConfigValidation } from "../handlers/updateConfig/validation" import { makeResponse } from "./makeResponse" @@ -37,3 +40,10 @@ export default new Hono() const [response, code] = makeResponse(result) return c.json(response, code) }) + .get("/subscribe", subscribeDescription, subscribeValidation, async (c) => { + c.header("Access-Control-Allow-Origin", "*") + const input = c.req.valid("query") + return streamSSE(c, async (s) => { + await subscribe(input, s) + }) + }) diff --git a/apps/sync-service/src/server/index.ts b/apps/sync-service/src/server/index.ts index ac973925a6..c4ae61c2da 100644 --- a/apps/sync-service/src/server/index.ts +++ b/apps/sync-service/src/server/index.ts @@ -26,9 +26,9 @@ app.use( }), ) app.use("*", timingMiddleware()) -app.use("*", loggerMiddleware()) -app.use("*", logJSONResponseMiddleware) -app.use("*", prettyJSONMiddleware()) +// app.use("*", loggerMiddleware()) +// app.use("*", logJSONResponseMiddleware) +// app.use("*", prettyJSONMiddleware()) app.use("*", timeoutMiddleware(30_000)) app.use("*", requestIdMiddleware()) diff --git a/apps/sync-service/src/services/notifyUpdates.ts b/apps/sync-service/src/services/notifyUpdates.ts new file mode 100644 index 0000000000..eaf032c449 --- /dev/null +++ b/apps/sync-service/src/services/notifyUpdates.ts @@ -0,0 +1,33 @@ +import type { Address } from "@happy.tech/common" +import type { SSEStreamingApi } from "hono/streaming" +import type { UpdateEvent } from "../dtos" + +const streams = new Map() + +export function notifyUpdates(event: UpdateEvent) { + const userStreams = streams.get(event.data.destination) + if (!userStreams) { + return + } + + for (const stream of userStreams) { + stream.writeSSE({ + data: JSON.stringify(event.data), + event: event.event, + id: event.id, + }) + } +} + +export function getStream(address: Address) { + return streams.get(address) +} + +export function saveStream(address: Address, stream: SSEStreamingApi) { + const userStreams = streams.get(address) + if (!userStreams) { + streams.set(address, [stream]) + } else { + userStreams.push(stream) + } +} \ No newline at end of file diff --git a/apps/sync-service/src/utils/logger.ts b/apps/sync-service/src/utils/logger.ts index 6c62cd82fd..5f8958bff4 100644 --- a/apps/sync-service/src/utils/logger.ts +++ b/apps/sync-service/src/utils/logger.ts @@ -13,6 +13,7 @@ export const logJSONResponseMiddleware = createMiddleware(async (c, next) => { if (LogLevel.TRACE > responseLogger.logLevel) return if (!c.req.path.startsWith("/api")) return + if (c.req.path.includes("/api/v1/settings/subscribe")) return try { responseLogger.trace(c.res.status, await c.res.clone().json()) } catch (e) { From ea5f15d47a8104de3f7357a169f9aa8a48565760 Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Mon, 23 Jun 2025 11:49:57 +0200 Subject: [PATCH 09/19] chore: syns service happy version dependency --- bun.lock | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/bun.lock b/bun.lock index f0744b4eae..f56bfbc624 100644 --- a/bun.lock +++ b/bun.lock @@ -914,7 +914,7 @@ "@floating-ui/core": ["@floating-ui/core@1.7.1", "", { "dependencies": { "@floating-ui/utils": "^0.2.9" } }, "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw=="], - "@floating-ui/dom": ["@floating-ui/dom@1.6.13", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w=="], + "@floating-ui/dom": ["@floating-ui/dom@1.7.1", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/utils": "^0.2.9" } }, "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ=="], "@floating-ui/react": ["@floating-ui/react@0.27.12", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.3", "@floating-ui/utils": "^0.2.9", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-kKlWNrpIQxF1B/a2MZvE0/uyKby4960yjO91W7nVyNKmmfNi62xU9HCjL1M1eWzx/LFj/VPSwJVbwQk9Pq/68A=="], @@ -4004,7 +4004,7 @@ "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], - "ripemd160": ["ripemd160@2.0.1", "", { "dependencies": { "hash-base": "^2.0.0", "inherits": "^2.0.1" } }, "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w=="], + "ripemd160": ["ripemd160@2.0.2", "", { "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" } }, "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA=="], "rlp": ["rlp@2.2.7", "", { "dependencies": { "bn.js": "^5.2.0" }, "bin": { "rlp": "bin/rlp" } }, "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ=="], @@ -4634,6 +4634,10 @@ "@ethersproject/providers/ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + "@firebase/component/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@firebase/logger/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@happy.tech/txm/kysely-bun-sqlite": ["kysely-bun-sqlite@0.4.0", "", { "dependencies": { "bun-types": "^1.1.31" }, "peerDependencies": { "kysely": "^0.28.2" } }, "sha512-2EkQE5sT4ewiw7IWfJsAkpxJ/QPVKXKO5sRYI/xjjJIJlECuOdtG+ssYM0twZJySrdrmuildNPFYVreyu1EdZg=="], "@humanwhocodes/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -4692,12 +4696,6 @@ "@microsoft/tsdoc-config/ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="], - "@motionone/easing/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@motionone/generators/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@motionone/utils/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@nomiclabs/hardhat-etherscan/cbor": ["cbor@5.2.0", "", { "dependencies": { "bignumber.js": "^9.0.1", "nofilter": "^1.0.4" } }, "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A=="], "@nomiclabs/hardhat-etherscan/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], @@ -4890,12 +4888,12 @@ "@web3auth/auth/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "@zag-js/popper/@floating-ui/dom": ["@floating-ui/dom@1.6.13", "", { "dependencies": { "@floating-ui/core": "^1.6.0", "@floating-ui/utils": "^0.2.9" } }, "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w=="], + "account-abstraction/typescript": ["typescript@4.9.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "aria-hidden/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "asn1.js/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], "ast-types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -4926,13 +4924,13 @@ "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "create-ecdh/bn.js": ["bn.js@4.11.6", "", {}, "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA=="], + "create-ecdh/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], "create-vocs/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="], "crypto-random-string/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], - "diffie-hellman/bn.js": ["bn.js@4.11.6", "", {}, "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA=="], + "diffie-hellman/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], "elliptic/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], @@ -5020,6 +5018,8 @@ "execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "extension-port-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "find-replace/array-back": ["array-back@1.0.4", "", { "dependencies": { "typical": "^2.6.0" } }, "sha512-1WxbZvrmyhkNoeYcizokbmh5oiOCIfyvGtcqbK3Ls1v1fKcquzxnQSceOx6tzq7jmai2kFLWIpGND2cLhH6TPw=="], @@ -5192,6 +5192,8 @@ "pbkdf2/create-hash": ["create-hash@1.1.3", "", { "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", "ripemd160": "^2.0.0", "sha.js": "^2.4.0" } }, "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA=="], + "pbkdf2/ripemd160": ["ripemd160@2.0.1", "", { "dependencies": { "hash-base": "^2.0.0", "inherits": "^2.0.1" } }, "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w=="], + "pkg-dir/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], "prebuild-install/detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], @@ -5200,7 +5202,7 @@ "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - "public-encrypt/bn.js": ["bn.js@4.11.6", "", {}, "sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA=="], + "public-encrypt/bn.js": ["bn.js@4.12.2", "", {}, "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw=="], "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], @@ -5208,8 +5210,6 @@ "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], - "react-remove-scroll/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "react-remove-scroll-bar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "react-style-singleton/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -5222,8 +5222,6 @@ "recast/esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "recast/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "recursive-readdir/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "rehype-autolink-headings/unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], @@ -5236,8 +5234,6 @@ "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "ripemd160/hash-base": ["hash-base@2.0.2", "", { "dependencies": { "inherits": "^2.0.1" } }, "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw=="], - "sc-istanbul/glob": ["glob@5.0.15", "", { "dependencies": { "inflight": "^1.0.4", "inherits": "2", "minimatch": "2 || 3", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA=="], "sc-istanbul/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], @@ -5594,6 +5590,8 @@ "ethers/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + "extension-port-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "ghost-testrpc/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "ghost-testrpc/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], @@ -5678,6 +5676,10 @@ "ora/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], + "pbkdf2/create-hash/ripemd160": ["ripemd160@2.0.2", "", { "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" } }, "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA=="], + + "pbkdf2/ripemd160/hash-base": ["hash-base@2.0.2", "", { "dependencies": { "inherits": "^2.0.1" } }, "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw=="], + "pkg-dir/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], From 36b6fee54301a012555035fb2a2c4154bd5eb039 Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Mon, 23 Jun 2025 14:21:53 +0200 Subject: [PATCH 10/19] chore: sync service sse --- apps/iframe/.env.example | 7 +++ apps/iframe/src/state/permissions.ts | 54 ++++++++----------- .../src/handlers/subscribe/subscribe.ts | 2 + apps/sync-service/src/index.ts | 1 + apps/sync-service/src/server/index.ts | 1 - apps/sync-service/src/utils/logger.ts | 4 +- 6 files changed, 34 insertions(+), 35 deletions(-) diff --git a/apps/iframe/.env.example b/apps/iframe/.env.example index 620c6bde58..dba3010382 100644 --- a/apps/iframe/.env.example +++ b/apps/iframe/.env.example @@ -88,6 +88,13 @@ VITE_FAUCET_ENDPOINT=https://faucet.testnet.happy.tech/faucet # Safe to publicize, this gets bundled in the client code served by the wallet. VITE_TURNSTILE_SITEKEY=0x4AAAAAABRnNdBbR6oFMviC +######################################################################################################################## +# SYNC SERVICE + +VITE_SYNC_SERVICE_URL=https://sync.testnet.happy.tech + + + ######################################################################################################################## # DEV UTILS diff --git a/apps/iframe/src/state/permissions.ts b/apps/iframe/src/state/permissions.ts index 9cac8acac5..fb5dd8de4e 100644 --- a/apps/iframe/src/state/permissions.ts +++ b/apps/iframe/src/state/permissions.ts @@ -10,6 +10,7 @@ import { emitUserUpdate } from "../utils/emitUserUpdate" import { revokedSessionKeys } from "./interfaceState" import { getUser } from "./user" import type { HappyUser } from "@happy.tech/wallet-common" +import { deploymentVar } from "#src/env.ts" // In EIP-2255, permissions define whether an app can make certain EIP-1193 requests to the wallets. // These permissions are scoped per app and per account. @@ -89,6 +90,9 @@ export type SessionKeyRequest = { */ export type PermissionsRequest = string | PermissionRequestObject + +const SYNC_SERVICE_URL = deploymentVar("VITE_SYNC_SERVICE_URL") + export const permissionsMapLegend = observable( syncedCrud({ list: async ({ lastSync }) => { @@ -96,14 +100,14 @@ export const permissionsMapLegend = observable( if (!user) return [] const response = await fetch( - `http://localhost:3000/api/v1/settings/list?user=${user.address}${lastSync ? `&lastUpdated=${lastSync}` : ""}`, + `${SYNC_SERVICE_URL}/api/v1/settings/list?user=${user.address}${lastSync ? `&lastUpdated=${lastSync}` : ""}`, ) const data = await response.json() return data.data as WalletPermission[] }, create: async (data: WalletPermission) => { - const response = await fetch("http://localhost:3000/api/v1/settings/create", { + const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/create`, { method: "POST", headers: { "Content-Type": "application/json", @@ -116,7 +120,7 @@ export const permissionsMapLegend = observable( const user = getUser() if (!user) return - const response = await fetch("http://localhost:3000/api/v1/settings/update", { + const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/update`, { method: "PUT", headers: { "Content-Type": "application/json", @@ -135,16 +139,27 @@ export const permissionsMapLegend = observable( console.log("Subscribing to updates for user", user.address) - const eventSource = new EventSource(`http://localhost:3000/api/v1/settings/subscribe?user=${user.address}`) - eventSource.addEventListener("update", (event) => { + const eventSource = new EventSource(`${SYNC_SERVICE_URL}/api/v1/settings/subscribe?user=${user.address}`) + eventSource.addEventListener("WalletPermissions.updated", (event) => { + const data = JSON.parse(event.data) + console.log("Received update", data) + refresh() + }) + eventSource.addEventListener("WalletPermissions.deleted", (event) => { + const data = JSON.parse(event.data) + console.log("Received update", data) + refresh() + }) + eventSource.addEventListener("WalletPermissions.created", (event) => { const data = JSON.parse(event.data) console.log("Received update", data) refresh() }) + return () => eventSource.close() }, delete: async ({ id }) => { - const response = await fetch("http://localhost:3000/api/v1/settings/delete", { + const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/delete`, { method: "DELETE", headers: { "Content-Type": "application/json", @@ -167,33 +182,6 @@ export const permissionsMapLegend = observable( }), ) -// // === RANDOM PERMISSION UPDATE SIMULATION ======================================================================== - -const mockPermissionUpdates = () => { - const permissions = permissionsMapLegend.get() - const permissionArray = Object.values(permissions) - - if (permissionArray.length === 0) return - - // Randomly select a permission to update - const randomIndex = Math.floor(Math.random() * permissionArray.length) - const permissionToUpdate = permissionArray[randomIndex] - - // Update the timestamp - const updatedPermission = { - ...permissionToUpdate, - caveats: [...permissionToUpdate.caveats, { type: "random", value: String(Math.random()) }], - updatedAt: Date.now() - } - - - // Update in the legend - permissionsMapLegend[permissionToUpdate.id].set(updatedPermission) -} - -// Set up interval for random updates every 5 seconds -setInterval(mockPermissionUpdates, 5000) - // === GET ALL PERMISSIONS ======================================================================================= diff --git a/apps/sync-service/src/handlers/subscribe/subscribe.ts b/apps/sync-service/src/handlers/subscribe/subscribe.ts index c4b2e13d54..49d7dc6c88 100644 --- a/apps/sync-service/src/handlers/subscribe/subscribe.ts +++ b/apps/sync-service/src/handlers/subscribe/subscribe.ts @@ -6,6 +6,8 @@ import type { SubscribeInput } from "./types" export async function subscribe(input: SubscribeInput, stream: SSEStreamingApi) { const {promise, reject } = promiseWithResolvers() + console.log("Subscribing to updates for user", input.user) + stream.onAbort(() => { reject(undefined) }) diff --git a/apps/sync-service/src/index.ts b/apps/sync-service/src/index.ts index ddfdd4cb7e..3c64da8b26 100644 --- a/apps/sync-service/src/index.ts +++ b/apps/sync-service/src/index.ts @@ -7,4 +7,5 @@ export type { AppType } export default { port: env.APP_PORT, fetch: app.fetch, + idleTimeout: 0 } diff --git a/apps/sync-service/src/server/index.ts b/apps/sync-service/src/server/index.ts index c4ae61c2da..72d0b19417 100644 --- a/apps/sync-service/src/server/index.ts +++ b/apps/sync-service/src/server/index.ts @@ -29,7 +29,6 @@ app.use("*", timingMiddleware()) // app.use("*", loggerMiddleware()) // app.use("*", logJSONResponseMiddleware) // app.use("*", prettyJSONMiddleware()) -app.use("*", timeoutMiddleware(30_000)) app.use("*", requestIdMiddleware()) // Routes setup diff --git a/apps/sync-service/src/utils/logger.ts b/apps/sync-service/src/utils/logger.ts index 5f8958bff4..05ba4280ab 100644 --- a/apps/sync-service/src/utils/logger.ts +++ b/apps/sync-service/src/utils/logger.ts @@ -6,7 +6,9 @@ const defaultLogLevel = logLevel(env.LOG_LEVEL) Logger.instance.setLogLevel(defaultLogLevel) export const logger = Logger.create("SettingsService") -const responseLogger = Logger.create("Response", LogLevel.TRACE) +const responseLogger = Logger.create("Response", { + level: LogLevel.TRACE, +}) export const logJSONResponseMiddleware = createMiddleware(async (c, next) => { await next() From 80abc18d55f0bd234e88db19f2e953051f781c2c Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Tue, 24 Jun 2025 12:18:29 +0200 Subject: [PATCH 11/19] chore: watch tokens --- .../tabs/views/tokens/RemoveTokenMenu.tsx | 5 +- .../home/tabs/views/tokens/TokenView.tsx | 10 +- .../home/tabs/views/tokens/WatchedAsset.tsx | 2 +- apps/iframe/src/requests/handlers/approved.ts | 2 +- apps/iframe/src/requests/handlers/injected.ts | 2 +- .../requests/tests/wallet_watchAsset.spec.ts | 6 +- apps/iframe/src/state/permissions.ts | 14 +- apps/iframe/src/state/watchedAssets.ts | 154 +++++++++++++----- .../db/migrations/Migration20250623143000.ts | 35 ++++ apps/sync-service/src/db/migrations/index.ts | 2 + apps/sync-service/src/db/types.ts | 15 ++ apps/sync-service/src/dtos.ts | 42 ++++- .../src/handlers/createConfig/createConfig.ts | 27 +-- .../src/handlers/createConfig/validation.ts | 5 +- .../src/handlers/deleteConfig/deleteConfig.ts | 31 ++-- .../src/handlers/listConfig/listConfig.ts | 17 +- .../src/handlers/listConfig/validation.ts | 5 +- .../src/handlers/subscribe/index.ts | 2 +- .../src/handlers/subscribe/subscribe.ts | 4 +- .../src/handlers/updateConfig/updateConfig.ts | 13 +- .../src/handlers/updateConfig/validation.ts | 6 +- apps/sync-service/src/index.ts | 2 +- .../src/repositories/watchAssetsRepository.ts | 73 +++++++++ apps/sync-service/src/server/configRoute.ts | 2 +- apps/sync-service/src/server/index.ts | 5 +- .../src/services/notifyUpdates.ts | 2 +- 26 files changed, 365 insertions(+), 118 deletions(-) create mode 100644 apps/sync-service/src/db/migrations/Migration20250623143000.ts create mode 100644 apps/sync-service/src/repositories/watchAssetsRepository.ts diff --git a/apps/iframe/src/components/interface/home/tabs/views/tokens/RemoveTokenMenu.tsx b/apps/iframe/src/components/interface/home/tabs/views/tokens/RemoveTokenMenu.tsx index 77895080fd..00cf083e48 100644 --- a/apps/iframe/src/components/interface/home/tabs/views/tokens/RemoveTokenMenu.tsx +++ b/apps/iframe/src/components/interface/home/tabs/views/tokens/RemoveTokenMenu.tsx @@ -9,11 +9,10 @@ enum TokenMenuActions { } interface RemoveTokensMenuProps { - user: Address token: Address } -const RemoveTokenMenu = ({ user, token }: RemoveTokensMenuProps) => { +const RemoveTokenMenu = ({ token }: RemoveTokensMenuProps) => { return ( @@ -31,7 +30,7 @@ const RemoveTokenMenu = ({ user, token }: RemoveTokensMenuProps) => { asChild className="text-primary dark:text-content cursor-pointer bg-primary/20 hover:bg-primary/30 dark:bg-primary/10 dark:hover:bg-primary/20 rounded-md p-1.5" value={TokenMenuActions.StopTracking} - onClick={() => removeWatchedAsset(user, token)} + onClick={() => removeWatchedAsset(token)} > {TokenMenuActions.StopTracking} diff --git a/apps/iframe/src/components/interface/home/tabs/views/tokens/TokenView.tsx b/apps/iframe/src/components/interface/home/tabs/views/tokens/TokenView.tsx index b0b4051c96..7b86511616 100644 --- a/apps/iframe/src/components/interface/home/tabs/views/tokens/TokenView.tsx +++ b/apps/iframe/src/components/interface/home/tabs/views/tokens/TokenView.tsx @@ -1,20 +1,20 @@ import { CoinsIcon } from "@phosphor-icons/react" import { useAtomValue } from "jotai" import { userAtom } from "#src/state/user" -import { watchedAssetsAtom } from "#src/state/watchedAssets" +import { getWatchedAssets } from "#src/state/watchedAssets" import { UserNotFoundWarning } from "../UserNotFoundWarning" import { TriggerImportTokensDialog } from "./ImportTokensDialog" import { WatchedAsset } from "./WatchedAsset" +import { observer } from "@legendapp/state/react" /** * Displays all watched assets registered by the connected user. */ -export const TokenView = () => { +export const TokenView = observer(() => { const user = useAtomValue(userAtom) - const watchedAssets = useAtomValue(watchedAssetsAtom) + const userAssets = getWatchedAssets() if (!user) return - const userAssets = watchedAssets[user.address] return (
    @@ -35,4 +35,4 @@ export const TokenView = () => {
) -} +}) diff --git a/apps/iframe/src/components/interface/home/tabs/views/tokens/WatchedAsset.tsx b/apps/iframe/src/components/interface/home/tabs/views/tokens/WatchedAsset.tsx index 5691b6ac84..00211dae79 100644 --- a/apps/iframe/src/components/interface/home/tabs/views/tokens/WatchedAsset.tsx +++ b/apps/iframe/src/components/interface/home/tabs/views/tokens/WatchedAsset.tsx @@ -84,7 +84,7 @@ export const WatchedAsset = ({ user, asset }: WatchedAssetProps) => { - + ) } diff --git a/apps/iframe/src/requests/handlers/approved.ts b/apps/iframe/src/requests/handlers/approved.ts index 609cae2b62..5d36840080 100644 --- a/apps/iframe/src/requests/handlers/approved.ts +++ b/apps/iframe/src/requests/handlers/approved.ts @@ -72,7 +72,7 @@ export async function dispatchApprovedRequest(request: PopupMsgs[Msgs.PopupAppro case "wallet_watchAsset": { const params = checkedWatchedAsset(request.payload.params) - return addWatchedAsset(user.address, params) + return addWatchedAsset(params) } case HappyMethodNames.LOAD_ABI: { diff --git a/apps/iframe/src/requests/handlers/injected.ts b/apps/iframe/src/requests/handlers/injected.ts index 64d22eef29..34b233c459 100644 --- a/apps/iframe/src/requests/handlers/injected.ts +++ b/apps/iframe/src/requests/handlers/injected.ts @@ -224,7 +224,7 @@ export async function dispatchInjectedRequest(request: ProviderMsgsFromApp[Msgs. case "wallet_watchAsset": { checkUser(user) const params = checkedWatchedAsset(request.payload.params) - return addWatchedAsset(user.address, params) + return addWatchedAsset(params) } case HappyMethodNames.LOAD_ABI: { diff --git a/apps/iframe/src/requests/tests/wallet_watchAsset.spec.ts b/apps/iframe/src/requests/tests/wallet_watchAsset.spec.ts index a66e1d20e7..e449bbc709 100644 --- a/apps/iframe/src/requests/tests/wallet_watchAsset.spec.ts +++ b/apps/iframe/src/requests/tests/wallet_watchAsset.spec.ts @@ -36,14 +36,12 @@ describe("walletClient wallet_watchAsset", () => { }) await dispatchApprovedRequest(request) - const userAssets = getWatchedAssets() - const assetsForAddress = userAssets[user.address] - expect(assetsForAddress.length).toBe(1) + expect(getWatchedAssets().length).toBe(1) // add the same token a second time, shouldn't add a new token but also returns true // since this isn't an error case const reAddTokenReq = await dispatchApprovedRequest(request) - expect(assetsForAddress.length).toBe(1) + expect(getWatchedAssets().length).toBe(1) expect(reAddTokenReq).toBe(true) }) }) diff --git a/apps/iframe/src/state/permissions.ts b/apps/iframe/src/state/permissions.ts index fb5dd8de4e..492ac68112 100644 --- a/apps/iframe/src/state/permissions.ts +++ b/apps/iframe/src/state/permissions.ts @@ -100,7 +100,7 @@ export const permissionsMapLegend = observable( if (!user) return [] const response = await fetch( - `${SYNC_SERVICE_URL}/api/v1/settings/list?user=${user.address}${lastSync ? `&lastUpdated=${lastSync}` : ""}`, + `${SYNC_SERVICE_URL}/api/v1/settings/list?type=WalletPermissions&user=${user.address}${lastSync ? `&lastUpdated=${lastSync}` : ""}`, ) const data = await response.json() @@ -140,17 +140,7 @@ export const permissionsMapLegend = observable( console.log("Subscribing to updates for user", user.address) const eventSource = new EventSource(`${SYNC_SERVICE_URL}/api/v1/settings/subscribe?user=${user.address}`) - eventSource.addEventListener("WalletPermissions.updated", (event) => { - const data = JSON.parse(event.data) - console.log("Received update", data) - refresh() - }) - eventSource.addEventListener("WalletPermissions.deleted", (event) => { - const data = JSON.parse(event.data) - console.log("Received update", data) - refresh() - }) - eventSource.addEventListener("WalletPermissions.created", (event) => { + eventSource.addEventListener("config.changed", (event) => { const data = JSON.parse(event.data) console.log("Received update", data) refresh() diff --git a/apps/iframe/src/state/watchedAssets.ts b/apps/iframe/src/state/watchedAssets.ts index 514dec66d0..00c7ee7e20 100644 --- a/apps/iframe/src/state/watchedAssets.ts +++ b/apps/iframe/src/state/watchedAssets.ts @@ -1,30 +1,111 @@ import type { Address } from "@happy.tech/common" import { getDefaultStore } from "jotai" import { atomWithStorage } from "jotai/utils" -import type { WatchAssetParameters } from "viem" +import type { WalletPermission, WatchAssetParameters } from "viem" import { StorageKey } from "#src/services/storage" +import { deploymentVar } from "#src/env.ts" +import { syncedCrud } from "@legendapp/state/sync-plugins/crud" +import { observable } from "@legendapp/state" +import { getUser } from "./user" +import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage" -export type UserWatchedAssetsRecord = Record +export type WatchedAsset = WatchAssetParameters & { + user: Address + id: string + createdAt: number + updatedAt: number + deleted: boolean +} -// === Atom Definition ================================================================================== -/** - * Atom to manage watched assets mapped to user's address, using localStorage. - */ -export const watchedAssetsAtom = atomWithStorage(StorageKey.WatchedAssets, {}, undefined, { - getOnInit: true, -}) +const SYNC_SERVICE_URL = deploymentVar("VITE_SYNC_SERVICE_URL") + +export const permissionsMapLegend = observable( + syncedCrud({ + list: async ({ lastSync }) => { + const user = getUser() + if (!user) return [] + + const response = await fetch( + `${SYNC_SERVICE_URL}/api/v1/settings/list?type=ERC20&user=${user.address}${lastSync ? `&lastUpdated=${lastSync}` : ""}`, + ) + const data = await response.json() + + return data.data as WatchedAsset[] + }, + create: async (data: WatchedAsset) => { + const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/create`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }) + await response.json() + }, + update: async (data: WatchedAsset) => { + const user = getUser() + if (!user) return + + const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/update`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...data, + type: "ERC20", + user: user.address, + }), + }) + await response.json() + }, + subscribe: ({ refresh }) => { + const user = getUser() + if (!user) return () => {} -// Store Instantiation -const store = getDefaultStore() + console.log("Subscribing to updates for user", user.address) + + const eventSource = new EventSource(`${SYNC_SERVICE_URL}/api/v1/settings/subscribe?user=${user.address}`) + eventSource.addEventListener("config.changed", (event) => { + const data = JSON.parse(event.data) + console.log("Received update", data) + refresh() + }) + + return () => eventSource.close() + }, + delete: async ({ id }) => { + const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/delete`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ id }), + }) + await response.json() + }, + persist: { + plugin: ObservablePersistLocalStorage, + name: "watched-assets-legend", + retrySync: true, // Retry sync after reload + }, + initial: {}, + fieldCreatedAt: "createdAt", + fieldUpdatedAt: "updatedAt", + fieldDeleted: "deleted", + changesSince: "last-sync", + updatePartial: true, + }), +) // === State Accessors ================================================================================== /** * Retrieves the current list of watched assets from the Jotai store. */ -export function getWatchedAssets(): UserWatchedAssetsRecord { - return store.get(watchedAssetsAtom) +export function getWatchedAssets(): WatchedAsset[] { + return Object.values(permissionsMapLegend.get()) } // === State Mutators =================================================================================== @@ -34,19 +115,19 @@ export function getWatchedAssets(): UserWatchedAssetsRecord { * If the asset does not already exist for the address, it is added. * Does nothing if the asset is already in the list. */ -export function addWatchedAsset(userAddress: Address, newAsset: WatchAssetParameters): boolean { - store.set(watchedAssetsAtom, (prevAssets) => { - const assetsForAddress = prevAssets[userAddress] || [] - const assetExists = assetsForAddress.some((asset) => asset.options.address === newAsset.options.address) - - return assetExists - ? prevAssets - : { - ...prevAssets, - [userAddress]: assetsForAddress.concat(newAsset), - } - }) +export function addWatchedAsset(newAsset: WatchAssetParameters): boolean { + const user = getUser() + if (!user) return false + const asset: WatchedAsset = { + ...newAsset, + user: user.address, + id: `${user.address}-${newAsset.options.address}`, + createdAt: Date.now(), + updatedAt: Date.now(), + deleted: false, + } + permissionsMapLegend[asset.id].set(asset) return true } @@ -54,22 +135,9 @@ export function addWatchedAsset(userAddress: Address, newAsset: WatchAssetParame * Removes a specific asset from the watched assets list by its contract address for a specific user. * Returns `true` if the asset was found and removed, or `false` if it was not in the list. */ -export function removeWatchedAsset(userAddress: Address, assetAddress: Address): boolean { - let assetRemoved = false - store.set(watchedAssetsAtom, (prevAssets) => { - const assetsForAddress = prevAssets[userAddress] || [] - const updatedAssets = assetsForAddress.filter((asset) => asset.options.address !== assetAddress) - assetRemoved = updatedAssets.length < assetsForAddress.length - - if (updatedAssets.length === 0) { - const { [userAddress]: _, ...remainingAssets } = prevAssets - return remainingAssets - } - - return { - ...prevAssets, - [userAddress]: updatedAssets, - } - }) - return assetRemoved +export function removeWatchedAsset(assetAddress: Address): boolean { + const asset = Object.values(permissionsMapLegend.get()).find((asset) => asset.options.address === assetAddress) + if (!asset) return false + permissionsMapLegend[asset.id].delete() + return true } diff --git a/apps/sync-service/src/db/migrations/Migration20250623143000.ts b/apps/sync-service/src/db/migrations/Migration20250623143000.ts new file mode 100644 index 0000000000..027f72c763 --- /dev/null +++ b/apps/sync-service/src/db/migrations/Migration20250623143000.ts @@ -0,0 +1,35 @@ +import type { Kysely } from "kysely" +import type { Database } from "../types" + +export type WatchAssetParams = { + /** Token type. */ + type: "ERC20" + options: { + /** The address of the token contract */ + address: string + /** A ticker symbol or shorthand, up to 11 characters */ + symbol: string + /** The number of token decimals */ + decimals: number + /** A string url of the token logo */ + image?: string | undefined + } +} + +export async function up(db: Kysely) { + await db.schema + .createTable("watchedAssets") + .addColumn("user", "text", (col) => col.notNull()) + .addColumn("type", "text", (col) => col.notNull()) + .addColumn("address", "text", (col) => col.notNull()) + .addColumn("symbol", "text", (col) => col.notNull()) + .addColumn("decimals", "integer", (col) => col.notNull()) + .addColumn("image", "text") + .addColumn("id", "text", (col) => col.notNull().primaryKey()) + .addColumn("updatedAt", "integer", (col) => col.notNull()) + .addColumn("createdAt", "integer", (col) => col.notNull()) + .addColumn("deleted", "boolean", (col) => col.notNull()) + .execute() +} + +export const migration20250623143000 = { up } diff --git a/apps/sync-service/src/db/migrations/index.ts b/apps/sync-service/src/db/migrations/index.ts index 8349d3b15f..fbb32073b9 100644 --- a/apps/sync-service/src/db/migrations/index.ts +++ b/apps/sync-service/src/db/migrations/index.ts @@ -1,5 +1,7 @@ import { migration20250515123000 } from "./Migration20250515123000" +import { migration20250623143000 } from "./Migration20250623143000" export const migrations = { "20250515123000": migration20250515123000, + "20250623143000": migration20250623143000, } diff --git a/apps/sync-service/src/db/types.ts b/apps/sync-service/src/db/types.ts index a9692cbe3a..152a28a742 100644 --- a/apps/sync-service/src/db/types.ts +++ b/apps/sync-service/src/db/types.ts @@ -33,6 +33,21 @@ export type WalletPermisisonRow = { deleted: ColumnType } +export type WatchAssetRow = { + user: Hex + type: string + address: Hex + symbol: string + decimals: number + image: string + id: string + updatedAt: number + createdAt: number + deleted: ColumnType +} + + export interface Database { walletPermissions: WalletPermisisonRow + watchedAssets: WatchAssetRow } diff --git a/apps/sync-service/src/dtos.ts b/apps/sync-service/src/dtos.ts index 9f6eff470f..d5b9b8d29d 100644 --- a/apps/sync-service/src/dtos.ts +++ b/apps/sync-service/src/dtos.ts @@ -44,7 +44,7 @@ export const walletPermissionUpdate = walletPermission.partial().extend({ export type WalletPermissionUpdate = z.infer export const updateEvent = z.object({ - event: z.enum(["WalletPermissions.updated", "WalletPermissions.deleted", "WalletPermissions.created"]), + event: z.enum(["config.changed"]), data: z.object({ destination: z.string().refine(isAddress).transform(checksum).openapi({ example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", @@ -57,3 +57,43 @@ export const updateEvent = z.object({ }) export type UpdateEvent = z.infer + +export const watchAsset = z.object({ + type: z.literal("ERC20").openapi({ + example: "ERC20", + type: "string", + }), + user: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + options: z.object({ + address: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + symbol: z.string().openapi({ example: "ETH" }), + decimals: z.number().openapi({ example: 18 }), + image: z.string().optional().openapi({ example: "https://example.com/logo.png" }), + }), + id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), + updatedAt: z.number().openapi({ example: 1715702400 }), + createdAt: z.number().openapi({ example: 1715702400 }), + deleted: z.boolean().openapi({ example: false }), +}) + +export type WatchAsset = z.infer + +export const watchAssetUpdate = watchAsset.partial().extend({ + type: z.literal("ERC20").openapi({ + example: "ERC20", + type: "string", + }), + user: z.string().refine(isAddress).transform(checksum).openapi({ + example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + type: "string", + }), + id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }) +}) + +export type WatchAssetUpdate = z.infer \ No newline at end of file diff --git a/apps/sync-service/src/handlers/createConfig/createConfig.ts b/apps/sync-service/src/handlers/createConfig/createConfig.ts index 1b0fb8e10c..a723d736e4 100644 --- a/apps/sync-service/src/handlers/createConfig/createConfig.ts +++ b/apps/sync-service/src/handlers/createConfig/createConfig.ts @@ -1,25 +1,28 @@ import { type Result, ok } from "neverthrow" +import { createUUID } from "../../../../../support/common/dist/index.es" import { savePermission } from "../../repositories/permissionsRepository" -import type { CreateConfigInput } from "./types" import { notifyUpdates } from "../../services/notifyUpdates" -import { createUUID } from "../../../../../support/common/dist/index.es" +import type { CreateConfigInput } from "./types" +import { saveWatchedAsset } from "../../repositories/watchAssetsRepository" export async function createConfig(input: CreateConfigInput): Promise> { console.log(input) if (input.type === "WalletPermissions") { await savePermission(input) - - notifyUpdates({ - event: "WalletPermissions.created", - data: { - destination: input.user, - resourceId: input.id, - updatedAt: input.updatedAt, - }, - id: createUUID(), - }) + } else if (input.type === "ERC20") { + await saveWatchedAsset(input) } + notifyUpdates({ + event: "config.changed", + data: { + destination: input.user, + resourceId: input.id, + updatedAt: input.updatedAt, + }, + id: createUUID(), + }) + return ok(undefined) } diff --git a/apps/sync-service/src/handlers/createConfig/validation.ts b/apps/sync-service/src/handlers/createConfig/validation.ts index 4aff719b9c..c80a4c2f60 100644 --- a/apps/sync-service/src/handlers/createConfig/validation.ts +++ b/apps/sync-service/src/handlers/createConfig/validation.ts @@ -2,11 +2,12 @@ import { describeRoute } from "hono-openapi" import { resolver } from "hono-openapi/zod" import { validator as zv } from "hono-openapi/zod" import { z } from "zod" -import { walletPermission } from "../../dtos" +import { walletPermission, watchAsset } from "../../dtos" import { isProduction } from "../../utils/isProduction" -export const inputSchema: z.ZodDiscriminatedUnion<"type", [typeof walletPermission]> = z.discriminatedUnion("type", [ +export const inputSchema: z.ZodDiscriminatedUnion<"type", [typeof walletPermission, typeof watchAsset]> = z.discriminatedUnion("type", [ walletPermission, + watchAsset, ]) export const outputSchema = z.object({ diff --git a/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts b/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts index b9c08adf13..2cc768a99b 100644 --- a/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts +++ b/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts @@ -1,24 +1,35 @@ -import { type Result, ok } from "neverthrow" +import { createUUID, type Address } from "@happy.tech/common" +import { type Result, err, ok } from "neverthrow" import { deletePermission, getPermission } from "../../repositories/permissionsRepository" -import type { DeleteConfigInput } from "./types" -import { createUUID } from "@happy.tech/common" import { notifyUpdates } from "../../services/notifyUpdates" +import type { DeleteConfigInput } from "./types" +import { deleteWatchedAsset, getWatchedAsset } from "../../repositories/watchAssetsRepository" export async function deleteConfig(input: DeleteConfigInput): Promise> { + + const permission = await getPermission(input.id) + const watchedAsset = await getWatchedAsset(input.id) - if (!permission) { - return ok(undefined) - } - await deletePermission(input.id) + let user: Address + if (permission) { + await deletePermission(input.id) + user = permission.user + } + else if (watchedAsset) { + await deleteWatchedAsset(input.id) + user = watchedAsset.user + } else { + return err(new Error("Config not found")) + } notifyUpdates({ - event: "WalletPermissions.deleted", + event: "config.changed", data: { - destination: permission.user, + destination: user, resourceId: input.id, - updatedAt: permission.updatedAt, + updatedAt: Date.now(), }, id: createUUID(), }) diff --git a/apps/sync-service/src/handlers/listConfig/listConfig.ts b/apps/sync-service/src/handlers/listConfig/listConfig.ts index 60046583be..91b47c985d 100644 --- a/apps/sync-service/src/handlers/listConfig/listConfig.ts +++ b/apps/sync-service/src/handlers/listConfig/listConfig.ts @@ -1,10 +1,19 @@ import { type Result, ok } from "neverthrow" -import type { WalletPermission } from "../../dtos" +import type { WalletPermission, WatchAsset } from "../../dtos" import { listPermissions } from "../../repositories/permissionsRepository" +import { listWatchedAssets } from "../../repositories/watchAssetsRepository" import type { ListConfigInput } from "./types" -export async function listConfig(input: ListConfigInput): Promise> { - const permissions = await listPermissions(input.user, input.lastUpdated) +export async function listConfig(input: ListConfigInput): Promise> { + const config: (WalletPermission | WatchAsset)[] = [] + if (input.type === "WalletPermissions" || input.type === undefined) { + const permissions = await listPermissions(input.user, input.lastUpdated) + config.push(...permissions) + } + if (input.type === "ERC20" || input.type === undefined) { + const watchedAssets = await listWatchedAssets(input.user, input.lastUpdated) + config.push(...watchedAssets) + } - return ok(permissions) + return ok(config) } diff --git a/apps/sync-service/src/handlers/listConfig/validation.ts b/apps/sync-service/src/handlers/listConfig/validation.ts index 2f437e0f61..767a4ef6dd 100644 --- a/apps/sync-service/src/handlers/listConfig/validation.ts +++ b/apps/sync-service/src/handlers/listConfig/validation.ts @@ -4,7 +4,7 @@ import { resolver } from "hono-openapi/zod" import { validator as zv } from "hono-openapi/zod" import { checksum } from "ox/Address" import { z } from "zod" -import { walletPermission } from "../../dtos" +import { walletPermission, watchAsset } from "../../dtos" import { isProduction } from "../../utils/isProduction" export const listConfigSchema = z @@ -21,6 +21,7 @@ export const listConfigSchema = z example: "1715702400", type: "number", }), + type: z.enum(["WalletPermissions", "ERC20"]).optional(), }) .strict() @@ -29,7 +30,7 @@ export const inputSchema = listConfigSchema export const outputSchema = z.object({ success: z.boolean(), message: z.string().optional(), - data: z.array(walletPermission), + data: z.array(z.discriminatedUnion("type", [walletPermission, watchAsset])), }) export const listConfigDescription = describeRoute({ diff --git a/apps/sync-service/src/handlers/subscribe/index.ts b/apps/sync-service/src/handlers/subscribe/index.ts index f92f39a368..26760afc10 100644 --- a/apps/sync-service/src/handlers/subscribe/index.ts +++ b/apps/sync-service/src/handlers/subscribe/index.ts @@ -1,2 +1,2 @@ export { subscribe } from "./subscribe" -export { subscribeValidation, subscribeDescription } from "./validation" \ No newline at end of file +export { subscribeValidation, subscribeDescription } from "./validation" diff --git a/apps/sync-service/src/handlers/subscribe/subscribe.ts b/apps/sync-service/src/handlers/subscribe/subscribe.ts index 49d7dc6c88..e15b12bbfd 100644 --- a/apps/sync-service/src/handlers/subscribe/subscribe.ts +++ b/apps/sync-service/src/handlers/subscribe/subscribe.ts @@ -1,10 +1,10 @@ -import type { SSEStreamingApi } from "hono/streaming" import { promiseWithResolvers } from "@happy.tech/common" +import type { SSEStreamingApi } from "hono/streaming" import { saveStream } from "../../services/notifyUpdates" import type { SubscribeInput } from "./types" export async function subscribe(input: SubscribeInput, stream: SSEStreamingApi) { - const {promise, reject } = promiseWithResolvers() + const { promise, reject } = promiseWithResolvers() console.log("Subscribing to updates for user", input.user) diff --git a/apps/sync-service/src/handlers/updateConfig/updateConfig.ts b/apps/sync-service/src/handlers/updateConfig/updateConfig.ts index ff33e1d326..fd3ddb7568 100644 --- a/apps/sync-service/src/handlers/updateConfig/updateConfig.ts +++ b/apps/sync-service/src/handlers/updateConfig/updateConfig.ts @@ -3,16 +3,21 @@ import { type Result, ok } from "neverthrow" import { savePermission } from "../../repositories/permissionsRepository" import { notifyUpdates } from "../../services/notifyUpdates" import type { UpdateConfigInput } from "./types" +import { saveWatchedAsset } from "../../repositories/watchAssetsRepository" export async function updateConfig(input: UpdateConfigInput): Promise> { - await savePermission(input) + if (input.type === "WalletPermissions") { + await savePermission(input) + } else if (input.type === "ERC20") { + await saveWatchedAsset(input) + } notifyUpdates({ - event: "WalletPermissions.updated", + event: "config.changed", data: { destination: input.user, - resourceId: createUUID(), - updatedAt: input.updatedAt ?? Date.now(), + resourceId: input.id, + updatedAt: Date.now(), }, id: input.id, }) diff --git a/apps/sync-service/src/handlers/updateConfig/validation.ts b/apps/sync-service/src/handlers/updateConfig/validation.ts index a6b98c7a2b..24cc551e07 100644 --- a/apps/sync-service/src/handlers/updateConfig/validation.ts +++ b/apps/sync-service/src/handlers/updateConfig/validation.ts @@ -2,12 +2,12 @@ import { describeRoute } from "hono-openapi" import { resolver } from "hono-openapi/zod" import { validator as zv } from "hono-openapi/zod" import { z } from "zod" -import { walletPermissionUpdate } from "../../dtos" +import { walletPermissionUpdate, watchAssetUpdate } from "../../dtos" import { isProduction } from "../../utils/isProduction" -export const inputSchema: z.ZodDiscriminatedUnion<"type", [typeof walletPermissionUpdate]> = z.discriminatedUnion( +export const inputSchema: z.ZodDiscriminatedUnion<"type", [typeof walletPermissionUpdate, typeof watchAssetUpdate]> = z.discriminatedUnion( "type", - [walletPermissionUpdate], + [walletPermissionUpdate, watchAssetUpdate], ) export const outputSchema = z.object({ diff --git a/apps/sync-service/src/index.ts b/apps/sync-service/src/index.ts index 3c64da8b26..350b0475d6 100644 --- a/apps/sync-service/src/index.ts +++ b/apps/sync-service/src/index.ts @@ -7,5 +7,5 @@ export type { AppType } export default { port: env.APP_PORT, fetch: app.fetch, - idleTimeout: 0 + idleTimeout: 0, } diff --git a/apps/sync-service/src/repositories/watchAssetsRepository.ts b/apps/sync-service/src/repositories/watchAssetsRepository.ts new file mode 100644 index 0000000000..8ad7d1cdf7 --- /dev/null +++ b/apps/sync-service/src/repositories/watchAssetsRepository.ts @@ -0,0 +1,73 @@ +import type { Hex } from "@happy.tech/common" +import type { Insertable, Selectable } from "kysely" +import { db } from "../db/driver" +import type { WatchAssetRow } from "../db/types" +import type { WatchAsset, WatchAssetUpdate } from "../dtos" + +function fromDtoToDbUpdate(watchedAsset: WatchAssetUpdate): Partial> { + const { options, ...rest } = watchedAsset + return { + ...rest, + ...(options ?? {}), + updatedAt: Date.now(), + } +} + +function fromDbToDto(watchedAsset: Selectable): WatchAsset { + return { + type: "ERC20", + options: { + symbol: watchedAsset.symbol, + address: watchedAsset.address, + decimals: watchedAsset.decimals, + image: watchedAsset.image ?? undefined, + }, + user: watchedAsset.user, + id: watchedAsset.id, + updatedAt: watchedAsset.updatedAt, + createdAt: watchedAsset.createdAt, + deleted: watchedAsset.deleted === 1, + } +} + + +export function getWatchedAsset(id: string) { + return db.selectFrom("watchedAssets").where("id", "=", id).selectAll().executeTakeFirst() +} + +export async function listWatchedAssets(user: Hex, lastUpdated?: number): Promise { + const result = await db + .selectFrom("watchedAssets") + .where("user", "=", user) + .$if(lastUpdated !== undefined, (qb) => qb.where("updatedAt", ">", lastUpdated as number)) + .selectAll() + .execute() + + console.log("listWatchedAssets", { user, lastUpdated, result }) + + return result.map(fromDbToDto) +} + +export async function saveWatchedAsset(watchedAsset: WatchAssetUpdate) { + const existing = await getWatchedAsset(watchedAsset.id) + if (existing) { + return await db + .updateTable("watchedAssets") + .set(fromDtoToDbUpdate(watchedAsset)) + .where("id", "=", watchedAsset.id) + .execute() + } + + return await db + .insertInto("watchedAssets") + .values(fromDtoToDbUpdate(watchedAsset) as Insertable) + .execute() +} + +export async function deleteWatchedAsset(id: string) { + return await db + .updateTable("watchedAssets") + .set({ deleted: true, updatedAt: Date.now() }) + .where("id", "=", id) + .execute() +} diff --git a/apps/sync-service/src/server/configRoute.ts b/apps/sync-service/src/server/configRoute.ts index ff4205e63b..f19b5dd54a 100644 --- a/apps/sync-service/src/server/configRoute.ts +++ b/apps/sync-service/src/server/configRoute.ts @@ -1,5 +1,5 @@ import { Hono } from "hono" -import { stream, streamSSE } from "hono/streaming" +import { streamSSE } from "hono/streaming" import { createConfig } from "../handlers/createConfig/createConfig" import { createConfigDescription, createConfigValidation } from "../handlers/createConfig/validation" import { deleteConfig } from "../handlers/deleteConfig/deleteConfig" diff --git a/apps/sync-service/src/server/index.ts b/apps/sync-service/src/server/index.ts index 72d0b19417..c338d59c64 100644 --- a/apps/sync-service/src/server/index.ts +++ b/apps/sync-service/src/server/index.ts @@ -4,16 +4,13 @@ import { Hono } from "hono" import { openAPISpecs } from "hono-openapi" import { cors } from "hono/cors" import { HTTPException } from "hono/http-exception" -import { logger as loggerMiddleware } from "hono/logger" -import { prettyJSON as prettyJSONMiddleware } from "hono/pretty-json" import { requestId as requestIdMiddleware } from "hono/request-id" -import { timeout as timeoutMiddleware } from "hono/timeout" import { timing as timingMiddleware } from "hono/timing" import { ZodError } from "zod" import pkg from "../../package.json" assert { type: "json" } import { env } from "../env" import { isProduction } from "../utils/isProduction" -import { logJSONResponseMiddleware, logger } from "../utils/logger" +import { logger } from "../utils/logger" import configRoute from "./configRoute" const app = new Hono() diff --git a/apps/sync-service/src/services/notifyUpdates.ts b/apps/sync-service/src/services/notifyUpdates.ts index eaf032c449..3b543e2939 100644 --- a/apps/sync-service/src/services/notifyUpdates.ts +++ b/apps/sync-service/src/services/notifyUpdates.ts @@ -30,4 +30,4 @@ export function saveStream(address: Address, stream: SSEStreamingApi) { } else { userStreams.push(stream) } -} \ No newline at end of file +} From f62149dcbb03f4567e5507e9139220adbda196d9 Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Wed, 25 Jun 2025 16:13:29 +0200 Subject: [PATCH 12/19] chore(sync-service): deploy --- .../workflows/deploy-staging-sync-service.yml | 66 +++++++ .github/workflows/deploy-sync-service.yml | 66 +++++++ Makefile | 4 + .../permissions/ClearAllAppsPermissions.tsx | 3 +- .../permissions/ListAppsWithPermissions.tsx | 2 +- .../permissions/ListSingleAppPermissions.tsx | 2 +- .../permissions/useAppsWithPermissions.ts | 3 +- apps/iframe/src/hooks/useHasPermissions.ts | 3 +- apps/iframe/src/listeners/atoms.ts | 2 +- .../{permissions.ts => permissions/index.ts} | 177 +----------------- .../src/state/permissions/observable.ts | 84 +++++++++ .../{ => permissions}/permissions.spec.ts | 2 +- apps/iframe/src/state/permissions/types.ts | 82 ++++++++ apps/iframe/src/state/watchedAssets/index.ts | 49 +++++ .../observable.ts} | 68 +------ apps/iframe/src/state/watchedAssets/types.ts | 10 + apps/sync-service/src/db/types.ts | 5 +- apps/sync-service/src/dtos.ts | 8 +- apps/sync-service/src/env.ts | 2 +- .../src/handlers/createConfig/createConfig.ts | 6 +- .../src/handlers/createConfig/validation.ts | 6 +- .../src/handlers/deleteConfig/deleteConfig.ts | 10 +- .../src/handlers/subscribe/subscribe.ts | 2 - .../src/handlers/updateConfig/updateConfig.ts | 3 +- .../src/handlers/updateConfig/validation.ts | 6 +- .../src/repositories/permissionsRepository.ts | 8 +- .../src/repositories/watchAssetsRepository.ts | 4 - apps/sync-service/src/server/index.ts | 9 +- .../src/services/notifyUpdates.ts | 4 +- 29 files changed, 412 insertions(+), 284 deletions(-) create mode 100644 .github/workflows/deploy-staging-sync-service.yml create mode 100644 .github/workflows/deploy-sync-service.yml rename apps/iframe/src/state/{permissions.ts => permissions/index.ts} (70%) create mode 100644 apps/iframe/src/state/permissions/observable.ts rename apps/iframe/src/state/{ => permissions}/permissions.spec.ts (99%) create mode 100644 apps/iframe/src/state/permissions/types.ts create mode 100644 apps/iframe/src/state/watchedAssets/index.ts rename apps/iframe/src/state/{watchedAssets.ts => watchedAssets/observable.ts} (56%) create mode 100644 apps/iframe/src/state/watchedAssets/types.ts diff --git a/.github/workflows/deploy-staging-sync-service.yml b/.github/workflows/deploy-staging-sync-service.yml new file mode 100644 index 0000000000..bab70a87dd --- /dev/null +++ b/.github/workflows/deploy-staging-sync-service.yml @@ -0,0 +1,66 @@ +name: Deploy staging sync service + +on: + workflow_dispatch: + pull_request: + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + + - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + with: + bun-version: '1.2.4' + + - name: Build code + run: | + make sync-service.build + + - name: Copy files to server + uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SERVER_KEY }} + port: ${{ secrets.SERVER_PORT }} + source: "apps/sync-service/build/*" + target: /home/deployer/staging_sync_service + strip_components: 2 + rm: true + debug: true + + - name: Deploy staging sync service to server + uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SERVER_KEY }} + port: ${{ secrets.SERVER_PORT }} + script_stop: true + script: | + chmod -R o+rX /home/deployer/staging_sync_service + rm -rf /tmp/staging_sync_service + mv /home/deployer/staging_sync_service /tmp + sudo -u staging_sync_service bash -c ' + rm -rf /home/staging_sync_service/build + cp -r /tmp/staging_sync_service/* /home/staging_sync_service/build + + cat > /home/staging_sync_service/build/.env <<-EOF + NODE_ENV=production + APP_PORT=49534 + SETTINGS_DB_URL=settings.sqlite + EOF + # cf. https://github.com/appleboy/drone-ssh/issues/175 + sed -i '/DRONE_SSH_PREV_COMMAND_EXIT_CODE/d' /home/staging_sync_service/build/.env + cd /home/staging_sync_service/build + bun migrate.es.js + ' + rm -rf /tmp/staging_sync_service + sudo /bin/systemctl restart staging-sync.service diff --git a/.github/workflows/deploy-sync-service.yml b/.github/workflows/deploy-sync-service.yml new file mode 100644 index 0000000000..3b95d5f2f5 --- /dev/null +++ b/.github/workflows/deploy-sync-service.yml @@ -0,0 +1,66 @@ +name: Deploy staging sync service + +on: + workflow_dispatch: + pull_request: + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + + - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 + with: + bun-version: '1.2.4' + + - name: Build code + run: | + make sync-service.build + + - name: Copy files to server + uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SERVER_KEY }} + port: ${{ secrets.SERVER_PORT }} + source: "apps/sync-service/build/*" + target: /home/deployer/sync_service + strip_components: 2 + rm: true + debug: true + + - name: Deploy staging sync service to server + uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + key: ${{ secrets.SERVER_KEY }} + port: ${{ secrets.SERVER_PORT }} + script_stop: true + script: | + chmod -R o+rX /home/deployer/sync_service + rm -rf /tmp/sync_service + mv /home/deployer/sync_service /tmp + sudo -u sync_service bash -c ' + rm -rf /home/sync_service/build + cp -r /tmp/sync_service/* /home/sync_service/build + + cat > /home/sync_service/build/.env <<-EOF + NODE_ENV=production + APP_PORT=49535 + SETTINGS_DB_URL=settings.sqlite + EOF + # cf. https://github.com/appleboy/drone-ssh/issues/175 + sed -i '/DRONE_SSH_PREV_COMMAND_EXIT_CODE/d' /home/sync_service/build/.env + cd /home/sync_service/build + bun migrate.es.js + ' + rm -rf /tmp/sync_service + sudo /bin/systemctl restart sync.service diff --git a/Makefile b/Makefile index 221a427958..63cb3cb683 100644 --- a/Makefile +++ b/Makefile @@ -415,6 +415,10 @@ monitor-service.build: setup shared.build submitter.build cd apps/monitor-service && make build .PHONY: monitor-service.build +sync-service.build: setup shared.build + cd apps/sync-service && make build +.PHONY: sync-service.build + # ================================================================================================== ##@ Docs diff --git a/apps/iframe/src/components/interface/permissions/ClearAllAppsPermissions.tsx b/apps/iframe/src/components/interface/permissions/ClearAllAppsPermissions.tsx index ae7c614c5a..00f2bea78e 100644 --- a/apps/iframe/src/components/interface/permissions/ClearAllAppsPermissions.tsx +++ b/apps/iframe/src/components/interface/permissions/ClearAllAppsPermissions.tsx @@ -1,7 +1,8 @@ import { useCollapsible } from "@ark-ui/react" import { Button } from "#src/components/primitives/button/Button" import { InlineDrawer } from "#src/components/primitives/collapsible/InlineDrawer" -import { type AppPermissions, clearAppPermissions } from "#src/state/permissions" +import { clearAppPermissions } from "#src/state/permissions" +import type { AppPermissions } from "#src/state/permissions/types" import type { AppURL } from "#src/utils/appURL" interface ClearAllDappsPermissionsProps { diff --git a/apps/iframe/src/components/interface/permissions/ListAppsWithPermissions.tsx b/apps/iframe/src/components/interface/permissions/ListAppsWithPermissions.tsx index ced5f39b68..e93637de65 100644 --- a/apps/iframe/src/components/interface/permissions/ListAppsWithPermissions.tsx +++ b/apps/iframe/src/components/interface/permissions/ListAppsWithPermissions.tsx @@ -1,7 +1,7 @@ import { CaretRightIcon } from "@phosphor-icons/react" import { Link } from "@tanstack/react-router" import { useState } from "react" -import type { AppPermissions } from "#src/state/permissions" +import type { AppPermissions } from "#src/state/permissions/types" import { getAppURL } from "#src/utils/appURL" interface ListItemProps { diff --git a/apps/iframe/src/components/interface/permissions/ListSingleAppPermissions.tsx b/apps/iframe/src/components/interface/permissions/ListSingleAppPermissions.tsx index 3e25c18cfb..62588d7201 100644 --- a/apps/iframe/src/components/interface/permissions/ListSingleAppPermissions.tsx +++ b/apps/iframe/src/components/interface/permissions/ListSingleAppPermissions.tsx @@ -4,7 +4,7 @@ import { Switch } from "#src/components/primitives/toggle-switch/Switch" import { PermissionName } from "#src/constants/permissions" import { type PermissionDescriptionIndex, permissionDescriptions } from "#src/constants/requestLabels" import { useLocalPermissionChanges } from "#src/hooks/useLocalPermissionChanges" -import type { AppPermissions, PermissionsRequest, WalletPermission } from "#src/state/permissions" +import type { AppPermissions, WalletPermission, PermissionsRequest } from "#src/state/permissions" import type { AppURL } from "#src/utils/appURL" import { SessionKeyCheckbox } from "./SessionKeyCheckbox" diff --git a/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts b/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts index f3e5d9b945..c7f5ba45c3 100644 --- a/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts +++ b/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts @@ -1,5 +1,6 @@ import { use$ } from "@legendapp/state/react" -import { type AppPermissions, permissionsMapLegend } from "#src/state/permissions" +import type { AppPermissions } from "#src/state/permissions/types" +import { permissionsMapLegend } from "#src/state/permissions/observable" import { type AppURL, isWallet } from "#src/utils/appURL" export function useAppsWithPermissions(): [AppURL, AppPermissions][] { diff --git a/apps/iframe/src/hooks/useHasPermissions.ts b/apps/iframe/src/hooks/useHasPermissions.ts index 3bc8dadc95..4fc67e070c 100644 --- a/apps/iframe/src/hooks/useHasPermissions.ts +++ b/apps/iframe/src/hooks/useHasPermissions.ts @@ -1,5 +1,6 @@ import { use$ } from "@legendapp/state/react" -import { type PermissionsRequest, hasPermissions } from "../state/permissions" +import type { PermissionsRequest } from "#src/state/permissions/types" +import { hasPermissions } from "#src/state/permissions" import { type AppURL, getAppURL } from "../utils/appURL" export function useHasPermissions(permissionsRequest: PermissionsRequest, app: AppURL = getAppURL()) { diff --git a/apps/iframe/src/listeners/atoms.ts b/apps/iframe/src/listeners/atoms.ts index 2e43b085a9..2e82c4a70f 100644 --- a/apps/iframe/src/listeners/atoms.ts +++ b/apps/iframe/src/listeners/atoms.ts @@ -2,7 +2,7 @@ import { Msgs } from "@happy.tech/wallet-common" import { getDefaultStore } from "jotai/vanilla" import { http, createPublicClient } from "viem" import { mainnet } from "viem/chains" -import { permissionsMapLegend } from "#src/state/permissions.ts" +import { permissionsMapLegend } from "#src/state/permissions/observable" import { appMessageBus } from "../services/eventBus" import { authStateAtom } from "../state/authState" import { userAtom } from "../state/user" diff --git a/apps/iframe/src/state/permissions.ts b/apps/iframe/src/state/permissions/index.ts similarity index 70% rename from apps/iframe/src/state/permissions.ts rename to apps/iframe/src/state/permissions/index.ts index 492ac68112..4bb7d75efe 100644 --- a/apps/iframe/src/state/permissions.ts +++ b/apps/iframe/src/state/permissions/index.ts @@ -1,177 +1,14 @@ import type { Address } from "@happy.tech/common" import { permissionsLogger } from "#src/utils/logger" -import { observable } from "@legendapp/state" -import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage" -import { syncedCrud } from "@legendapp/state/sync-plugins/crud" import { PermissionName } from "#src/constants/permissions" -import { type AppURL, getWalletURL, isApp, isStandaloneWallet } from "../utils/appURL" -import { checkIfCaveatsMatch } from "../utils/checkIfCaveatsMatch" -import { emitUserUpdate } from "../utils/emitUserUpdate" -import { revokedSessionKeys } from "./interfaceState" -import { getUser } from "./user" +import { type AppURL, getWalletURL, isApp, isStandaloneWallet } from "#src/utils/appURL" +import { checkIfCaveatsMatch } from "#src/utils/checkIfCaveatsMatch" +import { emitUserUpdate } from "#src/utils/emitUserUpdate" +import { revokedSessionKeys } from "#src/state/interfaceState" +import { getUser } from "#src/state/user" +import { permissionsMapLegend } from "./observable" +import type { AppPermissions, WalletPermission, WalletPermissionCaveat, PermissionsRequest } from "./types" import type { HappyUser } from "@happy.tech/wallet-common" -import { deploymentVar } from "#src/env.ts" - -// In EIP-2255, permissions define whether an app can make certain EIP-1193 requests to the wallets. -// These permissions are scoped per app and per account. -// -// The system is not widely adopted and mostly wallet only handles the `eth_accounts` permission, -// which defines whether an app can get the user's account(s) and subsequently make other requests -// (some of which will require confirmations, like `eth_sendTransaction`, some of which won't like -// `eth_call`). -// -// Like other wallets, we only handle the `eth_accounts` permission, but we support processing -// all incoming permission requests. -// -// References: -// https://eips.ethereum.org/EIPS/eip-2255 - -/** - * Maps an user + app pair to a {@link AppPermissions}, which is the set of permissions - * for that user on that app. - */ -export type PermissionsMap = Record> - -/** - * Maps permissions names to permission objects. - * EIP-2255 specifies that permissions names must be EIP-1193 request names (e.g. `eth_accounts`). - * However, we type this as a string in case we want to extend the permission system to other - * names that do not map to a request (or are custom requests). - */ -export type AppPermissions = Record - -/** - * Permission object for a specific permission. - * - * This type is copied from Viem (eip1193.ts) - */ -export type WalletPermission = { - type: "WalletPermissions" - // The user to which the permission is granted. - user: Address - // The app to which the permission is granted. - invoker: AppURL - // This is the EIP-1193 request that this permission is mapped to. - parentCapability: PermissionName | (string & {}) - caveats: WalletPermissionCaveat[] - date: number - // Not in the EIP, but Viem wants this. - id: string - updatedAt: number - createdAt: number - deleted: boolean -} - -/** - * A caveat is a specific specific restrictions applied to the permitted request. - */ -type WalletPermissionCaveat = { - type: string - value: unknown -} - -/** - * A request for one or more permissions. - */ -export type PermissionRequestObject = { - [requestName: string]: { [caveatName: string]: unknown } -} - -/** - * A refinement of {@link PermissionRequestObject} for requesting session keys. - */ -export type SessionKeyRequest = { - [PermissionName.SessionKey]: { target: Address } -} - -/** - * A permissions specifier, which can be either a single EIP-1193 request name, or a {@link - * PermissionRequestObject}. - */ -export type PermissionsRequest = string | PermissionRequestObject - - -const SYNC_SERVICE_URL = deploymentVar("VITE_SYNC_SERVICE_URL") - -export const permissionsMapLegend = observable( - syncedCrud({ - list: async ({ lastSync }) => { - const user = getUser() - if (!user) return [] - - const response = await fetch( - `${SYNC_SERVICE_URL}/api/v1/settings/list?type=WalletPermissions&user=${user.address}${lastSync ? `&lastUpdated=${lastSync}` : ""}`, - ) - const data = await response.json() - - return data.data as WalletPermission[] - }, - create: async (data: WalletPermission) => { - const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/create`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }) - await response.json() - }, - update: async (data: WalletPermission) => { - const user = getUser() - if (!user) return - - const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/update`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - ...data, - type: "WalletPermissions", - user: user.address, - }), - }) - await response.json() - }, - subscribe: ({ refresh }) => { - const user = getUser() - if (!user) return () => {} - - console.log("Subscribing to updates for user", user.address) - - const eventSource = new EventSource(`${SYNC_SERVICE_URL}/api/v1/settings/subscribe?user=${user.address}`) - eventSource.addEventListener("config.changed", (event) => { - const data = JSON.parse(event.data) - console.log("Received update", data) - refresh() - }) - - return () => eventSource.close() - }, - delete: async ({ id }) => { - const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/delete`, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ id }), - }) - await response.json() - }, - persist: { - plugin: ObservablePersistLocalStorage, - name: "config-legend", - retrySync: true, // Retry sync after reload - }, - initial: {}, - fieldCreatedAt: "createdAt", - fieldUpdatedAt: "updatedAt", - fieldDeleted: "deleted", - changesSince: "last-sync", - updatePartial: true, - }), -) - // === GET ALL PERMISSIONS ======================================================================================= diff --git a/apps/iframe/src/state/permissions/observable.ts b/apps/iframe/src/state/permissions/observable.ts new file mode 100644 index 0000000000..da4c345835 --- /dev/null +++ b/apps/iframe/src/state/permissions/observable.ts @@ -0,0 +1,84 @@ +import { deploymentVar } from "#src/env.ts" +import { observable } from "@legendapp/state" +import { syncedCrud } from "@legendapp/state/sync-plugins/crud" +import { getUser } from "../user" +import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage" +import type { WalletPermission } from "./types" + +const SYNC_SERVICE_URL = deploymentVar("VITE_SYNC_SERVICE_URL") + +export const permissionsMapLegend = observable( + syncedCrud({ + list: async ({ lastSync }) => { + const user = getUser() + if (!user) return [] + + const response = await fetch( + `${SYNC_SERVICE_URL}/api/v1/settings/list?type=WalletPermissions&user=${user.address}${lastSync ? `&lastUpdated=${lastSync}` : ""}`, + ) + const data = await response.json() + + return data.data as WalletPermission[] + }, + create: async (data: WalletPermission) => { + const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/create`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }) + await response.json() + }, + update: async (data: WalletPermission) => { + const user = getUser() + if (!user) return + + const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/update`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...data, + type: "WalletPermissions", + user: user.address, + }), + }) + await response.json() + }, + subscribe: ({ refresh }) => { + const user = getUser() + if (!user) return + const eventSource = new EventSource(`${SYNC_SERVICE_URL}/api/v1/settings/subscribe?user=${user.address}`) + eventSource.addEventListener("config.changed", (event) => { + const data = JSON.parse(event.data) + console.log("Received update", data) + refresh() + }) + + return () => eventSource.close() + }, + delete: async ({ id }) => { + const response = await fetch(`${SYNC_SERVICE_URL}/api/v1/settings/delete`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ id }), + }) + await response.json() + }, + persist: { + plugin: ObservablePersistLocalStorage, + name: "config-legend", + retrySync: true, // Retry sync after reload + }, + initial: {}, + fieldCreatedAt: "createdAt", + fieldUpdatedAt: "updatedAt", + fieldDeleted: "deleted", + changesSince: "last-sync", + updatePartial: true, + }), +) diff --git a/apps/iframe/src/state/permissions.spec.ts b/apps/iframe/src/state/permissions/permissions.spec.ts similarity index 99% rename from apps/iframe/src/state/permissions.spec.ts rename to apps/iframe/src/state/permissions/permissions.spec.ts index 2aa84cf12f..c5006fa206 100644 --- a/apps/iframe/src/state/permissions.spec.ts +++ b/apps/iframe/src/state/permissions/permissions.spec.ts @@ -10,7 +10,7 @@ import { revokePermissions, } from "#src/state/permissions" import { disablePermissionWarnings } from "#src/testing/utils" -import { setUser } from "../state/user" +import { setUser } from "#src/state/user" const { appURL, walletURL, appURLMock } = await vi // .hoisted(async () => await import("#src/testing/cross_origin.mocks")) diff --git a/apps/iframe/src/state/permissions/types.ts b/apps/iframe/src/state/permissions/types.ts new file mode 100644 index 0000000000..716b75d87e --- /dev/null +++ b/apps/iframe/src/state/permissions/types.ts @@ -0,0 +1,82 @@ +import type { Address } from "@happy.tech/common" +import type { AppURL } from "#src/utils/appURL" +import type { Permissions } from "#src/constants/permissions" + +// In EIP-2255, permissions define whether an app can make certain EIP-1193 requests to the wallets. +// These permissions are scoped per app and per account. +// +// The system is not widely adopted and mostly wallet only handles the `eth_accounts` permission, +// which defines whether an app can get the user's account(s) and subsequently make other requests +// (some of which will require confirmations, like `eth_sendTransaction`, some of which won't like +// `eth_call`). +// +// Like other wallets, we only handle the `eth_accounts` permission, but we support processing +// all incoming permission requests. +// +// References: +// https://eips.ethereum.org/EIPS/eip-2255 + +/** + * Maps an user + app pair to a {@link AppPermissions}, which is the set of permissions + * for that user on that app. + */ +export type PermissionsMap = Record> + +/** + * Maps permissions names to permission objects. + * EIP-2255 specifies that permissions names must be EIP-1193 request names (e.g. `eth_accounts`). + * However, we type this as a string in case we want to extend the permission system to other + * names that do not map to a request (or are custom requests). + */ +export type AppPermissions = Record + +/** + * Permission object for a specific permission. + * + * This type is copied from Viem (eip1193.ts) + */ +export type WalletPermission = { + type: "WalletPermissions" + // The user to which the permission is granted. + user: Address + // The app to which the permission is granted. + invoker: AppURL + // This is the EIP-1193 request that this permission is mapped to. + parentCapability: Permissions | (string & {}) + caveats: WalletPermissionCaveat[] + date: number + // Not in the EIP, but Viem wants this. + id: string + // Required by the sync service. + updatedAt: number + createdAt: number + deleted: boolean +} + +/** + * A caveat is a specific specific restrictions applied to the permitted request. + */ +export type WalletPermissionCaveat = { + type: string + value: unknown +} + +/** + * A request for one or more permissions. + */ +export type PermissionRequestObject = { + [requestName: string]: { [caveatName: string]: unknown } +} + +/** + * A refinement of {@link PermissionRequestObject} for requesting session keys. + */ +export type SessionKeyRequest = { + [Permissions.SessionKey]: { target: Address } +} + +/** + * A permissions specifier, which can be either a single EIP-1193 request name, or a {@link + * PermissionRequestObject}. + */ +export type PermissionsRequest = string | PermissionRequestObject \ No newline at end of file diff --git a/apps/iframe/src/state/watchedAssets/index.ts b/apps/iframe/src/state/watchedAssets/index.ts new file mode 100644 index 0000000000..210b7f093c --- /dev/null +++ b/apps/iframe/src/state/watchedAssets/index.ts @@ -0,0 +1,49 @@ +import type { Address } from "@happy.tech/common" +import type { WatchAssetParameters } from "viem" +import { getUser } from "#src/state/user" +import { watchedAssetsMapLegend } from "./observable" +import type { WatchedAsset } from "./types" + + +// === State Accessors ================================================================================== + +/** + * Retrieves the current list of watched assets from the Jotai store. + */ +export function getWatchedAssets(): WatchedAsset[] { + return Object.values(watchedAssetsMapLegend.get()) +} + +// === State Mutators =================================================================================== + +/** + * Adds a new asset to the store under the provided address. + * If the asset does not already exist for the address, it is added. + * Does nothing if the asset is already in the list. + */ +export function addWatchedAsset(newAsset: WatchAssetParameters): boolean { + const user = getUser() + if (!user) return false + + const asset: WatchedAsset = { + ...newAsset, + user: user.address, + id: `${user.address}-${newAsset.options.address}`, + createdAt: Date.now(), + updatedAt: Date.now(), + deleted: false, + } + watchedAssetsMapLegend[asset.id].set(asset) + return true +} + +/** + * Removes a specific asset from the watched assets list by its contract address for a specific user. + * Returns `true` if the asset was found and removed, or `false` if it was not in the list. + */ +export function removeWatchedAsset(assetAddress: Address): boolean { + const asset = Object.values(watchedAssetsMapLegend.get()).find((asset) => asset.options.address === assetAddress) + if (!asset) return false + watchedAssetsMapLegend[asset.id].delete() + return true +} diff --git a/apps/iframe/src/state/watchedAssets.ts b/apps/iframe/src/state/watchedAssets/observable.ts similarity index 56% rename from apps/iframe/src/state/watchedAssets.ts rename to apps/iframe/src/state/watchedAssets/observable.ts index 00c7ee7e20..356e76e98b 100644 --- a/apps/iframe/src/state/watchedAssets.ts +++ b/apps/iframe/src/state/watchedAssets/observable.ts @@ -1,26 +1,13 @@ -import type { Address } from "@happy.tech/common" -import { getDefaultStore } from "jotai" -import { atomWithStorage } from "jotai/utils" -import type { WalletPermission, WatchAssetParameters } from "viem" -import { StorageKey } from "#src/services/storage" +import { observable } from "@legendapp/state" +import { getUser } from "../user" import { deploymentVar } from "#src/env.ts" import { syncedCrud } from "@legendapp/state/sync-plugins/crud" -import { observable } from "@legendapp/state" -import { getUser } from "./user" +import type { WatchedAsset } from "./types" import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage" -export type WatchedAsset = WatchAssetParameters & { - user: Address - id: string - createdAt: number - updatedAt: number - deleted: boolean -} - - const SYNC_SERVICE_URL = deploymentVar("VITE_SYNC_SERVICE_URL") -export const permissionsMapLegend = observable( +export const watchedAssetsMapLegend = observable( syncedCrud({ list: async ({ lastSync }) => { const user = getUser() @@ -67,9 +54,7 @@ export const permissionsMapLegend = observable( console.log("Subscribing to updates for user", user.address) const eventSource = new EventSource(`${SYNC_SERVICE_URL}/api/v1/settings/subscribe?user=${user.address}`) - eventSource.addEventListener("config.changed", (event) => { - const data = JSON.parse(event.data) - console.log("Received update", data) + eventSource.addEventListener("config.changed", () => { refresh() }) @@ -98,46 +83,3 @@ export const permissionsMapLegend = observable( updatePartial: true, }), ) - -// === State Accessors ================================================================================== - -/** - * Retrieves the current list of watched assets from the Jotai store. - */ -export function getWatchedAssets(): WatchedAsset[] { - return Object.values(permissionsMapLegend.get()) -} - -// === State Mutators =================================================================================== - -/** - * Adds a new asset to the store under the provided address. - * If the asset does not already exist for the address, it is added. - * Does nothing if the asset is already in the list. - */ -export function addWatchedAsset(newAsset: WatchAssetParameters): boolean { - const user = getUser() - if (!user) return false - - const asset: WatchedAsset = { - ...newAsset, - user: user.address, - id: `${user.address}-${newAsset.options.address}`, - createdAt: Date.now(), - updatedAt: Date.now(), - deleted: false, - } - permissionsMapLegend[asset.id].set(asset) - return true -} - -/** - * Removes a specific asset from the watched assets list by its contract address for a specific user. - * Returns `true` if the asset was found and removed, or `false` if it was not in the list. - */ -export function removeWatchedAsset(assetAddress: Address): boolean { - const asset = Object.values(permissionsMapLegend.get()).find((asset) => asset.options.address === assetAddress) - if (!asset) return false - permissionsMapLegend[asset.id].delete() - return true -} diff --git a/apps/iframe/src/state/watchedAssets/types.ts b/apps/iframe/src/state/watchedAssets/types.ts new file mode 100644 index 0000000000..1eecd8a961 --- /dev/null +++ b/apps/iframe/src/state/watchedAssets/types.ts @@ -0,0 +1,10 @@ +import type { Address } from "@happy.tech/common" +import type { WatchAssetParameters } from "viem" + +export type WatchedAsset = WatchAssetParameters & { + user: Address + id: string + createdAt: number + updatedAt: number + deleted: boolean +} diff --git a/apps/sync-service/src/db/types.ts b/apps/sync-service/src/db/types.ts index 152a28a742..7024ee3f6b 100644 --- a/apps/sync-service/src/db/types.ts +++ b/apps/sync-service/src/db/types.ts @@ -17,7 +17,7 @@ type WalletPermissionCaveat = { * * This type is copied from Viem (eip1193.ts) but we add a user field. */ -export type WalletPermisisonRow = { +export type WalletPermissionRow = { // The user to which the permission is granted. user: Hex // The app to which the permission is granted. @@ -46,8 +46,7 @@ export type WatchAssetRow = { deleted: ColumnType } - export interface Database { - walletPermissions: WalletPermisisonRow + walletPermissions: WalletPermissionRow watchedAssets: WatchAssetRow } diff --git a/apps/sync-service/src/dtos.ts b/apps/sync-service/src/dtos.ts index d5b9b8d29d..1384af5f0a 100644 --- a/apps/sync-service/src/dtos.ts +++ b/apps/sync-service/src/dtos.ts @@ -43,7 +43,7 @@ export const walletPermissionUpdate = walletPermission.partial().extend({ export type WalletPermissionUpdate = z.infer -export const updateEvent = z.object({ +export const configChangedEvent = z.object({ event: z.enum(["config.changed"]), data: z.object({ destination: z.string().refine(isAddress).transform(checksum).openapi({ @@ -56,7 +56,7 @@ export const updateEvent = z.object({ id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), }) -export type UpdateEvent = z.infer +export type ConfigChangedEvent = z.infer export const watchAsset = z.object({ type: z.literal("ERC20").openapi({ @@ -93,7 +93,7 @@ export const watchAssetUpdate = watchAsset.partial().extend({ example: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", type: "string", }), - id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }) + id: z.string().openapi({ example: "78b7d642-e851-4f0f-9cd6-a47c6c2a572a" }), }) -export type WatchAssetUpdate = z.infer \ No newline at end of file +export type WatchAssetUpdate = z.infer diff --git a/apps/sync-service/src/env.ts b/apps/sync-service/src/env.ts index ba78181839..6aa45a7646 100644 --- a/apps/sync-service/src/env.ts +++ b/apps/sync-service/src/env.ts @@ -1,7 +1,7 @@ import { z } from "zod" const envSchema = z.object({ - NODE_ENV: z.enum(["development", "production"]), + NODE_ENV: z.enum(["development", "production", "staging"]), APP_PORT: z.string().transform((s) => Number(s)), SETTINGS_DB_URL: z.string().trim(), LOG_LEVEL: z.preprocess( diff --git a/apps/sync-service/src/handlers/createConfig/createConfig.ts b/apps/sync-service/src/handlers/createConfig/createConfig.ts index a723d736e4..83e00d6828 100644 --- a/apps/sync-service/src/handlers/createConfig/createConfig.ts +++ b/apps/sync-service/src/handlers/createConfig/createConfig.ts @@ -1,13 +1,11 @@ +import { createUUID } from "@happy.tech/common" import { type Result, ok } from "neverthrow" -import { createUUID } from "../../../../../support/common/dist/index.es" import { savePermission } from "../../repositories/permissionsRepository" +import { saveWatchedAsset } from "../../repositories/watchAssetsRepository" import { notifyUpdates } from "../../services/notifyUpdates" import type { CreateConfigInput } from "./types" -import { saveWatchedAsset } from "../../repositories/watchAssetsRepository" export async function createConfig(input: CreateConfigInput): Promise> { - console.log(input) - if (input.type === "WalletPermissions") { await savePermission(input) } else if (input.type === "ERC20") { diff --git a/apps/sync-service/src/handlers/createConfig/validation.ts b/apps/sync-service/src/handlers/createConfig/validation.ts index c80a4c2f60..344fe75c6c 100644 --- a/apps/sync-service/src/handlers/createConfig/validation.ts +++ b/apps/sync-service/src/handlers/createConfig/validation.ts @@ -5,10 +5,8 @@ import { z } from "zod" import { walletPermission, watchAsset } from "../../dtos" import { isProduction } from "../../utils/isProduction" -export const inputSchema: z.ZodDiscriminatedUnion<"type", [typeof walletPermission, typeof watchAsset]> = z.discriminatedUnion("type", [ - walletPermission, - watchAsset, -]) +export const inputSchema: z.ZodDiscriminatedUnion<"type", [typeof walletPermission, typeof watchAsset]> = + z.discriminatedUnion("type", [walletPermission, watchAsset]) export const outputSchema = z.object({ success: z.boolean(), diff --git a/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts b/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts index 2cc768a99b..66e35800e3 100644 --- a/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts +++ b/apps/sync-service/src/handlers/deleteConfig/deleteConfig.ts @@ -1,23 +1,19 @@ -import { createUUID, type Address } from "@happy.tech/common" +import { type Address, createUUID } from "@happy.tech/common" import { type Result, err, ok } from "neverthrow" import { deletePermission, getPermission } from "../../repositories/permissionsRepository" +import { deleteWatchedAsset, getWatchedAsset } from "../../repositories/watchAssetsRepository" import { notifyUpdates } from "../../services/notifyUpdates" import type { DeleteConfigInput } from "./types" -import { deleteWatchedAsset, getWatchedAsset } from "../../repositories/watchAssetsRepository" export async function deleteConfig(input: DeleteConfigInput): Promise> { - - const permission = await getPermission(input.id) const watchedAsset = await getWatchedAsset(input.id) - let user: Address if (permission) { await deletePermission(input.id) user = permission.user - } - else if (watchedAsset) { + } else if (watchedAsset) { await deleteWatchedAsset(input.id) user = watchedAsset.user } else { diff --git a/apps/sync-service/src/handlers/subscribe/subscribe.ts b/apps/sync-service/src/handlers/subscribe/subscribe.ts index e15b12bbfd..cbc422ede7 100644 --- a/apps/sync-service/src/handlers/subscribe/subscribe.ts +++ b/apps/sync-service/src/handlers/subscribe/subscribe.ts @@ -6,8 +6,6 @@ import type { SubscribeInput } from "./types" export async function subscribe(input: SubscribeInput, stream: SSEStreamingApi) { const { promise, reject } = promiseWithResolvers() - console.log("Subscribing to updates for user", input.user) - stream.onAbort(() => { reject(undefined) }) diff --git a/apps/sync-service/src/handlers/updateConfig/updateConfig.ts b/apps/sync-service/src/handlers/updateConfig/updateConfig.ts index fd3ddb7568..6ae2753217 100644 --- a/apps/sync-service/src/handlers/updateConfig/updateConfig.ts +++ b/apps/sync-service/src/handlers/updateConfig/updateConfig.ts @@ -1,9 +1,8 @@ -import { createUUID } from "@happy.tech/common" import { type Result, ok } from "neverthrow" import { savePermission } from "../../repositories/permissionsRepository" +import { saveWatchedAsset } from "../../repositories/watchAssetsRepository" import { notifyUpdates } from "../../services/notifyUpdates" import type { UpdateConfigInput } from "./types" -import { saveWatchedAsset } from "../../repositories/watchAssetsRepository" export async function updateConfig(input: UpdateConfigInput): Promise> { if (input.type === "WalletPermissions") { diff --git a/apps/sync-service/src/handlers/updateConfig/validation.ts b/apps/sync-service/src/handlers/updateConfig/validation.ts index 24cc551e07..971ddab2d8 100644 --- a/apps/sync-service/src/handlers/updateConfig/validation.ts +++ b/apps/sync-service/src/handlers/updateConfig/validation.ts @@ -5,10 +5,8 @@ import { z } from "zod" import { walletPermissionUpdate, watchAssetUpdate } from "../../dtos" import { isProduction } from "../../utils/isProduction" -export const inputSchema: z.ZodDiscriminatedUnion<"type", [typeof walletPermissionUpdate, typeof watchAssetUpdate]> = z.discriminatedUnion( - "type", - [walletPermissionUpdate, watchAssetUpdate], -) +export const inputSchema: z.ZodDiscriminatedUnion<"type", [typeof walletPermissionUpdate, typeof watchAssetUpdate]> = + z.discriminatedUnion("type", [walletPermissionUpdate, watchAssetUpdate]) export const outputSchema = z.object({ success: z.boolean(), diff --git a/apps/sync-service/src/repositories/permissionsRepository.ts b/apps/sync-service/src/repositories/permissionsRepository.ts index 8abafb3933..1b18b1235c 100644 --- a/apps/sync-service/src/repositories/permissionsRepository.ts +++ b/apps/sync-service/src/repositories/permissionsRepository.ts @@ -1,10 +1,10 @@ import type { Hex } from "@happy.tech/common" import type { Insertable, Selectable } from "kysely" import { db } from "../db/driver" -import type { WalletPermisisonRow } from "../db/types" +import type { WalletPermissionRow } from "../db/types" import type { WalletPermission, WalletPermissionUpdate } from "../dtos" -function fromDtoToDbUpdate(permission: WalletPermissionUpdate): Partial> { +function fromDtoToDbUpdate(permission: WalletPermissionUpdate): Partial> { const { type, caveats, ...rest } = permission return { ...rest, @@ -13,7 +13,7 @@ function fromDtoToDbUpdate(permission: WalletPermissionUpdate): Partial): WalletPermission { +function fromDbToDto(permission: Selectable): WalletPermission { return { type: "WalletPermissions", ...permission, @@ -48,7 +48,7 @@ export async function savePermission(permission: WalletPermissionUpdate) { return await db .insertInto("walletPermissions") - .values(fromDtoToDbUpdate(permission) as Insertable) + .values(fromDtoToDbUpdate(permission) as Insertable) .execute() } diff --git a/apps/sync-service/src/repositories/watchAssetsRepository.ts b/apps/sync-service/src/repositories/watchAssetsRepository.ts index 8ad7d1cdf7..6582d216f0 100644 --- a/apps/sync-service/src/repositories/watchAssetsRepository.ts +++ b/apps/sync-service/src/repositories/watchAssetsRepository.ts @@ -30,7 +30,6 @@ function fromDbToDto(watchedAsset: Selectable): WatchAsset { } } - export function getWatchedAsset(id: string) { return db.selectFrom("watchedAssets").where("id", "=", id).selectAll().executeTakeFirst() } @@ -42,9 +41,6 @@ export async function listWatchedAssets(user: Hex, lastUpdated?: number): Promis .$if(lastUpdated !== undefined, (qb) => qb.where("updatedAt", ">", lastUpdated as number)) .selectAll() .execute() - - console.log("listWatchedAssets", { user, lastUpdated, result }) - return result.map(fromDbToDto) } diff --git a/apps/sync-service/src/server/index.ts b/apps/sync-service/src/server/index.ts index c338d59c64..03f4d1c8a2 100644 --- a/apps/sync-service/src/server/index.ts +++ b/apps/sync-service/src/server/index.ts @@ -4,6 +4,8 @@ import { Hono } from "hono" import { openAPISpecs } from "hono-openapi" import { cors } from "hono/cors" import { HTTPException } from "hono/http-exception" +import { logger as loggerMiddleware } from "hono/logger" +import { prettyJSON as prettyJSONMiddleware } from "hono/pretty-json" import { requestId as requestIdMiddleware } from "hono/request-id" import { timing as timingMiddleware } from "hono/timing" import { ZodError } from "zod" @@ -11,6 +13,7 @@ import pkg from "../../package.json" assert { type: "json" } import { env } from "../env" import { isProduction } from "../utils/isProduction" import { logger } from "../utils/logger" +import { logJSONResponseMiddleware } from "../utils/logger" import configRoute from "./configRoute" const app = new Hono() @@ -23,10 +26,10 @@ app.use( }), ) app.use("*", timingMiddleware()) -// app.use("*", loggerMiddleware()) -// app.use("*", logJSONResponseMiddleware) -// app.use("*", prettyJSONMiddleware()) +app.use("*", logJSONResponseMiddleware) +app.use("*", prettyJSONMiddleware()) app.use("*", requestIdMiddleware()) +app.use("*", loggerMiddleware()) // Routes setup app.get("/", (c) => c.text("Welcome to the Settings Service!")) diff --git a/apps/sync-service/src/services/notifyUpdates.ts b/apps/sync-service/src/services/notifyUpdates.ts index 3b543e2939..daccbb543d 100644 --- a/apps/sync-service/src/services/notifyUpdates.ts +++ b/apps/sync-service/src/services/notifyUpdates.ts @@ -1,10 +1,10 @@ import type { Address } from "@happy.tech/common" import type { SSEStreamingApi } from "hono/streaming" -import type { UpdateEvent } from "../dtos" +import type { ConfigChangedEvent } from "../dtos" const streams = new Map() -export function notifyUpdates(event: UpdateEvent) { +export function notifyUpdates(event: ConfigChangedEvent) { const userStreams = streams.get(event.data.destination) if (!userStreams) { return From 4a093ba7b591c27982f18abd4215acd47059053a Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Thu, 26 Jun 2025 10:37:06 +0200 Subject: [PATCH 13/19] chore: force gh actions --- .github/workflows/deploy-staging-sync-service.yml | 1 + .github/workflows/deploy-sync-service.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/deploy-staging-sync-service.yml b/.github/workflows/deploy-staging-sync-service.yml index bab70a87dd..a8793877c9 100644 --- a/.github/workflows/deploy-staging-sync-service.yml +++ b/.github/workflows/deploy-staging-sync-service.yml @@ -22,6 +22,7 @@ jobs: - name: Build code run: | make sync-service.build + - name: Copy files to server uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7 diff --git a/.github/workflows/deploy-sync-service.yml b/.github/workflows/deploy-sync-service.yml index 3b95d5f2f5..4819080334 100644 --- a/.github/workflows/deploy-sync-service.yml +++ b/.github/workflows/deploy-sync-service.yml @@ -23,6 +23,7 @@ jobs: run: | make sync-service.build + - name: Copy files to server uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7 with: From a55239065725c842b3594306fb654fc3873246f8 Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Thu, 26 Jun 2025 10:37:29 +0200 Subject: [PATCH 14/19] chore: format --- .../interface/home/tabs/views/tokens/TokenView.tsx | 2 +- .../interface/permissions/ListSingleAppPermissions.tsx | 3 ++- .../interface/permissions/useAppsWithPermissions.ts | 2 +- apps/iframe/src/hooks/useHasPermissions.ts | 2 +- apps/iframe/src/state/permissions/index.ts | 6 +++--- apps/iframe/src/state/permissions/observable.ts | 6 +++--- apps/iframe/src/state/permissions/permissions.spec.ts | 2 +- apps/iframe/src/state/permissions/types.ts | 4 ++-- apps/iframe/src/state/watchedAssets/index.ts | 1 - apps/iframe/src/state/watchedAssets/observable.ts | 8 ++++---- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/apps/iframe/src/components/interface/home/tabs/views/tokens/TokenView.tsx b/apps/iframe/src/components/interface/home/tabs/views/tokens/TokenView.tsx index 7b86511616..fccb0d5eff 100644 --- a/apps/iframe/src/components/interface/home/tabs/views/tokens/TokenView.tsx +++ b/apps/iframe/src/components/interface/home/tabs/views/tokens/TokenView.tsx @@ -1,3 +1,4 @@ +import { observer } from "@legendapp/state/react" import { CoinsIcon } from "@phosphor-icons/react" import { useAtomValue } from "jotai" import { userAtom } from "#src/state/user" @@ -5,7 +6,6 @@ import { getWatchedAssets } from "#src/state/watchedAssets" import { UserNotFoundWarning } from "../UserNotFoundWarning" import { TriggerImportTokensDialog } from "./ImportTokensDialog" import { WatchedAsset } from "./WatchedAsset" -import { observer } from "@legendapp/state/react" /** * Displays all watched assets registered by the connected user. diff --git a/apps/iframe/src/components/interface/permissions/ListSingleAppPermissions.tsx b/apps/iframe/src/components/interface/permissions/ListSingleAppPermissions.tsx index 62588d7201..c3ea209ddb 100644 --- a/apps/iframe/src/components/interface/permissions/ListSingleAppPermissions.tsx +++ b/apps/iframe/src/components/interface/permissions/ListSingleAppPermissions.tsx @@ -4,7 +4,8 @@ import { Switch } from "#src/components/primitives/toggle-switch/Switch" import { PermissionName } from "#src/constants/permissions" import { type PermissionDescriptionIndex, permissionDescriptions } from "#src/constants/requestLabels" import { useLocalPermissionChanges } from "#src/hooks/useLocalPermissionChanges" -import type { AppPermissions, WalletPermission, PermissionsRequest } from "#src/state/permissions" +import type { PermissionsRequest } from "#src/state/permissions/types" +import type { AppPermissions, WalletPermission } from "#src/state/permissions/types" import type { AppURL } from "#src/utils/appURL" import { SessionKeyCheckbox } from "./SessionKeyCheckbox" diff --git a/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts b/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts index c7f5ba45c3..552b23db0c 100644 --- a/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts +++ b/apps/iframe/src/components/interface/permissions/useAppsWithPermissions.ts @@ -1,6 +1,6 @@ import { use$ } from "@legendapp/state/react" -import type { AppPermissions } from "#src/state/permissions/types" import { permissionsMapLegend } from "#src/state/permissions/observable" +import type { AppPermissions } from "#src/state/permissions/types" import { type AppURL, isWallet } from "#src/utils/appURL" export function useAppsWithPermissions(): [AppURL, AppPermissions][] { diff --git a/apps/iframe/src/hooks/useHasPermissions.ts b/apps/iframe/src/hooks/useHasPermissions.ts index 4fc67e070c..2ed54b8c31 100644 --- a/apps/iframe/src/hooks/useHasPermissions.ts +++ b/apps/iframe/src/hooks/useHasPermissions.ts @@ -1,6 +1,6 @@ import { use$ } from "@legendapp/state/react" -import type { PermissionsRequest } from "#src/state/permissions/types" import { hasPermissions } from "#src/state/permissions" +import type { PermissionsRequest } from "#src/state/permissions/types" import { type AppURL, getAppURL } from "../utils/appURL" export function useHasPermissions(permissionsRequest: PermissionsRequest, app: AppURL = getAppURL()) { diff --git a/apps/iframe/src/state/permissions/index.ts b/apps/iframe/src/state/permissions/index.ts index 4bb7d75efe..0412909e08 100644 --- a/apps/iframe/src/state/permissions/index.ts +++ b/apps/iframe/src/state/permissions/index.ts @@ -1,14 +1,14 @@ import type { Address } from "@happy.tech/common" import { permissionsLogger } from "#src/utils/logger" import { PermissionName } from "#src/constants/permissions" +import { revokedSessionKeys } from "#src/state/interfaceState" +import { getUser } from "#src/state/user" import { type AppURL, getWalletURL, isApp, isStandaloneWallet } from "#src/utils/appURL" import { checkIfCaveatsMatch } from "#src/utils/checkIfCaveatsMatch" import { emitUserUpdate } from "#src/utils/emitUserUpdate" -import { revokedSessionKeys } from "#src/state/interfaceState" -import { getUser } from "#src/state/user" import { permissionsMapLegend } from "./observable" -import type { AppPermissions, WalletPermission, WalletPermissionCaveat, PermissionsRequest } from "./types" import type { HappyUser } from "@happy.tech/wallet-common" +import type { AppPermissions, PermissionsRequest, WalletPermission, WalletPermissionCaveat } from "./types" // === GET ALL PERMISSIONS ======================================================================================= diff --git a/apps/iframe/src/state/permissions/observable.ts b/apps/iframe/src/state/permissions/observable.ts index da4c345835..c470ea4364 100644 --- a/apps/iframe/src/state/permissions/observable.ts +++ b/apps/iframe/src/state/permissions/observable.ts @@ -1,12 +1,12 @@ -import { deploymentVar } from "#src/env.ts" import { observable } from "@legendapp/state" +import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage" import { syncedCrud } from "@legendapp/state/sync-plugins/crud" +import { deploymentVar } from "#src/env.ts" import { getUser } from "../user" -import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage" import type { WalletPermission } from "./types" const SYNC_SERVICE_URL = deploymentVar("VITE_SYNC_SERVICE_URL") - + export const permissionsMapLegend = observable( syncedCrud({ list: async ({ lastSync }) => { diff --git a/apps/iframe/src/state/permissions/permissions.spec.ts b/apps/iframe/src/state/permissions/permissions.spec.ts index c5006fa206..3f93356610 100644 --- a/apps/iframe/src/state/permissions/permissions.spec.ts +++ b/apps/iframe/src/state/permissions/permissions.spec.ts @@ -9,8 +9,8 @@ import { hasPermissions, revokePermissions, } from "#src/state/permissions" -import { disablePermissionWarnings } from "#src/testing/utils" import { setUser } from "#src/state/user" +import { disablePermissionWarnings } from "#src/testing/utils" const { appURL, walletURL, appURLMock } = await vi // .hoisted(async () => await import("#src/testing/cross_origin.mocks")) diff --git a/apps/iframe/src/state/permissions/types.ts b/apps/iframe/src/state/permissions/types.ts index 716b75d87e..f35a93f5dc 100644 --- a/apps/iframe/src/state/permissions/types.ts +++ b/apps/iframe/src/state/permissions/types.ts @@ -1,6 +1,6 @@ import type { Address } from "@happy.tech/common" -import type { AppURL } from "#src/utils/appURL" import type { Permissions } from "#src/constants/permissions" +import type { AppURL } from "#src/utils/appURL" // In EIP-2255, permissions define whether an app can make certain EIP-1193 requests to the wallets. // These permissions are scoped per app and per account. @@ -79,4 +79,4 @@ export type SessionKeyRequest = { * A permissions specifier, which can be either a single EIP-1193 request name, or a {@link * PermissionRequestObject}. */ -export type PermissionsRequest = string | PermissionRequestObject \ No newline at end of file +export type PermissionsRequest = string | PermissionRequestObject diff --git a/apps/iframe/src/state/watchedAssets/index.ts b/apps/iframe/src/state/watchedAssets/index.ts index 210b7f093c..0db50b583b 100644 --- a/apps/iframe/src/state/watchedAssets/index.ts +++ b/apps/iframe/src/state/watchedAssets/index.ts @@ -4,7 +4,6 @@ import { getUser } from "#src/state/user" import { watchedAssetsMapLegend } from "./observable" import type { WatchedAsset } from "./types" - // === State Accessors ================================================================================== /** diff --git a/apps/iframe/src/state/watchedAssets/observable.ts b/apps/iframe/src/state/watchedAssets/observable.ts index 356e76e98b..a020bf365b 100644 --- a/apps/iframe/src/state/watchedAssets/observable.ts +++ b/apps/iframe/src/state/watchedAssets/observable.ts @@ -1,12 +1,12 @@ import { observable } from "@legendapp/state" -import { getUser } from "../user" -import { deploymentVar } from "#src/env.ts" +import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage" import { syncedCrud } from "@legendapp/state/sync-plugins/crud" +import { deploymentVar } from "#src/env.ts" +import { getUser } from "../user" import type { WatchedAsset } from "./types" -import { ObservablePersistLocalStorage } from "@legendapp/state/persist-plugins/local-storage" const SYNC_SERVICE_URL = deploymentVar("VITE_SYNC_SERVICE_URL") - + export const watchedAssetsMapLegend = observable( syncedCrud({ list: async ({ lastSync }) => { From dc7b763745fd723c323c228a39164630c2b1458c Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Thu, 26 Jun 2025 10:42:44 +0200 Subject: [PATCH 15/19] chore: duplicated actions name --- .github/workflows/deploy-sync-service.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-sync-service.yml b/.github/workflows/deploy-sync-service.yml index 4819080334..a83bc674dc 100644 --- a/.github/workflows/deploy-sync-service.yml +++ b/.github/workflows/deploy-sync-service.yml @@ -1,4 +1,4 @@ -name: Deploy staging sync service +name: Deploy sync service on: workflow_dispatch: From 3b30a9eed86c0241f53d5c61569abecb6662cc72 Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Thu, 26 Jun 2025 10:55:19 +0200 Subject: [PATCH 16/19] chore: format --- apps/iframe/src/state/permissions/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/iframe/src/state/permissions/index.ts b/apps/iframe/src/state/permissions/index.ts index 0412909e08..e025dc20d0 100644 --- a/apps/iframe/src/state/permissions/index.ts +++ b/apps/iframe/src/state/permissions/index.ts @@ -1,13 +1,13 @@ import type { Address } from "@happy.tech/common" -import { permissionsLogger } from "#src/utils/logger" +import type { HappyUser } from "@happy.tech/wallet-common" import { PermissionName } from "#src/constants/permissions" import { revokedSessionKeys } from "#src/state/interfaceState" import { getUser } from "#src/state/user" import { type AppURL, getWalletURL, isApp, isStandaloneWallet } from "#src/utils/appURL" import { checkIfCaveatsMatch } from "#src/utils/checkIfCaveatsMatch" import { emitUserUpdate } from "#src/utils/emitUserUpdate" +import { permissionsLogger } from "#src/utils/logger" import { permissionsMapLegend } from "./observable" -import type { HappyUser } from "@happy.tech/wallet-common" import type { AppPermissions, PermissionsRequest, WalletPermission, WalletPermissionCaveat } from "./types" // === GET ALL PERMISSIONS ======================================================================================= From a4907bc35af7fc54e641b1dfa660ae3a862b5ec5 Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Thu, 26 Jun 2025 10:59:59 +0200 Subject: [PATCH 17/19] chore: fix iframe build --- apps/iframe/src/hooks/useCachedPermissions.ts | 9 +++++---- apps/iframe/src/hooks/useLocalPermissionChanges.ts | 2 +- apps/iframe/src/state/permissions/types.ts | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/iframe/src/hooks/useCachedPermissions.ts b/apps/iframe/src/hooks/useCachedPermissions.ts index 93f08c5c9e..098f8bc9b1 100644 --- a/apps/iframe/src/hooks/useCachedPermissions.ts +++ b/apps/iframe/src/hooks/useCachedPermissions.ts @@ -1,7 +1,8 @@ import { useAtomValue } from "jotai" import { useState } from "react" -import { getAppPermissions, getAppPermissionsPure, permissionsMapAtom } from "#src/state/permissions" -import type { AppPermissions } from "#src/state/permissions" +import { getAppPermissions, getAppPermissionsPure } from "#src/state/permissions" +import { permissionsMapLegend } from "#src/state/permissions/observable" +import type { AppPermissions } from "#src/state/permissions/types" import { userAtom } from "#src/state/user" import type { AppURL } from "#src/utils/appURL" import { canonicalCaveatKey, mergeCaveats } from "#src/utils/caveats" @@ -11,8 +12,8 @@ export function useCachedPermissions(appURL: AppURL): { permissions: AppPermissi // and can be toggle back on while we don't navigate away. const [cachedPermissions, setCachedPermissions] = useState(structuredClone(getAppPermissions(appURL))) const user = useAtomValue(userAtom) - const permissionsMap = useAtomValue(permissionsMapAtom) - const reactivePermissions = getAppPermissionsPure(user, appURL, permissionsMap) + const permissionsMap = permissionsMapLegend.get() + const reactivePermissions = getAppPermissionsPure(user, appURL, Object.values(permissionsMap)) /** * flag to track if any update has occurred. If se, we will set state diff --git a/apps/iframe/src/hooks/useLocalPermissionChanges.ts b/apps/iframe/src/hooks/useLocalPermissionChanges.ts index fca153dc4f..0228d1ec75 100644 --- a/apps/iframe/src/hooks/useLocalPermissionChanges.ts +++ b/apps/iframe/src/hooks/useLocalPermissionChanges.ts @@ -6,7 +6,7 @@ import { PermissionName } from "#src/constants/permissions" import { revokeSessionKeys } from "#src/requests/utils/sessionKeys" import { revokedSessionKeys } from "#src/state/interfaceState" import { grantPermissions, hasPermissions, permissionRequestEntries, revokePermissions } from "#src/state/permissions" -import type { PermissionsRequest } from "#src/state/permissions" +import type { PermissionsRequest } from "#src/state/permissions/types" import type { AppURL } from "#src/utils/appURL" import { mergeCaveats } from "#src/utils/caveats" import { checkIfCaveatsMatch } from "#src/utils/checkIfCaveatsMatch" diff --git a/apps/iframe/src/state/permissions/types.ts b/apps/iframe/src/state/permissions/types.ts index f35a93f5dc..0b82427b5a 100644 --- a/apps/iframe/src/state/permissions/types.ts +++ b/apps/iframe/src/state/permissions/types.ts @@ -1,5 +1,5 @@ import type { Address } from "@happy.tech/common" -import type { Permissions } from "#src/constants/permissions" +import { PermissionName } from "#src/constants/permissions" import type { AppURL } from "#src/utils/appURL" // In EIP-2255, permissions define whether an app can make certain EIP-1193 requests to the wallets. @@ -42,7 +42,7 @@ export type WalletPermission = { // The app to which the permission is granted. invoker: AppURL // This is the EIP-1193 request that this permission is mapped to. - parentCapability: Permissions | (string & {}) + parentCapability: PermissionName | (string & {}) caveats: WalletPermissionCaveat[] date: number // Not in the EIP, but Viem wants this. @@ -72,7 +72,7 @@ export type PermissionRequestObject = { * A refinement of {@link PermissionRequestObject} for requesting session keys. */ export type SessionKeyRequest = { - [Permissions.SessionKey]: { target: Address } + [PermissionName.SessionKey]: { target: Address } } /** From 5b515134f3417a19c18400a1b18dc700c8b38f44 Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Thu, 26 Jun 2025 11:11:07 +0200 Subject: [PATCH 18/19] chore: change docs servers --- apps/sync-service/src/server/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/sync-service/src/server/index.ts b/apps/sync-service/src/server/index.ts index 03f4d1c8a2..bab1f8c5ac 100644 --- a/apps/sync-service/src/server/index.ts +++ b/apps/sync-service/src/server/index.ts @@ -49,7 +49,8 @@ app.get( }, ] : []), - { url: "https://settings.testnet.happy.tech", description: "Testnet" }, + { url: "https://sync-staging.happy.tech", description: "Staging" }, + { url: "https://sync.happy.tech", description: "Production" }, ], }, }), From f7764cc6736bebc1a7651ad70cb32dab8be6b9f0 Mon Sep 17 00:00:00 2001 From: GabrielMartinezRodriguez Date: Thu, 26 Jun 2025 12:38:06 +0200 Subject: [PATCH 19/19] chore: final changes --- .github/workflows/deploy-sync-service.yml | 2 +- apps/iframe/.env.example | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy-sync-service.yml b/.github/workflows/deploy-sync-service.yml index a83bc674dc..d5864afe29 100644 --- a/.github/workflows/deploy-sync-service.yml +++ b/.github/workflows/deploy-sync-service.yml @@ -37,7 +37,7 @@ jobs: rm: true debug: true - - name: Deploy staging sync service to server + - name: Deploy sync service to server uses: appleboy/ssh-action@25ce8cbbcb08177468c7ff7ec5cbfa236f9341e1 # v1.1.0 with: host: ${{ secrets.SERVER_HOST }} diff --git a/apps/iframe/.env.example b/apps/iframe/.env.example index dba3010382..01a778f9f9 100644 --- a/apps/iframe/.env.example +++ b/apps/iframe/.env.example @@ -91,9 +91,7 @@ VITE_TURNSTILE_SITEKEY=0x4AAAAAABRnNdBbR6oFMviC ######################################################################################################################## # SYNC SERVICE -VITE_SYNC_SERVICE_URL=https://sync.testnet.happy.tech - - +VITE_SYNC_SERVICE_URL=https://sync-staging.happy.tech ######################################################################################################################## # DEV UTILS