From 7c880991694de2b88245a62f4f2e4549985fd474 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 6 Mar 2025 16:50:09 -0500 Subject: [PATCH] feat: add minikit provider (#2082) --- package.json | 2 + src/DefaultOnchainKitProviders.test.tsx | 104 ++++++++ src/DefaultOnchainKitProviders.tsx | 59 +++++ src/OnchainKitProvider.tsx | 58 +--- src/core/createWagmiConfig.ts | 15 +- src/core/types.ts | 3 + src/minikit/MiniKitProvider.test.tsx | 334 ++++++++++++++++++++++++ src/minikit/MiniKitProvider.tsx | 148 +++++++++++ src/minikit/index.ts | 2 + src/minikit/types.ts | 27 ++ yarn.lock | 69 +++++ 11 files changed, 766 insertions(+), 55 deletions(-) create mode 100644 src/DefaultOnchainKitProviders.test.tsx create mode 100644 src/DefaultOnchainKitProviders.tsx create mode 100644 src/minikit/MiniKitProvider.test.tsx create mode 100644 src/minikit/MiniKitProvider.tsx create mode 100644 src/minikit/index.ts create mode 100644 src/minikit/types.ts diff --git a/package.json b/package.json index eb3c8ccab7..d9c959209b 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "react-dom": "^18 || ^19" }, "dependencies": { + "@farcaster/frame-sdk": "^0.0.28", + "@farcaster/frame-wagmi-connector": "^0.0.16", "@tanstack/react-query": "^5", "clsx": "^2.1.1", "graphql": "^14 || ^15 || ^16", diff --git a/src/DefaultOnchainKitProviders.test.tsx b/src/DefaultOnchainKitProviders.test.tsx new file mode 100644 index 0000000000..8f2f9682f0 --- /dev/null +++ b/src/DefaultOnchainKitProviders.test.tsx @@ -0,0 +1,104 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { render, screen } from '@testing-library/react'; +import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest'; +import { http, WagmiProvider, createConfig } from 'wagmi'; +import { DefaultOnchainKitProviders } from './DefaultOnchainKitProviders'; +import { useProviderDependencies } from './internal/hooks/useProviderDependencies'; + +const queryClient = new QueryClient(); +const wagmiConfig = createConfig({ + chains: [ + { + id: 1, + name: 'Mock Chain', + nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 }, + rpcUrls: { default: { http: ['http://localhost'] } }, + }, + ], + connectors: [], + transports: { [1]: http() }, +}); + +vi.mock('wagmi', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + WagmiProvider: vi.fn(({ children }) => ( +
{children}
+ )), + }; +}); + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + QueryClientProvider: vi.fn(({ children }) => ( +
{children}
+ )), + }; +}); + +vi.mock('./internal/hooks/useProviderDependencies', () => ({ + useProviderDependencies: vi.fn(() => ({ + providedWagmiConfig: vi.fn(), + providedQueryClient: vi.fn(), + })), +})); + +describe('DefaultOnchainKitProviders', () => { + beforeEach(() => { + (useProviderDependencies as Mock).mockReturnValue({ + providedWagmiConfig: false, + providedQueryClient: false, + }); + }); + + it('should wrap children in default providers', () => { + render( + +
Test Child
+
, + ); + + expect(screen.getByText('Test Child')).toBeInTheDocument(); + expect(screen.queryAllByTestId('wagmi-provider')).toHaveLength(1); + expect(screen.queryAllByTestId('query-client-provider')).toHaveLength(1); + }); + + it('should not render duplicate default providers when a wagmi provider already exists', () => { + (useProviderDependencies as Mock).mockReturnValue({ + providedWagmiConfig: wagmiConfig, + providedQueryClient: null, + }); + + render( + + +
Test Child
+
+
, + ); + + expect(screen.getByText('Test Child')).toBeInTheDocument(); + expect(screen.queryAllByTestId('wagmi-provider')).toHaveLength(1); + }); + + it('should not render duplicate default providers when a query client already exists', () => { + (useProviderDependencies as Mock).mockReturnValue({ + providedWagmiConfig: null, + providedQueryClient: queryClient, + }); + + render( + + +
Test Child
+
+
, + ); + + expect(screen.getByText('Test Child')).toBeInTheDocument(); + expect(screen.queryAllByTestId('query-client-provider')).toHaveLength(1); + }); +}); diff --git a/src/DefaultOnchainKitProviders.tsx b/src/DefaultOnchainKitProviders.tsx new file mode 100644 index 0000000000..8388e7c346 --- /dev/null +++ b/src/DefaultOnchainKitProviders.tsx @@ -0,0 +1,59 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { type PropsWithChildren, useMemo } from 'react'; +import { WagmiProvider } from 'wagmi'; +import { coinbaseWallet } from 'wagmi/connectors'; +import { createWagmiConfig } from './core/createWagmiConfig'; +import type { CreateWagmiConfigParams } from './core/types'; +import { useProviderDependencies } from './internal/hooks/useProviderDependencies'; + +export function DefaultOnchainKitProviders({ + apiKey, + appName, + appLogoUrl, + connectors = [ + coinbaseWallet({ + appName, + appLogoUrl, + preference: 'all', + }), + ], + children, +}: PropsWithChildren) { + // Check the React context for WagmiProvider and QueryClientProvider + const { providedWagmiConfig, providedQueryClient } = + useProviderDependencies(); + + const defaultConfig = useMemo(() => { + // IMPORTANT: Don't create a new Wagmi configuration if one already exists + // This prevents the user-provided WagmiConfig from being overridden + return ( + providedWagmiConfig || + createWagmiConfig({ + apiKey, + appName, + appLogoUrl, + connectors, + }) + ); + }, [apiKey, appName, appLogoUrl, connectors, providedWagmiConfig]); + + const defaultQueryClient = useMemo(() => { + // IMPORTANT: Don't create a new QueryClient if one already exists + // This prevents the user-provided QueryClient from being overridden + return providedQueryClient || new QueryClient(); + }, [providedQueryClient]); + + // If both dependencies are missing, return a context with default parent providers + // If only one dependency is provided, expect the user to also provide the missing one + if (!providedWagmiConfig && !providedQueryClient) { + return ( + + + {children} + + + ); + } + + return children; +} diff --git a/src/OnchainKitProvider.tsx b/src/OnchainKitProvider.tsx index 91529858bd..cbbff96ccc 100644 --- a/src/OnchainKitProvider.tsx +++ b/src/OnchainKitProvider.tsx @@ -4,15 +4,12 @@ import { ONCHAIN_KIT_CONFIG, setOnchainKitConfig, } from '@/core/OnchainKitConfig'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { createContext, useMemo } from 'react'; -import { WagmiProvider } from 'wagmi'; +import { DefaultOnchainKitProviders } from './DefaultOnchainKitProviders'; import OnchainKitProviderBoundary from './OnchainKitProviderBoundary'; import { DEFAULT_PRIVACY_URL, DEFAULT_TERMS_URL } from './core/constants'; -import { createWagmiConfig } from './core/createWagmiConfig'; import type { OnchainKitContextType } from './core/types'; import { COINBASE_VERIFIED_ACCOUNT_SCHEMA_ID } from './identity/constants'; -import { useProviderDependencies } from './internal/hooks/useProviderDependencies'; import { checkHashLength } from './internal/utils/checkHashLength'; import type { OnchainKitProviderReact } from './types'; @@ -85,50 +82,15 @@ export function OnchainKitProvider({ sessionId, ]); - // Check the React context for WagmiProvider and QueryClientProvider - const { providedWagmiConfig, providedQueryClient } = - useProviderDependencies(); - - const defaultConfig = useMemo(() => { - // IMPORTANT: Don't create a new Wagmi configuration if one already exists - // This prevents the user-provided WagmiConfig from being overridden - return ( - providedWagmiConfig || - createWagmiConfig({ - apiKey, - appName: value.config.appearance.name, - appLogoUrl: value.config.appearance.logo, - }) - ); - }, [ - apiKey, - providedWagmiConfig, - value.config.appearance.name, - value.config.appearance.logo, - ]); - const defaultQueryClient = useMemo(() => { - // IMPORTANT: Don't create a new QueryClient if one already exists - // This prevents the user-provided QueryClient from being overridden - return providedQueryClient || new QueryClient(); - }, [providedQueryClient]); - - // If both dependencies are missing, return a context with default parent providers - // If only one dependency is provided, expect the user to also provide the missing one - if (!providedWagmiConfig && !providedQueryClient) { - return ( - - - - {children} - - - - ); - } - return ( - - {children} - + + + {children} + + ); } diff --git a/src/core/createWagmiConfig.ts b/src/core/createWagmiConfig.ts index 0a047ff765..f94cd35d19 100644 --- a/src/core/createWagmiConfig.ts +++ b/src/core/createWagmiConfig.ts @@ -11,16 +11,17 @@ export const createWagmiConfig = ({ apiKey, appName, appLogoUrl, + connectors = [ + coinbaseWallet({ + appName, + appLogoUrl, + preference: 'all', + }), + ], }: CreateWagmiConfigParams) => { return createConfig({ chains: [base, baseSepolia], - connectors: [ - coinbaseWallet({ - appName, - appLogoUrl, - preference: 'all', - }), - ], + connectors, storage: createStorage({ storage: cookieStorage, }), diff --git a/src/core/types.ts b/src/core/types.ts index a2fc4220e8..3bdde13ef1 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,5 +1,6 @@ import type { EASSchemaUid } from '@/identity/types'; import type { Address, Chain } from 'viem'; +import type { CreateConnectorFn } from 'wagmi'; /** * Note: exported as public Type @@ -38,6 +39,8 @@ export type CreateWagmiConfigParams = { appName?: string; /** Application logo URL */ appLogoUrl?: string; + /** Connectors to use, defaults to coinbaseWallet */ + connectors?: CreateConnectorFn[]; }; /** diff --git a/src/minikit/MiniKitProvider.test.tsx b/src/minikit/MiniKitProvider.test.tsx new file mode 100644 index 0000000000..004e504b20 --- /dev/null +++ b/src/minikit/MiniKitProvider.test.tsx @@ -0,0 +1,334 @@ +import sdk, { type Context } from '@farcaster/frame-sdk'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { render } from '@testing-library/react'; +import { act, useContext } from 'react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { http, WagmiProvider, createConfig } from 'wagmi'; +import { base } from 'wagmi/chains'; +import { MiniKitContext, MiniKitProvider } from './MiniKitProvider'; +import type { MiniKitContextType } from './types'; + +vi.mock('@farcaster/frame-sdk', () => { + let listeners: Record void> = {}; + + return { + default: { + emit: vi.fn((event: string, data: object) => { + if (listeners[event]) { + listeners[event](data); + } + }), + on: vi.fn((event, callback) => { + listeners[event] = callback; + }), + removeAllListeners: vi.fn(() => { + listeners = {}; + }), + context: vi.fn(), + }, + }; +}); + +vi.mock('@farcaster/frame-wagmi-connector', () => ({ + farcasterFrame: vi.fn(), +})); + +const mockConfig = { + chains: [base], + connectors: [], + transports: { [base.id]: http() }, +} as const; + +const queryClient = new QueryClient(); + +describe('MiniKitProvider', () => { + beforeEach(() => { + vi.mocked(sdk).context = Promise.resolve({ + client: { + notificationDetails: null, + added: false, + safeAreaInsets: { top: 0, bottom: 0, left: 0, right: 0 }, + }, + }) as unknown as Promise; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should load context initially', async () => { + let contextValue: MiniKitContextType | undefined; + + function TestComponent() { + contextValue = useContext(MiniKitContext); + return null; + } + + render( + + + + + + + , + ); + + expect(contextValue?.context).toBeNull(); + expect(contextValue?.notificationProxyUrl).toBe('/api/notify'); + expect(typeof contextValue?.updateClientContext).toBe('function'); + + await act(() => Promise.resolve()); + + expect(contextValue?.context).not.toBeNull(); + }); + + it('should handle rejected context', async () => { + let contextValue: MiniKitContextType | undefined; + + function TestComponent() { + contextValue = useContext(MiniKitContext); + contextValue?.updateClientContext({ + details: { + url: 'https://example.com', + token: '1234567890', + }, + }); + return null; + } + + vi.mocked(sdk).context = Promise.reject(); + + await act(async () => { + render( + + + + + + + , + ); + }); + + expect(contextValue?.context).toBeNull(); + }); + + it('should render children with safe area insets', async () => { + const { container } = render( + + + +
Test Child
+
+
+
, + ); + + await act(() => Promise.resolve()); + + expect(container.querySelector('div')).toHaveStyle({ + paddingTop: '0px', + paddingBottom: '0px', + paddingLeft: '0px', + paddingRight: '0px', + }); + }); + + it('should set up frame event listeners', async () => { + render( + + + +
Test Child
+
+
+
, + ); + + await act(() => Promise.resolve()); + + expect(sdk.on).toHaveBeenCalledWith('frameAdded', expect.any(Function)); + expect(sdk.on).toHaveBeenCalledWith('frameRemoved', expect.any(Function)); + expect(sdk.on).toHaveBeenCalledWith( + 'notificationsEnabled', + expect.any(Function), + ); + expect(sdk.on).toHaveBeenCalledWith( + 'notificationsDisabled', + expect.any(Function), + ); + }); + + it('should clean up listeners on unmount', async () => { + const { unmount } = render( + + + +
Test Child
+
+
+
, + ); + + await act(() => Promise.resolve()); + unmount(); + + expect(sdk.removeAllListeners).toHaveBeenCalled(); + }); + + it('should update context when frame is added and removed', async () => { + let contextValue: MiniKitContextType | undefined; + + function TestComponent() { + const context = useContext(MiniKitContext); + contextValue = context; + return null; + } + + render( + + + + + + + , + ); + + await act(() => Promise.resolve()); + + const notificationDetails = { + url: 'https://example.com', + token: '1234567890', + }; + + act(() => { + sdk.emit('frameAdded', { + notificationDetails, + }); + }); + + expect(contextValue?.context?.client.notificationDetails).toEqual( + notificationDetails, + ); + expect(contextValue?.context?.client.added).toBe(true); + + act(() => { + sdk.emit('frameRemoved'); + }); + + expect(contextValue?.context?.client.notificationDetails).toBeUndefined(); + expect(contextValue?.context?.client.added).toBe(false); + }); + + it('should log an error when frameAddRejected is emitted', async () => { + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(vi.fn()); + + render( + + + +
Test Child
+
+
+
, + ); + + await act(() => Promise.resolve()); + + sdk.emit('frameAddRejected', { + reason: 'invalid_domain_manifest', + }); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Frame add rejected', + 'invalid_domain_manifest', + ); + }); + + it('should update context when notifications are enabled and remove when disabled', async () => { + let contextValue: MiniKitContextType | undefined; + + function TestComponent() { + const context = useContext(MiniKitContext); + contextValue = context; + return null; + } + + render( + + + + + + + , + ); + + await act(() => Promise.resolve()); + + const notificationDetails = { + url: 'https://example.com', + token: '1234567890', + }; + + act(() => { + sdk.emit('notificationsEnabled', { + notificationDetails, + }); + }); + + expect(contextValue?.context?.client.notificationDetails).toEqual( + notificationDetails, + ); + + act(() => { + sdk.emit('notificationsDisabled'); + }); + + expect(contextValue?.context?.client.notificationDetails).toBeUndefined(); + }); + + it('should handle context fetch error', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + vi.mocked(sdk).context = Promise.reject(new Error('Test error')); + + let contextValue: MiniKitContextType | undefined; + function TestComponent() { + contextValue = useContext(MiniKitContext); + return null; + } + + await act(async () => { + render( + + + + + + + , + ); + }); + + expect(consoleSpy).toHaveBeenCalledWith( + 'Error fetching context:', + expect.any(Error), + ); + expect(contextValue?.context).toBeNull(); + + consoleSpy.mockRestore(); + }); +}); diff --git a/src/minikit/MiniKitProvider.tsx b/src/minikit/MiniKitProvider.tsx new file mode 100644 index 0000000000..82d095fa2b --- /dev/null +++ b/src/minikit/MiniKitProvider.tsx @@ -0,0 +1,148 @@ +'use client'; + +import { DefaultOnchainKitProviders } from '@/DefaultOnchainKitProviders'; +import { OnchainKitProvider } from '@/OnchainKitProvider'; +import type { OnchainKitProviderReact } from '@/types'; +import sdk, { type Context } from '@farcaster/frame-sdk'; +import { farcasterFrame } from '@farcaster/frame-wagmi-connector'; +import { + createContext, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; +import { coinbaseWallet } from 'wagmi/connectors'; +import type { + MiniKitContextType, + MiniKitProviderReact, + UpdateClientContextParams, +} from './types'; + +export const emptyContext = {} as MiniKitContextType; + +export const MiniKitContext = createContext(emptyContext); + +/** + * Provides the MiniKit React Context to the app. + */ +export function MiniKitProvider({ + children, + notificationProxyUrl = '/api/notify', + ...onchainKitProps +}: MiniKitProviderReact & OnchainKitProviderReact) { + const [context, setContext] = useState(null); + + useEffect(() => { + sdk.on('frameAdded', ({ notificationDetails }) => { + if (notificationDetails) { + updateClientContext({ + details: notificationDetails, + frameAdded: true, + }); + } + }); + + sdk.on('frameAddRejected', ({ reason }) => { + console.error('Frame add rejected', reason); + }); + + sdk.on('frameRemoved', () => { + updateClientContext({ + details: undefined, + frameAdded: false, + }); + }); + + sdk.on('notificationsEnabled', ({ notificationDetails }) => { + updateClientContext({ + details: notificationDetails, + }); + }); + + sdk.on('notificationsDisabled', () => { + updateClientContext({ + details: undefined, + }); + }); + + async function fetchContext() { + try { + // if not running in a frame, context resolves as undefined + const context = await sdk.context; + setContext(context); + } catch (error) { + console.error('Error fetching context:', error); + } + } + + fetchContext(); + + return () => { + sdk.removeAllListeners(); + }; + }, []); + + const updateClientContext = useCallback( + ({ details, frameAdded }: UpdateClientContextParams) => { + setContext((prevContext) => { + if (!prevContext) { + return null; + } + return { + ...prevContext, + client: { + ...prevContext.client, + notificationDetails: details ?? undefined, + added: frameAdded ?? prevContext.client.added, + }, + }; + }); + }, + [], + ); + + const connectors = useMemo(() => { + return [ + context // if context is set, the app is running in a frame, use farcasterFrame connector + ? farcasterFrame() + : coinbaseWallet({ + appName: process.env.NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME, + appLogoUrl: process.env.NEXT_PUBLIC_ICON_URL, + preference: 'all', + }), + ]; + }, [context]); + + const value = useMemo(() => { + return { + context, + updateClientContext, + notificationProxyUrl, + }; + }, [updateClientContext, notificationProxyUrl, context]); + + return ( + + + +
+ {children} +
+
+
+
+ ); +} diff --git a/src/minikit/index.ts b/src/minikit/index.ts new file mode 100644 index 0000000000..9dc1fda422 --- /dev/null +++ b/src/minikit/index.ts @@ -0,0 +1,2 @@ +export { MiniKitProvider } from './MiniKitProvider'; +export type { MiniKitProviderReact } from './types'; diff --git a/src/minikit/types.ts b/src/minikit/types.ts new file mode 100644 index 0000000000..bd6c088865 --- /dev/null +++ b/src/minikit/types.ts @@ -0,0 +1,27 @@ +import type { Context, FrameNotificationDetails } from '@farcaster/frame-sdk'; + +export type UpdateClientContextParams = { + details?: FrameNotificationDetails | null; + frameAdded?: boolean; +}; + +/** + * Note: exported as public Type + */ +export type MiniKitProviderReact = { + children: React.ReactNode; + /* + * The URL of the notification proxy. + * Notifications are sent by posting a cross origin request to a url returned by + * the frames context. This prop allows you to set a custom proxy route for MiniKits + * notification related hooks to use. + * @default `/api/notify` + */ + notificationProxyUrl?: string; +}; + +export type MiniKitContextType = { + context: Context.FrameContext | null; + updateClientContext: (params: UpdateClientContextParams) => void; + notificationProxyUrl: string; +}; diff --git a/yarn.lock b/yarn.lock index 6b6651dcf8..657eefbf63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2037,6 +2037,8 @@ __metadata: "@biomejs/biome": "npm:1.8.3" "@changesets/cli": "npm:^2.27.10" "@chromatic-com/storybook": "npm:^1.7.0" + "@farcaster/frame-sdk": "npm:^0.0.28" + "@farcaster/frame-wagmi-connector": "npm:^0.0.16" "@storybook/addon-a11y": "npm:^8.2.9" "@storybook/addon-essentials": "npm:^8.2.9" "@storybook/addon-interactions": "npm:^8.2.9" @@ -2529,6 +2531,39 @@ __metadata: languageName: node linkType: hard +"@farcaster/frame-core@npm:0.0.26": + version: 0.0.26 + resolution: "@farcaster/frame-core@npm:0.0.26" + dependencies: + ox: "npm:^0.4.4" + zod: "npm:^3.24.1" + checksum: 4be35197af05ef13dbda9a836e2e64e00c1a99fea03dc72a6926a29831ac2c5f33c52b631672506e36a9b371a5d16027b28cd90d237f061c7fd979fc9f1147f0 + languageName: node + linkType: hard + +"@farcaster/frame-sdk@npm:^0.0.28": + version: 0.0.28 + resolution: "@farcaster/frame-sdk@npm:0.0.28" + dependencies: + "@farcaster/frame-core": "npm:0.0.26" + comlink: "npm:^4.4.2" + eventemitter3: "npm:^5.0.1" + ox: "npm:^0.4.4" + checksum: 541c0fce0b4195dc340a2c1d82ae4a9a371f700d37876e5e9d82dcf20d254ec011d20d4cd6cb335cd73128371d7f179bb267a1b809bace0684447a8279a8bea6 + languageName: node + linkType: hard + +"@farcaster/frame-wagmi-connector@npm:^0.0.16": + version: 0.0.16 + resolution: "@farcaster/frame-wagmi-connector@npm:0.0.16" + peerDependencies: + "@farcaster/frame-sdk": ^0.0.28 + "@wagmi/core": ^2.14.1 + viem: ^2.21.55 + checksum: 830fd2125821dcb97ad821b9bd0a85eacd1cd0f964d18fbbe453f8454fd22a85ab60cfd03eb6f442e6b77bcd3c0c0c063d5429596034c6911bca99cec64e75d6 + languageName: node + linkType: hard + "@graphql-typed-document-node/core@npm:^3.2.0": version: 3.2.0 resolution: "@graphql-typed-document-node/core@npm:3.2.0" @@ -6679,6 +6714,13 @@ __metadata: languageName: node linkType: hard +"comlink@npm:^4.4.2": + version: 4.4.2 + resolution: "comlink@npm:4.4.2" + checksum: 38aa1f455cf08e94aaa8fc494fd203cc0ef02ece6c21404b7931ce17567e8a72deacddab98aa5650cfd78332ff24c34610586f6fb27fd19dc77e753ed1980deb + languageName: node + linkType: hard + "commander@npm:^12.1.0": version: 12.1.0 resolution: "commander@npm:12.1.0" @@ -11499,6 +11541,26 @@ __metadata: languageName: node linkType: hard +"ox@npm:^0.4.4": + version: 0.4.4 + resolution: "ox@npm:0.4.4" + dependencies: + "@adraffy/ens-normalize": "npm:^1.10.1" + "@noble/curves": "npm:^1.6.0" + "@noble/hashes": "npm:^1.5.0" + "@scure/bip32": "npm:^1.5.0" + "@scure/bip39": "npm:^1.4.0" + abitype: "npm:^1.0.6" + eventemitter3: "npm:5.0.1" + peerDependencies: + typescript: ">=5.4.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: ce1539ec41c97f6d8386ea8f0cdd85d4af025d3b7a6762607aa1a6545f550ca637abba8c9be4b26f6bf2136f16ccd579454e6976ee06ddde7225ac8fcfd4254d + languageName: node + linkType: hard + "p-defer@npm:^1.0.0": version: 1.0.0 resolution: "p-defer@npm:1.0.0" @@ -15270,6 +15332,13 @@ __metadata: languageName: node linkType: hard +"zod@npm:^3.24.1": + version: 3.24.1 + resolution: "zod@npm:3.24.1" + checksum: 0223d21dbaa15d8928fe0da3b54696391d8e3e1e2d0283a1a070b5980a1dbba945ce631c2d1eccc088fdbad0f2dfa40155590bf83732d3ac4fcca2cc9237591b + languageName: node + linkType: hard + "zustand@npm:5.0.0": version: 5.0.0 resolution: "zustand@npm:5.0.0"