diff --git a/packages/@controllers/package.json b/packages/@controllers/package.json index e0b57cf3e..ffa3dddeb 100644 --- a/packages/@controllers/package.json +++ b/packages/@controllers/package.json @@ -14,7 +14,8 @@ }, "exports": { ".": "./src/index.ts", - "./isle": "./src/isle/index.ts" + "./isle": "./src/isle/index.ts", + "./secure-enclave": "./src/secure-enclave/index.ts" }, "license": "MIT", "peerDependencies": { diff --git a/packages/@controllers/src/index.ts b/packages/@controllers/src/index.ts index 48efd5401..b827128ac 100644 --- a/packages/@controllers/src/index.ts +++ b/packages/@controllers/src/index.ts @@ -1 +1,2 @@ export * from "./isle"; +export * from "./secure-enclave"; diff --git a/packages/@controllers/src/secure-enclave/iframe-enclave.ts b/packages/@controllers/src/secure-enclave/iframe-enclave.ts new file mode 100644 index 000000000..7c26762ea --- /dev/null +++ b/packages/@controllers/src/secure-enclave/iframe-enclave.ts @@ -0,0 +1,259 @@ +import { base64Encode, type idOSCredential } from "@idos-network/core"; + +import type { + BackupPasswordInfo, + DiscoverUserEncryptionPublicKeyResponse, + EnclaveOptions, + EnclaveProvider, + StoredData, +} from "./types"; + +export class IframeEnclave implements EnclaveProvider { + options: Omit; + container: string; + iframe: HTMLIFrameElement; + hostUrl: URL; + + constructor(options: EnclaveOptions) { + const { container, ...other } = options; + this.container = container; + this.options = other; + this.hostUrl = new URL(other.url ?? "https://enclave.idos.network"); + this.iframe = document.createElement("iframe"); + this.iframe.id = "idos-enclave-iframe"; + } + + async load(): Promise { + await this.#loadEnclave(); + + await this.#requestToEnclave({ configure: this.options }); + + return (await this.#requestToEnclave({ storage: {} })) as StoredData; + } + + async ready( + userId?: string, + signerAddress?: string, + signerPublicKey?: string, + expectedUserEncryptionPublicKey?: string, + ): Promise { + let { encryptionPublicKey: userEncryptionPublicKey } = (await this.#requestToEnclave({ + storage: { + userId, + signerAddress, + signerPublicKey, + expectedUserEncryptionPublicKey, + }, + })) as StoredData; + + while (!userEncryptionPublicKey) { + this.#showEnclave(); + try { + userEncryptionPublicKey = (await this.#requestToEnclave({ + keys: {}, + })) as Uint8Array; + } catch (e) { + if (this.options.throwOnUserCancelUnlock) throw e; + } finally { + this.#hideEnclave(); + } + } + + return userEncryptionPublicKey; + } + + async store(key: string, value: string): Promise { + return this.#requestToEnclave({ storage: { [key]: value } }) as Promise; + } + + async reset(): Promise { + this.#requestToEnclave({ reset: {} }); + } + + async updateStore(key: string, value: unknown): Promise { + await this.#requestToEnclave({ updateStore: { key, value } }); + } + + async confirm(message: string): Promise { + this.#showEnclave(); + + return this.#requestToEnclave({ confirm: { message } }).then((response) => { + this.#hideEnclave(); + return response as boolean; + }); + } + + async encrypt( + message: Uint8Array, + receiverPublicKey: Uint8Array, + ): Promise<{ content: Uint8Array; encryptorPublicKey: Uint8Array }> { + return this.#requestToEnclave({ + encrypt: { message, receiverPublicKey }, + }) as Promise<{ content: Uint8Array; encryptorPublicKey: Uint8Array }>; + } + + async decrypt(message: Uint8Array, senderPublicKey: Uint8Array): Promise { + return this.#requestToEnclave({ + decrypt: { fullMessage: message, senderPublicKey }, + }) as Promise; + } + + async filterCredentialsByCountries(credentials: Record[], countries: string[]) { + return this.#requestToEnclave({ + filterCredentialsByCountries: { credentials, countries }, + }) as Promise; + } + + filterCredentials( + credentials: Record[], + privateFieldFilters: { pick: Record; omit: Record }, + ): Promise { + return this.#requestToEnclave({ + filterCredentials: { credentials, privateFieldFilters }, + }) as Promise; + } + + async #loadEnclave(): Promise { + const container = + document.querySelector(this.container) || + throwNew(Error, `Can't find container with selector ${this.container}`); + + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy#directives + const permissionsPolicies = ["publickey-credentials-get", "storage-access"]; + + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox + const liftedSandboxRestrictions = [ + "forms", + "modals", + "popups", + "popups-to-escape-sandbox", + "same-origin", + "scripts", + ].map((toLift) => `allow-${toLift}`); + + // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#referrerpolicy + const referrerPolicy = "origin"; + + const styles = { + "aspect-ratio": "4/1", + "background-color": "transparent", + border: "none", + display: "block", + width: "100%", + }; + + this.iframe.allow = permissionsPolicies.join("; "); + this.iframe.referrerPolicy = referrerPolicy; + this.iframe.sandbox.add(...liftedSandboxRestrictions); + this.iframe.src = this.hostUrl.toString(); + for (const [k, v] of Object.entries(styles)) { + this.iframe.style.setProperty(k, v); + } + + let el: HTMLElement | null; + // biome-ignore lint/suspicious/noAssignInExpressions: it's on purpose + while ((el = document.getElementById(this.iframe.id))) { + console.log("reinstalling idOS iframe..."); + container.removeChild(el); + } + container.appendChild(this.iframe); + + return new Promise((resolve) => + this.iframe.addEventListener( + "load", + () => { + resolve(); + }, + { once: true }, + ), + ); + } + + #showEnclave() { + // biome-ignore lint/style/noNonNullAssertion: Make the explosion visible. + this.iframe.parentElement!.classList.add("visible"); + } + + #hideEnclave() { + // biome-ignore lint/style/noNonNullAssertion: Make the explosion visible. + this.iframe.parentElement!.classList.remove("visible"); + } + + // biome-ignore lint/suspicious/noExplicitAny: `any` is fine here. We will type it properly later. + async #requestToEnclave(request: any) { + return new Promise((resolve, reject) => { + const { port1, port2 } = new MessageChannel(); + + port1.onmessage = ({ data }) => { + port1.close(); + data.error ? reject(data.error) : resolve(data.result); + }; + + // biome-ignore lint/style/noNonNullAssertion: Make the explosion visible. + this.iframe.contentWindow!.postMessage(request, this.hostUrl.origin, [port2]); + }); + } + + async backupPasswordOrSecret( + backupFn: (data: BackupPasswordInfo) => Promise, + ): Promise { + const abortController = new AbortController(); + this.#showEnclave(); + + window.addEventListener( + "message", + async (event) => { + if (event.data.type !== "idOS:store" || event.origin !== this.hostUrl.origin) return; + + let status = ""; + + try { + status = "success"; + await backupFn(event); + this.#hideEnclave(); + } catch (error) { + status = "failure"; + this.#hideEnclave(); + } + + event.ports[0].postMessage({ + result: { + type: "idOS:store", + status, + }, + }); + event.ports[0].close(); + abortController.abort(); + }, + { signal: abortController.signal }, + ); + + try { + await this.#requestToEnclave({ + backupPasswordOrSecret: {}, + }); + } catch (error) { + console.error(error); + } finally { + this.#hideEnclave(); + } + } + + async discoverUserEncryptionPublicKey( + userId: string, + ): Promise { + if (this.options.mode !== "new") + throw new Error("You can only call `discoverUserEncryptionPublicKey` when mode is `new`."); + + const userEncryptionPublicKey = await this.ready(userId); + + return { + userId, + userEncryptionPublicKey: base64Encode(userEncryptionPublicKey), + }; + } +} + +function throwNew(ErrorClass: ErrorConstructor, ...args: Parameters): never { + throw new ErrorClass(...args); +} diff --git a/packages/@controllers/src/secure-enclave/index.ts b/packages/@controllers/src/secure-enclave/index.ts new file mode 100644 index 000000000..4913b14cd --- /dev/null +++ b/packages/@controllers/src/secure-enclave/index.ts @@ -0,0 +1,2 @@ +export { IframeEnclave } from "./iframe-enclave"; +export { MetaMaskSnapEnclave } from "./metamask-snap-enclave"; diff --git a/packages/@controllers/src/secure-enclave/metamask-snap-enclave.ts b/packages/@controllers/src/secure-enclave/metamask-snap-enclave.ts new file mode 100644 index 000000000..bcf02b225 --- /dev/null +++ b/packages/@controllers/src/secure-enclave/metamask-snap-enclave.ts @@ -0,0 +1,114 @@ +import type { idOSCredential } from "@idos-network/core"; +import type { + DiscoverUserEncryptionPublicKeyResponse, + EnclaveProvider, + StoredData, +} from "./src/types"; + +export class MetaMaskSnapEnclave implements EnclaveProvider { + // biome-ignore lint/suspicious/noExplicitAny: Types will be added later + enclaveHost: any; + snapId: string; + + constructor(_?: Record) { + // biome-ignore lint/suspicious/noExplicitAny: Types will be added later + this.enclaveHost = (window as any).ethereum; + this.snapId = "npm:@idos-network/metamask-snap-enclave"; + } + filterCredentials( + credentials: Record[], + privateFieldFilters: { pick: Record; omit: Record }, + ): Promise { + console.log(credentials, privateFieldFilters); + throw new Error("Method not implemented."); + } + + async discoverUserEncryptionPublicKey(): Promise { + throw new Error("Method not implemented."); + } + + filterCredentialsByCountries( + credentials: Record[], + countries: string[], + ): Promise { + console.log(credentials, countries); + + throw new Error("Method not implemented."); + } + async load(): Promise { + const snaps = await this.enclaveHost.request({ method: "wallet_getSnaps" }); + // biome-ignore lint/suspicious/noExplicitAny: Types will be added later + const connected = Object.values(snaps).find((snap: any) => snap.id === this.snapId); + + if (!connected) + await this.enclaveHost.request({ + method: "wallet_requestSnaps", + params: { [this.snapId]: {} }, + }); + + const storage = JSON.parse((await this.invokeSnap("storage")) || {}); + storage.encryptionPublicKey &&= Uint8Array.from(Object.values(storage.encryptionPublicKey)); + + return storage; + } + + async ready( + userId?: string, + signerAddress?: string, + signerPublicKey?: string, + ): Promise { + let { encryptionPublicKey } = JSON.parse( + await this.invokeSnap("storage", { userId, signerAddress, signerPublicKey }), + ); + + encryptionPublicKey ||= await this.invokeSnap("init"); + encryptionPublicKey &&= Uint8Array.from(Object.values(encryptionPublicKey)); + + return encryptionPublicKey; + } + + invokeSnap(method: string, params: unknown = {}) { + return this.enclaveHost.request({ + method: "wallet_invokeSnap", + params: { + snapId: this.snapId, + request: { method, params }, + }, + }); + } + + async store(key: string, value: string): Promise { + return this.invokeSnap("storage", { [key]: value }); + } + + async reset(): Promise { + return this.invokeSnap("reset"); + } + + updateStore(key: string, value: unknown): Promise { + return this.invokeSnap("updateStore", { key, value }); + } + + async confirm(message: string): Promise { + return this.invokeSnap("confirm", { message }); + } + + async encrypt( + message: Uint8Array, + receiverPublicKey: Uint8Array, + ): Promise<{ content: Uint8Array; encryptorPublicKey: Uint8Array }> { + await this.invokeSnap("encrypt", { message, receiverPublicKey }); + + throw new Error("The Metamask Enclave needs to be updated"); + } + + async decrypt(message: Uint8Array, senderPublicKey: Uint8Array): Promise { + const decrypted = await this.invokeSnap("decrypt", { message, senderPublicKey }); + + return Uint8Array.from(Object.values(decrypted)); + } + + async backupPasswordOrSecret(): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/packages/@controllers/src/secure-enclave/types.ts b/packages/@controllers/src/secure-enclave/types.ts new file mode 100644 index 000000000..4ce06105a --- /dev/null +++ b/packages/@controllers/src/secure-enclave/types.ts @@ -0,0 +1,67 @@ +import type { idOSCredential } from "@idos-network/core"; + +export type BackupPasswordInfo = { + data: { + payload: { + accessControlConditions: string[]; + passwordCiphers: { ciphertext: string; dataToEncryptHash: string }; + }; + }; +}; + +export interface StoredData { + encryptionPublicKey?: Uint8Array; + userId?: string; + signerAddress?: string; + signerPublicKey?: string; +} + +export interface DiscoverUserEncryptionPublicKeyResponse { + userId: string; + userEncryptionPublicKey: string; +} + +export interface EnclaveOptions { + container: string; + theme?: "light" | "dark"; + mode?: "new" | "existing"; + url?: string; + throwOnUserCancelUnlock?: boolean; +} + +export interface EnclaveProvider { + load(): Promise; + + ready( + userId?: string, + signerAddress?: string, + signerPublicKey?: string, + currentUserEncryptionPublicKey?: string, + ): Promise; + store(key: string, value: string): Promise; + reset(): Promise; + confirm(message: string): Promise; + updateStore(key: string, value: unknown): Promise; + encrypt( + message: Uint8Array, + receiverPublicKey?: Uint8Array, + ): Promise<{ content: Uint8Array; encryptorPublicKey: Uint8Array }>; + decrypt(message: Uint8Array, senderPublicKey?: Uint8Array): Promise; + discoverUserEncryptionPublicKey(userId: string): Promise; + filterCredentialsByCountries( + credentials: Record[], + countries: string[], + ): Promise; + + filterCredentials( + credentials: Record[], + privateFieldFilters: { + pick: Record; + omit: Record; + }, + ): Promise; + + backupPasswordOrSecret( + callbackFn: (response: BackupPasswordInfo) => Promise, + ): Promise; +} diff --git a/packages/issuer-sdk-js/package.json b/packages/issuer-sdk-js/package.json index 2b35b4cde..83be43437 100644 --- a/packages/issuer-sdk-js/package.json +++ b/packages/issuer-sdk-js/package.json @@ -9,6 +9,7 @@ "tweetnacl": "^1.0.3" }, "devDependencies": { + "@idos-network/controllers": "workspace:*", "@idos-network/core": "workspace:*", "@release-it/keep-a-changelog": "^5.0.0", "@types/node": "^22.7.9", diff --git a/packages/issuer-sdk-js/src/client/user.ts b/packages/issuer-sdk-js/src/client/user.ts index 0a6932a81..b11f41b9e 100644 --- a/packages/issuer-sdk-js/src/client/user.ts +++ b/packages/issuer-sdk-js/src/client/user.ts @@ -1,10 +1,10 @@ +import { IframeEnclave } from "@idos-network/controllers/secure-enclave"; import { type DelegatedWriteGrantSignatureRequest, getUserProfile as _getUserProfile, requestDWGSignature as _requestDWGSignature, hasProfile, } from "@idos-network/core"; - import type { IssuerConfig } from "./create-issuer-config"; export async function checkUserProfile({ kwilClient }: IssuerConfig, address: string) { @@ -22,3 +22,21 @@ export async function getUserProfile({ kwilClient }: IssuerConfig) { const user = await _getUserProfile(kwilClient); return user; } + +/** + * Get the public key of the user's encryption key + */ +export async function getUserEncryptionPublicKey(userId: string, container: string) { + let enclave: IframeEnclave | null = new IframeEnclave({ + container, + mode: "new", + }); + + await enclave.load(); + await enclave.reset(); + const publicKey = await enclave.discoverUserEncryptionPublicKey(userId); + enclave = null; + document.querySelector("#idOS-enclave")?.children[0].remove(); + + return publicKey; +} diff --git a/packages/issuer-sdk-js/tsconfig.json b/packages/issuer-sdk-js/tsconfig.json index f2a1ecfae..05d579b05 100644 --- a/packages/issuer-sdk-js/tsconfig.json +++ b/packages/issuer-sdk-js/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "ESNext", - "lib": ["ESNext"], + "lib": ["DOM", "ESNext"], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6991fad4..81510bcff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -732,7 +732,7 @@ importers: version: 17.10.0(typescript@5.6.3) tsup: specifier: 8.0.2 - version: 8.0.2(@swc/core@1.10.1)(postcss@8.5.1)(ts-node@10.9.1(@swc/core@1.10.1)(@types/node@22.10.2)(typescript@5.6.3))(typescript@5.6.3) + version: 8.0.2(@swc/core@1.10.1)(postcss@8.5.1)(ts-node@10.9.1(@swc/core@1.10.1)(typescript@5.6.3))(typescript@5.6.3) typescript: specifier: ^5.2.2 version: 5.6.3 @@ -837,6 +837,9 @@ importers: specifier: ^1.0.3 version: 1.0.3 devDependencies: + '@idos-network/controllers': + specifier: workspace:* + version: link:../@controllers '@idos-network/core': specifier: workspace:* version: link:../@core @@ -16438,7 +16441,7 @@ snapshots: - utf-8-validate - zod - '@wagmi/connectors@5.7.8(@types/react@18.3.18)(@wagmi/core@2.16.5(@tanstack/query-core@5.66.4)(@types/react@18.3.18)(react@18.3.1)(typescript@5.6.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.23.5(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.24.1)))(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(supports-color@8.1.1)(typescript@5.6.3)(utf-8-validate@5.0.10)(viem@2.23.5(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.24.1))(zod@3.24.1)': + '@wagmi/connectors@5.7.8(@types/react@18.3.18)(@wagmi/core@2.16.5(@tanstack/query-core@5.66.4)(@types/react@18.3.18)(react@18.3.1)(typescript@5.6.3)(viem@2.23.5(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.24.1)))(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(supports-color@8.1.1)(typescript@5.6.3)(utf-8-validate@5.0.10)(viem@2.23.5(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.24.1))(zod@3.24.1)': dependencies: '@coinbase/wallet-sdk': 4.3.0 '@metamask/sdk': 0.32.0(bufferutil@4.0.8)(encoding@0.1.13)(supports-color@8.1.1)(utf-8-validate@5.0.10) @@ -21688,13 +21691,13 @@ snapshots: postcss: 8.5.1 ts-node: 10.9.1(@swc/core@1.10.1)(@types/node@22.10.2)(typescript@5.6.3) - postcss-load-config@4.0.2(postcss@8.5.1)(ts-node@10.9.1(@types/node@20.17.10)(typescript@5.6.3)): + postcss-load-config@4.0.2(postcss@8.5.1)(ts-node@10.9.1(@swc/core@1.10.1)(typescript@5.6.3)): dependencies: lilconfig: 3.1.3 yaml: 2.6.1 optionalDependencies: postcss: 8.5.1 - ts-node: 10.9.1(@swc/core@1.10.1)(@types/node@20.17.10)(typescript@5.6.3) + ts-node: 10.9.1(@swc/core@1.10.1)(@types/node@22.10.2)(typescript@5.6.3) postcss-nested@6.2.0(postcss@8.4.49): dependencies: @@ -22684,7 +22687,7 @@ snapshots: postcss: 8.5.1 postcss-import: 15.1.0(postcss@8.5.1) postcss-js: 4.0.1(postcss@8.5.1) - postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.1(@types/node@20.17.10)(typescript@5.6.3)) + postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.1(@swc/core@1.10.1)(@types/node@20.17.10)(typescript@5.6.3)) postcss-nested: 6.2.0(postcss@8.5.1) postcss-selector-parser: 6.1.2 resolve: 1.22.9 @@ -22875,6 +22878,30 @@ snapshots: - supports-color - ts-node + tsup@8.0.2(@swc/core@1.10.1)(postcss@8.5.1)(ts-node@10.9.1(@swc/core@1.10.1)(typescript@5.6.3))(typescript@5.6.3): + dependencies: + bundle-require: 4.2.1(esbuild@0.19.12) + cac: 6.7.14 + chokidar: 3.6.0 + debug: 4.4.0(supports-color@8.1.1) + esbuild: 0.19.12 + execa: 5.1.1 + globby: 11.1.0 + joycon: 3.1.1 + postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.1(@swc/core@1.10.1)(typescript@5.6.3)) + resolve-from: 5.0.0 + rollup: 4.28.1 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tree-kill: 1.2.2 + optionalDependencies: + '@swc/core': 1.10.1(@swc/helpers@0.5.15) + postcss: 8.5.1 + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + - ts-node + tsup@8.0.2(@swc/core@1.10.1)(postcss@8.5.1)(typescript@5.7.3): dependencies: bundle-require: 4.2.1(esbuild@0.19.12) @@ -22885,7 +22912,7 @@ snapshots: execa: 5.1.1 globby: 11.1.0 joycon: 3.1.1 - postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.1(@types/node@20.17.10)(typescript@5.6.3)) + postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.1(@swc/core@1.10.1)(@types/node@22.10.2)(typescript@5.6.3)) resolve-from: 5.0.0 rollup: 4.28.1 source-map: 0.8.0-beta.0 @@ -23533,7 +23560,7 @@ snapshots: wagmi@2.14.12(@tanstack/query-core@5.66.4)(@tanstack/react-query@5.45.1(react@18.3.1))(@types/react@18.3.18)(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(supports-color@8.1.1)(typescript@5.6.3)(utf-8-validate@5.0.10)(viem@2.23.5(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.24.1))(zod@3.24.1): dependencies: '@tanstack/react-query': 5.45.1(react@18.3.1) - '@wagmi/connectors': 5.7.8(@types/react@18.3.18)(@wagmi/core@2.16.5(@tanstack/query-core@5.66.4)(@types/react@18.3.18)(react@18.3.1)(typescript@5.6.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.23.5(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.24.1)))(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(supports-color@8.1.1)(typescript@5.6.3)(utf-8-validate@5.0.10)(viem@2.23.5(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.24.1))(zod@3.24.1) + '@wagmi/connectors': 5.7.8(@types/react@18.3.18)(@wagmi/core@2.16.5(@tanstack/query-core@5.66.4)(@types/react@18.3.18)(react@18.3.1)(typescript@5.6.3)(viem@2.23.5(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.24.1)))(bufferutil@4.0.8)(encoding@0.1.13)(react@18.3.1)(supports-color@8.1.1)(typescript@5.6.3)(utf-8-validate@5.0.10)(viem@2.23.5(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.24.1))(zod@3.24.1) '@wagmi/core': 2.16.5(@tanstack/query-core@5.66.4)(@types/react@18.3.18)(react@18.3.1)(typescript@5.6.3)(use-sync-external-store@1.4.0(react@18.3.1))(viem@2.23.5(bufferutil@4.0.8)(typescript@5.6.3)(utf-8-validate@5.0.10)(zod@3.24.1)) react: 18.3.1 use-sync-external-store: 1.4.0(react@18.3.1)