diff --git a/packages/coordinator/.env.example b/packages/coordinator/.env.example index 067c41dc..73f84265 100644 --- a/packages/coordinator/.env.example +++ b/packages/coordinator/.env.example @@ -45,4 +45,5 @@ SUBGRAPH_DEPLOY_KEY= # Subgraph project folder SUBGRAPH_FOLDER=./node_modules/maci-subgraph - +# API Key for Pimlico RPC Bundler +PIMLICO_API_KEY="" diff --git a/packages/coordinator/.gitignore b/packages/coordinator/.gitignore index 1751988f..1e6853cb 100644 --- a/packages/coordinator/.gitignore +++ b/packages/coordinator/.gitignore @@ -3,4 +3,4 @@ zkeys/ proofs/ tally.json - +session-keys.json diff --git a/packages/coordinator/package.json b/packages/coordinator/package.json index e6330fe6..a4b4c6c8 100644 --- a/packages/coordinator/package.json +++ b/packages/coordinator/package.json @@ -37,23 +37,30 @@ "@nestjs/websockets": "^10.3.10", "@nomicfoundation/hardhat-ethers": "^3.0.6", "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "@zerodev/ecdsa-validator": "^5.3.1", + "@zerodev/permissions": "^5.4.3", + "@zerodev/sdk": "^5.3.8", + "@zerodev/session-key": "^5.4.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.5", "ethers": "^6.13.1", "hardhat": "^2.22.6", "helmet": "^7.1.0", + "lowdb": "^1.0.0", "maci-circuits": "^2.1.0", "maci-cli": "^2.1.0", "maci-contracts": "^2.1.0", "maci-domainobjs": "^2.0.0", "maci-subgraph": "^2.1.0", "mustache": "^4.2.0", + "permissionless": "^0.1.44", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "socket.io": "^4.7.5", "tar": "^7.4.1", - "ts-node": "^10.9.1" + "ts-node": "^10.9.1", + "viem": "^2.7.15" }, "devDependencies": { "@nestjs/cli": "^10.4.2", @@ -61,6 +68,7 @@ "@nestjs/testing": "^10.3.10", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/lowdb": "^1.0.15", "@types/node": "^20.14.11", "@types/supertest": "^6.0.2", "fast-check": "^3.20.0", @@ -103,6 +111,9 @@ "!/ts/jest/*.js", "!/hardhat.config.js" ], + "coveragePathIgnorePatterns": [ + "/ts/sessionKeys/__tests__/utils.ts" + ], "coverageDirectory": "/coverage", "testEnvironment": "node" } diff --git a/packages/coordinator/ts/app.module.ts b/packages/coordinator/ts/app.module.ts index f363afec..fdd623a5 100644 --- a/packages/coordinator/ts/app.module.ts +++ b/packages/coordinator/ts/app.module.ts @@ -4,6 +4,7 @@ import { ThrottlerModule } from "@nestjs/throttler"; import { CryptoModule } from "./crypto/crypto.module"; import { FileModule } from "./file/file.module"; import { ProofModule } from "./proof/proof.module"; +import { SessionKeysModule } from "./sessionKeys/sessionKeys.module"; import { SubgraphModule } from "./subgraph/subgraph.module"; @Module({ @@ -18,6 +19,7 @@ import { SubgraphModule } from "./subgraph/subgraph.module"; CryptoModule, SubgraphModule, ProofModule, + SessionKeysModule, ], }) export class AppModule {} diff --git a/packages/coordinator/ts/common/__tests__/common.test.ts b/packages/coordinator/ts/common/__tests__/common.test.ts new file mode 100644 index 00000000..cc9a7b29 --- /dev/null +++ b/packages/coordinator/ts/common/__tests__/common.test.ts @@ -0,0 +1,17 @@ +import { genPimlicoRPCUrl } from "../accountAbstraction"; +import { ErrorCodes } from "../errors"; + +describe("common", () => { + describe("genPimlicoRPCUrl", () => { + test("should return the correct RPCUrl", () => { + const rpcUrl = genPimlicoRPCUrl("optimism-sepolia"); + expect(rpcUrl).toBeDefined(); + expect(rpcUrl).toContain("https://api.pimlico.io/v2/optimism-sepolia/rpc"); + }); + + test("should throw when PIMLICO_API_KEY is not set", () => { + delete process.env.PIMLICO_API_KEY; + expect(() => genPimlicoRPCUrl("optimism-sepolia")).toThrow(ErrorCodes.PIMLICO_API_KEY_NOT_SET); + }); + }); +}); diff --git a/packages/coordinator/ts/common/accountAbstraction.ts b/packages/coordinator/ts/common/accountAbstraction.ts new file mode 100644 index 00000000..07c24cec --- /dev/null +++ b/packages/coordinator/ts/common/accountAbstraction.ts @@ -0,0 +1,20 @@ +import dotenv from "dotenv"; + +import { ErrorCodes } from "./errors"; + +dotenv.config(); + +/** + * Generate the RPCUrl for Pimlico based on the chain we need to interact with + * @param network - the network we want to interact with + * @returns the RPCUrl for the network + */ +export const genPimlicoRPCUrl = (network: string): string => { + const pimlicoAPIKey = process.env.PIMLICO_API_KEY; + + if (!pimlicoAPIKey) { + throw new Error(ErrorCodes.PIMLICO_API_KEY_NOT_SET); + } + + return `https://api.pimlico.io/v2/${network}/rpc?apikey=${pimlicoAPIKey}`; +}; diff --git a/packages/coordinator/ts/common/errors.ts b/packages/coordinator/ts/common/errors.ts index 87606ae1..293e4aa2 100644 --- a/packages/coordinator/ts/common/errors.ts +++ b/packages/coordinator/ts/common/errors.ts @@ -10,4 +10,7 @@ export enum ErrorCodes { ENCRYPTION = "5", FILE_NOT_FOUND = "6", SUBGRAPH_DEPLOY = "7", + SESSION_KEY_NOT_FOUND = "8", + PIMLICO_API_KEY_NOT_SET = "9", + INVALID_APPROVAL = "10", } diff --git a/packages/coordinator/ts/file/__tests__/file.service.test.ts b/packages/coordinator/ts/file/__tests__/file.service.test.ts index d70e14cb..5256994f 100644 --- a/packages/coordinator/ts/file/__tests__/file.service.test.ts +++ b/packages/coordinator/ts/file/__tests__/file.service.test.ts @@ -12,6 +12,34 @@ describe("FileService", () => { jest.clearAllMocks(); }); + test("should save session key properly", () => { + const service = new FileService(); + + const sessionKeyAddress = "0x123"; + const sessionKey = "0x456"; + + service.storeSessionKey(sessionKey, sessionKeyAddress); + + const storedSessionKey = service.getSessionKey(sessionKeyAddress); + + expect(storedSessionKey).toEqual(sessionKey); + }); + + test("should delete session key properly", () => { + const service = new FileService(); + + const sessionKeyAddress = "0x123"; + const sessionKey = "0x456"; + + service.storeSessionKey(sessionKey, sessionKeyAddress); + + service.deleteSessionKey(sessionKeyAddress); + + const deletedSessionKey = service.getSessionKey(sessionKeyAddress); + + expect(deletedSessionKey).toBeUndefined(); + }); + test("should return public key properly", async () => { const service = new FileService(); diff --git a/packages/coordinator/ts/file/file.service.ts b/packages/coordinator/ts/file/file.service.ts index d0bdff26..e98e14ec 100644 --- a/packages/coordinator/ts/file/file.service.ts +++ b/packages/coordinator/ts/file/file.service.ts @@ -1,12 +1,21 @@ import { Injectable, Logger } from "@nestjs/common"; +import low from "lowdb"; +import FileSync from "lowdb/adapters/FileSync"; import fs from "fs"; import path from "path"; import type { IGetPrivateKeyData, IGetPublicKeyData, IGetZkeyFilePathsData } from "./types"; +import type { Hex } from "viem"; import { ErrorCodes } from "../common"; +/** + * Internal storage structure type. + * named: keys can be queried by name + */ +type TStorage = Record; + /** * FileService is responsible for working with local files like: * 1. RSA public/private keys @@ -19,11 +28,46 @@ export class FileService { */ private readonly logger: Logger; + /** + * Json file database instance + */ + private db: low.LowdbSync; + /** * Initialize service */ constructor() { this.logger = new Logger(FileService.name); + this.db = low(new FileSync(path.resolve(__dirname, "..", "..", "./session-keys.json"))); + } + + /** + * Store session key + * + * @param sessionKey - session key + * @param address - key address + */ + storeSessionKey(sessionKey: Hex, address: string): void { + this.db.set(address, sessionKey).write(); + } + + /** + * Delete session key + * + * @param address - key address + */ + deleteSessionKey(address: string): void { + this.db.unset(address).write(); + } + + /** + * Get session key + * + * @param address - key name + * @returns session key + */ + getSessionKey(address: string): Hex | undefined { + return this.db.get(address).value() as Hex | undefined; } /** diff --git a/packages/coordinator/ts/proof/proof.service.ts b/packages/coordinator/ts/proof/proof.service.ts index dc4794d0..010c47ec 100644 --- a/packages/coordinator/ts/proof/proof.service.ts +++ b/packages/coordinator/ts/proof/proof.service.ts @@ -44,7 +44,6 @@ export class ProofGeneratorService { ) { this.deployment = Deployment.getInstance(hre); this.deployment.setHre(hre); - this.fileService = fileService; this.logger = new Logger(ProofGeneratorService.name); } diff --git a/packages/coordinator/ts/sessionKeys/__tests__/sessionKeys.controller.test.ts b/packages/coordinator/ts/sessionKeys/__tests__/sessionKeys.controller.test.ts new file mode 100644 index 00000000..1b865a7c --- /dev/null +++ b/packages/coordinator/ts/sessionKeys/__tests__/sessionKeys.controller.test.ts @@ -0,0 +1,57 @@ +import { Test } from "@nestjs/testing"; +import { zeroAddress } from "viem"; + +import type { IGenerateSessionKeyReturn } from "../types"; + +import { SessionKeysController } from "../sessionKeys.controller"; +import { SessionKeysService } from "../sessionKeys.service"; + +describe("SessionKeysController", () => { + let sessionKeysController: SessionKeysController; + + const mockSessionKeysService = { + generateSessionKey: jest.fn(), + deactivateSessionKey: jest.fn(), + }; + + const defaultGenerateSessionKeyReturn: IGenerateSessionKeyReturn = { + sessionKeyAddress: zeroAddress, + }; + + beforeEach(async () => { + const app = await Test.createTestingModule({ + controllers: [SessionKeysController], + }) + .useMocker((token) => { + if (token === SessionKeysService) { + mockSessionKeysService.generateSessionKey.mockResolvedValue(defaultGenerateSessionKeyReturn); + return mockSessionKeysService; + } + + return jest.fn(); + }) + .compile(); + + sessionKeysController = app.get(SessionKeysController); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("v1/session-keys/generate", () => { + test("should return a session key address", async () => { + const data = await sessionKeysController.generateSessionKey(); + expect(data).toStrictEqual(defaultGenerateSessionKeyReturn); + }); + }); + + describe("v1/session-keys/delete", () => { + test("should delete a session key", () => { + sessionKeysController.deactivateSessionKey({ + sessionKeyAddress: zeroAddress, + }); + expect(mockSessionKeysService.deactivateSessionKey).toHaveBeenCalledWith(zeroAddress); + }); + }); +}); diff --git a/packages/coordinator/ts/sessionKeys/__tests__/sessionKeys.service.test.ts b/packages/coordinator/ts/sessionKeys/__tests__/sessionKeys.service.test.ts new file mode 100644 index 00000000..adfb331b --- /dev/null +++ b/packages/coordinator/ts/sessionKeys/__tests__/sessionKeys.service.test.ts @@ -0,0 +1,79 @@ +import dotenv from "dotenv"; +import { ZeroAddress } from "ethers"; +import { zeroAddress } from "viem"; +import { optimismSepolia } from "viem/chains"; + +import { KeyLike } from "crypto"; + +import { ErrorCodes } from "../../common"; +import { CryptoService } from "../../crypto/crypto.service"; +import { FileService } from "../../file/file.service"; +import { SessionKeysService } from "../sessionKeys.service"; + +import { mockSessionKeyApproval } from "./utils"; + +dotenv.config(); + +describe("SessionKeysService", () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + const cryptoService = new CryptoService(); + const fileService = new FileService(); + const sessionKeysService = new SessionKeysService(cryptoService, fileService); + let publicKey: KeyLike; + + beforeAll(async () => { + publicKey = (await fileService.getPublicKey()).publicKey; + }); + + describe("generateSessionKey", () => { + test("should generate and store a session key", () => { + const sessionKeyAddress = sessionKeysService.generateSessionKey(); + expect(sessionKeyAddress).toBeDefined(); + expect(sessionKeyAddress).not.toEqual(ZeroAddress); + + const sessionKey = fileService.getSessionKey(sessionKeyAddress.sessionKeyAddress); + expect(sessionKey).toBeDefined(); + }); + }); + + describe("deactivateSessionKey", () => { + test("should delete a session key", () => { + const sessionKeyAddress = sessionKeysService.generateSessionKey(); + expect(sessionKeyAddress).toBeDefined(); + expect(sessionKeyAddress).not.toEqual(ZeroAddress); + + const sessionKey = fileService.getSessionKey(sessionKeyAddress.sessionKeyAddress); + expect(sessionKey).toBeDefined(); + + sessionKeysService.deactivateSessionKey(sessionKeyAddress.sessionKeyAddress); + const sessionKeyDeleted = fileService.getSessionKey(sessionKeyAddress.sessionKeyAddress); + expect(sessionKeyDeleted).toBeUndefined(); + }); + }); + + describe("generateClientFromSessionKey", () => { + test("should fail to generate a client with an invalid approval", async () => { + const sessionKeyAddress = sessionKeysService.generateSessionKey(); + const approval = await mockSessionKeyApproval(sessionKeyAddress.sessionKeyAddress); + const encryptedApproval = cryptoService.encrypt(publicKey, approval); + await expect( + sessionKeysService.generateClientFromSessionKey( + sessionKeyAddress.sessionKeyAddress, + encryptedApproval, + optimismSepolia, + ), + ).rejects.toThrow(ErrorCodes.INVALID_APPROVAL); + }); + + test("should throw when given a non existent session key address", async () => { + const approval = await mockSessionKeyApproval(zeroAddress); + const encryptedApproval = cryptoService.encrypt(publicKey, approval); + await expect( + sessionKeysService.generateClientFromSessionKey(zeroAddress, encryptedApproval, optimismSepolia), + ).rejects.toThrow(ErrorCodes.SESSION_KEY_NOT_FOUND); + }); + }); +}); diff --git a/packages/coordinator/ts/sessionKeys/__tests__/utils.ts b/packages/coordinator/ts/sessionKeys/__tests__/utils.ts new file mode 100644 index 00000000..dcd78412 --- /dev/null +++ b/packages/coordinator/ts/sessionKeys/__tests__/utils.ts @@ -0,0 +1,90 @@ +import { signerToEcdsaValidator } from "@zerodev/ecdsa-validator"; +import { type Policy, serializePermissionAccount, toPermissionValidator } from "@zerodev/permissions"; +import { toTimestampPolicy } from "@zerodev/permissions/policies"; +import { toECDSASigner } from "@zerodev/permissions/signers"; +import { addressToEmptyAccount, createKernelAccount } from "@zerodev/sdk"; +import { KERNEL_V3_1 } from "@zerodev/sdk/constants"; +import { ENTRYPOINT_ADDRESS_V07 } from "permissionless"; +import { Address, createPublicClient, type Hex, http } from "viem"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { localhost } from "viem/chains"; + +// use the latest kernel version +const kernelVersion = KERNEL_V3_1; +// we use the most recent entrypoint +const entryPoint = ENTRYPOINT_ADDRESS_V07; + +/** + * Generate a timestamp policy + * @param endTime - The end time of the policy + * @param start - The start time of the policy + * @returns The timestamp policy + */ +export const generateTimestampPolicy = (endTime: number, start?: number): Policy => + toTimestampPolicy({ + validAfter: start, + validUntil: endTime, + }); + +jest.mock("@zerodev/sdk", (): unknown => ({ + ...jest.requireActual("@zerodev/sdk"), + createKernelAccount: jest.fn(), +})); + +jest.mock("@zerodev/permissions", (): unknown => ({ + ...jest.requireActual("@zerodev/permissions"), + serializePermissionAccount: () => "approval", +})); + +/** + * Mock a session key approval + * @dev This will fail in hardhat with: + * "InvalidEntryPointError: The entry point address + * (`entryPoint` = 0x0000000071727De22E5E9d8BAf0edAc6f37da032) + * is not a valid entry point. getSenderAddress did not revert with + * a SenderAddressResult error." + * + * @param sessionKeyAddress - The address of the session key + * @returns The approval string + */ +export const mockSessionKeyApproval = async (sessionKeyAddress: Hex): Promise => { + const policies = [generateTimestampPolicy(Math.floor(Date.now() / 1000) + 1000)]; + + const publicClient = createPublicClient({ + chain: localhost, + transport: http(), + }); + + const sessionPrivateKey = generatePrivateKey(); + + const signer = privateKeyToAccount(sessionPrivateKey); + + const ecdsaValidator = await signerToEcdsaValidator(publicClient, { + signer, + entryPoint, + kernelVersion, + }); + + // Create an "empty account" as the signer -- you only need the public + // key (address) to do this. + const emptyAccount = addressToEmptyAccount(sessionKeyAddress as unknown as Address); + const emptySessionKeySigner = toECDSASigner({ signer: emptyAccount }); + + const permissionPlugin = await toPermissionValidator(publicClient, { + entryPoint, + kernelVersion, + signer: emptySessionKeySigner, + policies, + }); + + const sessionKeyAccount = await createKernelAccount(publicClient, { + entryPoint, + kernelVersion, + plugins: { + sudo: ecdsaValidator, + regular: permissionPlugin, + }, + }); + + return serializePermissionAccount(sessionKeyAccount); +}; diff --git a/packages/coordinator/ts/sessionKeys/dto.ts b/packages/coordinator/ts/sessionKeys/dto.ts new file mode 100644 index 00000000..56a0064a --- /dev/null +++ b/packages/coordinator/ts/sessionKeys/dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEthereumAddress } from "class-validator"; + +import type { Hex } from "viem"; + +/** + * Data transfer object for Deactivate session key + */ +export class DeactivateSessionKeyDto { + /** + * Session key address + */ + @ApiProperty({ + description: "Session key address", + type: String, + }) + @IsEthereumAddress() + sessionKeyAddress!: Hex; +} diff --git a/packages/coordinator/ts/sessionKeys/sessionKeys.controller.ts b/packages/coordinator/ts/sessionKeys/sessionKeys.controller.ts new file mode 100644 index 00000000..c2c7662a --- /dev/null +++ b/packages/coordinator/ts/sessionKeys/sessionKeys.controller.ts @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/no-shadow */ +import { Body, Controller, Delete, Get, HttpStatus, UseGuards } from "@nestjs/common"; +import { ApiBearerAuth, ApiBody, ApiResponse, ApiTags } from "@nestjs/swagger"; + +import type { IGenerateSessionKeyReturn } from "./types"; + +import { AccountSignatureGuard } from "../auth/AccountSignatureGuard.service"; + +import { DeactivateSessionKeyDto } from "./dto"; +import { SessionKeysService } from "./sessionKeys.service"; + +@ApiTags("v1/session-keys") +@ApiBearerAuth() +@Controller("v1/session-keys") +@UseGuards(AccountSignatureGuard) +export class SessionKeysController { + /** + * Initialize SessionKeysController + * + * @param sessionKeysService - session keys service + */ + constructor(private readonly sessionKeysService: SessionKeysService) {} + + /** + * Generate a session key api method + * + * @returns generated session key address + */ + @ApiResponse({ status: HttpStatus.CREATED, description: "The session key was successfully generated" }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: "Forbidden" }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: "BadRequest" }) + @Get("generate") + async generateSessionKey(): Promise { + return Promise.resolve(this.sessionKeysService.generateSessionKey()); + } + + /** + * Delete a session key api method + * + * @param args - delete session key dto + * @returns deleted session key address + */ + @ApiBody({ type: DeactivateSessionKeyDto }) + @ApiResponse({ status: HttpStatus.CREATED, description: "The session key was successfully deactivated" }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, description: "Forbidden" }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: "BadRequest" }) + @Delete("delete") + deactivateSessionKey(@Body() args: DeactivateSessionKeyDto): void { + this.sessionKeysService.deactivateSessionKey(args.sessionKeyAddress); + } +} diff --git a/packages/coordinator/ts/sessionKeys/sessionKeys.module.ts b/packages/coordinator/ts/sessionKeys/sessionKeys.module.ts new file mode 100644 index 00000000..fdc40cb3 --- /dev/null +++ b/packages/coordinator/ts/sessionKeys/sessionKeys.module.ts @@ -0,0 +1,14 @@ +import { Module } from "@nestjs/common"; + +import { CryptoModule } from "../crypto/crypto.module"; +import { FileModule } from "../file/file.module"; + +import { SessionKeysController } from "./sessionKeys.controller"; +import { SessionKeysService } from "./sessionKeys.service"; + +@Module({ + imports: [FileModule, CryptoModule], + controllers: [SessionKeysController], + providers: [SessionKeysService], +}) +export class SessionKeysModule {} diff --git a/packages/coordinator/ts/sessionKeys/sessionKeys.service.ts b/packages/coordinator/ts/sessionKeys/sessionKeys.service.ts new file mode 100644 index 00000000..2ecd67bd --- /dev/null +++ b/packages/coordinator/ts/sessionKeys/sessionKeys.service.ts @@ -0,0 +1,135 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { deserializePermissionAccount } from "@zerodev/permissions"; +import { toECDSASigner } from "@zerodev/permissions/signers"; +import { createKernelAccountClient, KernelAccountClient, KernelSmartAccount } from "@zerodev/sdk"; +import { KERNEL_V3_1 } from "@zerodev/sdk/constants"; +import { ENTRYPOINT_ADDRESS_V07 } from "permissionless"; +import { ENTRYPOINT_ADDRESS_V07_TYPE } from "permissionless/types"; +import { type Chain, createPublicClient, type Hex, http, type HttpTransport, type Transport } from "viem"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; + +import { ErrorCodes } from "../common"; +import { genPimlicoRPCUrl } from "../common/accountAbstraction"; +import { CryptoService } from "../crypto/crypto.service"; +import { FileService } from "../file/file.service"; + +import { IGenerateSessionKeyReturn } from "./types"; + +/** + * SessionKeysService is responsible for generating and managing session keys. + */ +@Injectable() +export class SessionKeysService { + /** + * Logger + */ + private readonly logger: Logger; + + /** + * Create a new instance of SessionKeysService + * + * @param cryptoService - crypto service + * @param fileService - file service + */ + constructor( + private readonly cryptoService: CryptoService, + private readonly fileService: FileService, + ) { + this.logger = new Logger(SessionKeysService.name); + } + + /** + * Generate a session key + * + * @returns session key address + */ + generateSessionKey(): IGenerateSessionKeyReturn { + const sessionPrivateKey = generatePrivateKey(); + + const sessionKeySigner = toECDSASigner({ + signer: privateKeyToAccount(sessionPrivateKey), + }); + + const sessionKeyAddress = sessionKeySigner.account.address; + + // save the key + this.fileService.storeSessionKey(sessionPrivateKey, sessionKeyAddress); + + return { + sessionKeyAddress, + }; + } + + /** + * Generate a KernelClient from a session key and an approval + * + * @param sessionKeyAddress - the address of the session key + * @param encryptedApproval - the encrypted approval string + * @param chain - the chain to use + * @returns + */ + async generateClientFromSessionKey( + sessionKeyAddress: Hex, + encryptedApproval: string, + chain: Chain, + ): Promise< + KernelAccountClient< + ENTRYPOINT_ADDRESS_V07_TYPE, + Transport, + undefined, + KernelSmartAccount + > + > { + // the approval will have been encrypted so we need to decrypt it + const { privateKey } = await this.fileService.getPrivateKey(); + const approval = this.cryptoService.decrypt(privateKey, encryptedApproval); + + // retrieve the session key from the file service + const sessionKey = this.fileService.getSessionKey(sessionKeyAddress); + + if (!sessionKey) { + this.logger.error(`Session key not found: ${sessionKeyAddress}`); + throw new Error(ErrorCodes.SESSION_KEY_NOT_FOUND); + } + + // get the bundler url and create a public client + const bundlerUrl = genPimlicoRPCUrl(chain.name); + const publicClient = createPublicClient({ + transport: http(bundlerUrl), + }); + + // Using a stored private key + const sessionKeySigner = toECDSASigner({ + signer: privateKeyToAccount(sessionKey), + }); + + try { + // deserialize the permission account using approval and session key + const sessionKeyAccount = await deserializePermissionAccount( + publicClient, + ENTRYPOINT_ADDRESS_V07, + KERNEL_V3_1, + approval, + sessionKeySigner, + ); + + return createKernelAccountClient({ + bundlerTransport: http(bundlerUrl), + entryPoint: ENTRYPOINT_ADDRESS_V07, + account: sessionKeyAccount, + }); + } catch (error) { + this.logger.error("Error deserializing permission account", error); + throw new Error(ErrorCodes.INVALID_APPROVAL); + } + } + + /** + * Deactivate a session key + * + * @param sessionKeyAddress - key address + */ + deactivateSessionKey(sessionKeyAddress: Hex): void { + this.fileService.deleteSessionKey(sessionKeyAddress); + } +} diff --git a/packages/coordinator/ts/sessionKeys/types.ts b/packages/coordinator/ts/sessionKeys/types.ts new file mode 100644 index 00000000..ff4f93d7 --- /dev/null +++ b/packages/coordinator/ts/sessionKeys/types.ts @@ -0,0 +1,11 @@ +import type { Hex } from "viem"; + +/** + * Generate session key return type + */ +export interface IGenerateSessionKeyReturn { + /** + * Session key address + */ + sessionKeyAddress: Hex; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc490dd3..fb86b514 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -189,6 +189,18 @@ importers: '@nomicfoundation/hardhat-toolbox': specifier: ^5.0.0 version: 5.0.0(qc2x6fg2hvcwib2hi3ibxymkay) + '@zerodev/ecdsa-validator': + specifier: ^5.3.1 + version: 5.3.1(@zerodev/sdk@5.3.9(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)) + '@zerodev/permissions': + specifier: ^5.4.3 + version: 5.4.3(@zerodev/sdk@5.3.9(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(@zerodev/webauthn-key@5.3.1(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)) + '@zerodev/sdk': + specifier: ^5.3.8 + version: 5.3.9(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)) + '@zerodev/session-key': + specifier: ^5.4.2 + version: 5.4.2(@zerodev/sdk@5.3.9(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)) class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -207,6 +219,9 @@ importers: helmet: specifier: ^7.1.0 version: 7.1.0 + lowdb: + specifier: ^1.0.0 + version: 1.0.0 maci-circuits: specifier: ^2.1.0 version: 2.1.0(@types/snarkjs@0.7.8)(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -225,6 +240,9 @@ importers: mustache: specifier: ^4.2.0 version: 4.2.0 + permissionless: + specifier: ^0.1.44 + version: 0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)) reflect-metadata: specifier: ^0.2.0 version: 0.2.2 @@ -240,6 +258,9 @@ importers: ts-node: specifier: ^10.9.1 version: 10.9.2(@types/node@20.14.14)(typescript@5.5.4) + viem: + specifier: ^2.7.15 + version: 2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4) devDependencies: '@nestjs/cli': specifier: ^10.4.2 @@ -256,6 +277,9 @@ importers: '@types/jest': specifier: ^29.5.2 version: 29.5.12 + '@types/lowdb': + specifier: ^1.0.15 + version: 1.0.15 '@types/node': specifier: ^20.14.11 version: 20.14.14 @@ -3512,6 +3536,19 @@ packages: resolution: {integrity: sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==} engines: {node: ^16.14.0 || >=18.0.0} + '@simplewebauthn/browser@8.3.7': + resolution: {integrity: sha512-ZtRf+pUEgOCvjrYsbMsJfiHOdKcrSZt2zrAnIIpfmA06r0FxBovFYq0rJ171soZbe13KmWzAoLKjSxVW7KxCdQ==} + + '@simplewebauthn/browser@9.0.1': + resolution: {integrity: sha512-wD2WpbkaEP4170s13/HUxPcAV5y4ZXaKo1TfNklS5zDefPinIgXOpgz1kpEvobAsaLPa2KeH7AKKX/od1mrBJw==} + + '@simplewebauthn/types@9.0.1': + resolution: {integrity: sha512-tGSRP1QvsAvsJmnOlRQyw/mvK9gnPtjEc5fg2+m8n+QUa+D7rvrKkOYyfpy42GTs90X3RDOnqJgfHt+qO67/+w==} + + '@simplewebauthn/typescript-types@8.3.4': + resolution: {integrity: sha512-38xtca0OqfRVNloKBrFB5LEM6PN5vzFbJG6rAutPVrtGHFYxPdiV3btYWq0eAZAZmP+dqFPYJxJWeJrGfmYHng==} + deprecated: This package has been renamed to @simplewebauthn/types. Please install @simplewebauthn/types instead to ensure you receive future updates. + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -4440,6 +4477,39 @@ packages: resolution: {integrity: sha512-aiATs7pSutzda/rq8fnuPwTglyVwjM22bNnK2ZgjrpAjQHSSl3lztd2f9evst1W/qnC58DRz7T7QndUDumAR4Q==} engines: {node: '>=14.15.0'} + '@zerodev/ecdsa-validator@5.3.1': + resolution: {integrity: sha512-4xg/uBgjQLyO5GoE9TkQqO7HRwSPVbchd6UXit+R3excN1fC27k7UcjLmm5rTrM2JxbgncIodGcNQMxYKNejsA==} + peerDependencies: + '@zerodev/sdk': ^5.2.1 + permissionless: ^0.1.18 + viem: ^2.16.3 + + '@zerodev/permissions@5.4.3': + resolution: {integrity: sha512-rC5npYPRQO8lwhMXO6h2MXO3DGgRSDqAEVh9E2pRnlYPhaMDHRVd+nPqLIsx1OubRT0jSXrPFTQJd2MqeKcJ6w==} + peerDependencies: + '@zerodev/sdk': ^5.2.13 + '@zerodev/webauthn-key': ^5.3.0 + permissionless: ^0.1.18 + viem: ^2.16.3 + + '@zerodev/sdk@5.3.9': + resolution: {integrity: sha512-5SEgVTV8RFNp8sXdP8ew7MFhTXVunKn7X2jEOnEMN4+BKxN5V794VNk1hHdBaYCRhcRLlyxjlp4f1wrLhbNQqw==} + peerDependencies: + permissionless: ^0.1.18 + viem: ^2.16.3 + + '@zerodev/session-key@5.4.2': + resolution: {integrity: sha512-/u2/m2BFEe2L3GAIs6DaTfFJVRufnVMZlr+0F3J6GEXNqW3cSM0JcO2jetgFRRGF9Ir6MGnPzgUME3JPNtK9TA==} + peerDependencies: + '@zerodev/sdk': ^5.2.1 + permissionless: ^0.1.18 + viem: ^2.16.3 + + '@zerodev/webauthn-key@5.3.1': + resolution: {integrity: sha512-8s/gdKppXgh15wrER50nAIhaD/QTEwecN288RbXI+TWVmdwzuLOpbMuzfgBvisfiE1kNJlfYEl9qos7aKm6EkQ==} + peerDependencies: + viem: ^2.16.3 + '@zk-kit/artifacts@1.8.0': resolution: {integrity: sha512-G2rQ1BxYt9CuVyU4Egc4ceSLLWx9BRrtFGZWS0RWwHhAMfSV/Fq9Qz6OX02leFzTbi7Tr3bTP6DgDSqr28OQnw==} @@ -5031,6 +5101,9 @@ packages: big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + bin-links@4.0.4: resolution: {integrity: sha512-cMtq4W5ZsEwcutJrVId+a/tjt8GSbS+h0oNkdl6+6rBuEv8Ot33Bevj5KPm40t309zuhVic8NjpuL42QCiJWWA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -5163,6 +5236,9 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer-reverse@1.0.1: + resolution: {integrity: sha512-M87YIUBsZ6N924W57vDwT/aOu8hw7ZgdByz6ijksLjmHJELBASmYTTlNHRgjE+pTsT9oJXGaDSgqqwfdHotDUg==} + buffer-xor@1.0.3: resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} @@ -5899,6 +5975,9 @@ packages: crypt@0.0.2: resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + css-in-js-utils@3.1.0: resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} @@ -9338,6 +9417,10 @@ packages: merge@2.1.1: resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==} + merkletreejs@0.3.11: + resolution: {integrity: sha512-LJKTl4iVNTndhL+3Uz/tfkjD0klIWsHlUzgtuNnNrsf7bAlXR30m+xYB7lHr5Z/l6e/yAIsr26Dabx6Buo4VGQ==} + engines: {node: '>= 7.6.0'} + methods@1.1.2: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} @@ -10388,6 +10471,11 @@ packages: performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + permissionless@0.1.44: + resolution: {integrity: sha512-NQBATmG4Fp3Zqy1IjjBihfp2huV6sTzUUzZzuSQ7xBnRNABOyIm8d+q76gy2B0LnFfu47RA/aW+fNfZjnbzl4Q==} + peerDependencies: + viem: '>=2.14.1 <2.18.0' + picocolors@1.0.1: resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} @@ -12154,6 +12242,10 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + treeify@1.1.0: + resolution: {integrity: sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==} + engines: {node: '>=0.6'} + treeverse@3.0.0: resolution: {integrity: sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -17620,6 +17712,18 @@ snapshots: '@sigstore/core': 1.1.0 '@sigstore/protobuf-specs': 0.3.2 + '@simplewebauthn/browser@8.3.7': + dependencies: + '@simplewebauthn/typescript-types': 8.3.4 + + '@simplewebauthn/browser@9.0.1': + dependencies: + '@simplewebauthn/types': 9.0.1 + + '@simplewebauthn/types@9.0.1': {} + + '@simplewebauthn/typescript-types@8.3.4': {} + '@sinclair/typebox@0.27.8': {} '@sindresorhus/is@0.7.0': {} @@ -19053,6 +19157,40 @@ snapshots: js-yaml: 3.14.1 tslib: 2.6.3 + '@zerodev/ecdsa-validator@5.3.1(@zerodev/sdk@5.3.9(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4))': + dependencies: + '@zerodev/sdk': 5.3.9(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)) + permissionless: 0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)) + viem: 2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4) + + '@zerodev/permissions@5.4.3(@zerodev/sdk@5.3.9(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(@zerodev/webauthn-key@5.3.1(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4))': + dependencies: + '@simplewebauthn/browser': 9.0.1 + '@zerodev/sdk': 5.3.9(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)) + '@zerodev/webauthn-key': 5.3.1(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)) + merkletreejs: 0.3.11 + permissionless: 0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)) + viem: 2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4) + + '@zerodev/sdk@5.3.9(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4))': + dependencies: + permissionless: 0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)) + semver: 7.6.3 + viem: 2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4) + + '@zerodev/session-key@5.4.2(@zerodev/sdk@5.3.9(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4))': + dependencies: + '@zerodev/sdk': 5.3.9(permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)))(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)) + merkletreejs: 0.3.11 + permissionless: 0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)) + viem: 2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4) + + '@zerodev/webauthn-key@5.3.1(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4))': + dependencies: + '@noble/curves': 1.4.2 + '@simplewebauthn/browser': 8.3.7 + viem: 2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4) + '@zk-kit/artifacts@1.8.0': {} '@zk-kit/baby-jubjub@1.0.1': @@ -19710,6 +19848,8 @@ snapshots: big.js@5.2.2: {} + bignumber.js@9.1.2: {} + bin-links@4.0.4: dependencies: cmd-shim: 6.0.3 @@ -19879,6 +20019,8 @@ snapshots: buffer-from@1.1.2: {} + buffer-reverse@1.0.1: {} + buffer-xor@1.0.3: {} buffer@5.7.1: @@ -20691,6 +20833,8 @@ snapshots: crypt@0.0.2: {} + crypto-js@4.2.0: {} + css-in-js-utils@3.1.0: dependencies: hyphenate-style-name: 1.1.0 @@ -25758,6 +25902,14 @@ snapshots: merge@2.1.1: {} + merkletreejs@0.3.11: + dependencies: + bignumber.js: 9.1.2 + buffer-reverse: 1.0.1 + crypto-js: 4.2.0 + treeify: 1.1.0 + web3-utils: 1.10.4 + methods@1.1.2: {} metro-babel-transformer@0.80.9: @@ -27112,6 +27264,10 @@ snapshots: performance-now@2.1.0: {} + permissionless@0.1.44(viem@2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4)): + dependencies: + viem: 2.19.1(bufferutil@4.0.8)(typescript@5.5.4)(utf-8-validate@5.0.10)(zod@3.22.4) + picocolors@1.0.1: {} picomatch@2.3.1: {} @@ -29183,6 +29339,8 @@ snapshots: tree-kill@1.2.2: {} + treeify@1.1.0: {} + treeverse@3.0.0: {} trim-lines@3.0.1: {} @@ -29866,7 +30024,7 @@ snapshots: webauthn-p256@0.0.5: dependencies: - '@noble/curves': 1.4.0 + '@noble/curves': 1.4.2 '@noble/hashes': 1.4.0 webcrypto-core@1.8.0: