@openzeppelin/contracts-ui-builder-adapter-evm
Version:
EVM Adapter for Contracts UI Builder
256 lines (199 loc) • 14.1 kB
Markdown
# EVM Adapter Wallet Module
This directory contains the wallet integration layer for the EVM adapter, providing all wallet-related UI, hooks, context, and utilities for EVM-compatible chains using the `wagmi` library.
## Architectural Approach: UI Kit Extensibility
The EVM ecosystem is mature and features a wide variety of wallet libraries and UI kits (e.g., RainbowKit, ConnectKit, Web3Modal). To accommodate this, the `EvmAdapter`'s wallet module is designed to be highly extensible. It uses a singleton manager (`EvmUiKitManager`) to dynamically configure and load different UI kits at runtime.
This architecture allows applications to switch between different wallet UIs (like RainbowKit or a set of custom-styled components) through configuration, without changing the application code.
**Note:** This complex, manager-based architecture is specific to the needs of the EVM ecosystem. Other adapters for ecosystems with a single, primary wallet (like the `MidnightAdapter` for the Lace wallet) can use a much simpler, more direct implementation. The `MidnightWalletProvider` serves as a good example of a minimal implementation without UI kit management.
## Purpose
- **UI Environment Provision**: Sets up a stable UI environment for EVM wallet interactions. The adapter exports `EvmWalletUiRoot`, a React component that internally uses a singleton `EvmUiKitManager` to manage the `WagmiConfig`, UI kit assets (like RainbowKit's provider and CSS), and overall state. This root component ensures `WagmiProvider` and `QueryClientProvider` are always rendered, aiming to prevent UI flicker during network or configuration changes.
- **Facade Hooks**: Exposes a standardized set of React hooks (`evmFacadeHooks`) for wallet, account, and network management, wrapping `wagmi` core hooks. These are primarily consumed via `useWalletState()` from `/contracts-ui-builder-react-core`.
- **UI Kit Integration**: Supports third-party UI kits like RainbowKit, as well as a default set of custom-styled wallet UI components (`CustomConnectButton`, `CustomAccountDisplay`, `CustomNetworkSwitcher`).
- **Configuration**: Provides a flexible, layered configuration system allowing applications to define UI kit preferences and kit-specific parameters through global application configuration (`AppConfigService`), user-authored native TypeScript configuration files, and programmatic overrides.
## Directory Structure
```
wallet/
├── components/ # Core UI root (`EvmWalletUiRoot`) and custom wallet UI components
│ ├── EvmWalletUiRoot.tsx
│ ├── account/
│ ├── connect/
│ └── network/
├── context/ # React context (e.g., `WagmiProviderInitializedContext`)
├── evmUiKitManager.ts # Singleton manager for UI kit state, WagmiConfig, and assets
├── hooks/ # Facade hooks (`facade-hooks.ts`), config access (`useUiKitConfig.ts` - for baseline)
├── implementation/ # Internal Wagmi core logic (`wagmi-implementation.ts`)
├── rainbowkit/ # RainbowKit-specific modules
│ ├── rainbowkitAssetManager.ts # Handles dynamic loading of RainbowKit JS & CSS
│ ├── config-service.ts # Creates RainbowKit-specific WagmiConfig, caching
│ ├── components.tsx # Lazy-loaded RainbowKitConnectButton wrapper
│ └── componentFactory.ts # Provides RainbowKit components to the adapter
├── services/ # Utility services (e.g., `configResolutionService.ts`)
├── utils/ # General wallet utilities, connection logic, singleton manager for Wagmi impl.
└── index.ts # Barrel export for key wallet module parts (excluding internal singletons like EvmUiKitManager)
```
## Key Components & Concepts
- **`EvmAdapter` UI Methods**:
- `getEcosystemReactUiContextProvider()`: Returns the `EvmWalletUiRoot` component.
- `configureUiKit(programmaticOverrides, options)`: Configures the desired UI kit (`kitName`, `kitConfig`) and triggers the `EvmUiKitManager`.
- `getEcosystemWalletComponents()`: Returns UI components (e.g., ConnectButton) for the active kit.
- `getEcosystemReactHooks()`: Returns `evmFacadeHooks`.
- **`EvmUiKitManager`**: Singleton, manages `WagmiConfig`, selected UI kit (e.g., RainbowKit), its assets (provider component, CSS status), and initialization/error states. Not directly used by consuming apps.
- **`EvmWalletUiRoot`**: Stable React component. Subscribes to `EvmUiKitManager`. Always renders `WagmiProvider` & `QueryClientProvider`. Conditionally renders the active UI kit's specific provider (e.g., RainbowKit's) inside. Provides `WagmiProviderInitializedContext`.
- **User Native Configuration Files**: For kits like RainbowKit, users provide detailed configuration in `src/config/wallet/[kitName].config.ts`.
- **`loadConfigModule` Prop**: Passed by the application to `WalletStateProvider`, enabling the adapter to dynamically load the user-native config files.
## Configuration
The EVM adapter's wallet UI and behavior are configured through a layered system:
### 1. Application-Level Configuration (via `AppConfigService`)
This is the baseline configuration, typically set in `app.config.json` (for exported apps) or Vite environment variables (for the builder `/contracts-ui-builder-app` app).
**Path in `app.config.json`**: `globalServiceConfigs.walletui.config`
**Example `app.config.json` snippet:**
```json
{
"globalServiceConfigs": {
"walletui": {
"_comment": "Optional: Configure the Wallet UI Kit...",
"config": {
"kitName": "rainbowkit", // or "custom", "none"
"kitConfig": {
// Baseline/default kit-specific options, e.g., for custom kit:
"showInjectedConnector": true,
"components": {
"exclude": ["NetworkSwitcher"]
}
// For RainbowKit, AppConfigService might provide a default projectId if not in native file,
// but wagmiParams & providerProps are best defined in the native .ts file.
}
}
}
}
}
```
- **`kitName`**: Determines the UI kit. Supported: `'rainbowkit'`, `'custom'` (default), `'none'`.
- **`kitConfig`**: An object for kit-specific baseline settings.
### 2. User-Native Kit-Specific Configuration File (Recommended for Detailed Kit Setup)
For UI kits requiring detailed setup (like RainbowKit), applications should provide a TypeScript configuration file in their `src` directory following this convention:
`src/config/wallet/[kitName].config.ts`
**Example for RainbowKit (`src/config/wallet/rainbowkit.config.ts`):**
```typescript
// Example: src/config/wallet/rainbowkit.config.ts
// Users should import types from '@rainbow-me/rainbowkit' for their config for type safety.
// import type { RainbowKitProviderProps } from '@rainbow-me/rainbowkit';
// import { mainnet } from 'viem/chains'; // etc.
const rainbowKitAppConfig = {
// Parameters for RainbowKit's getDefaultConfig() from '@rainbow-me/rainbowkit'
wagmiParams: {
appName: 'My Awesome dApp',
projectId: 'YOUR_WALLETCONNECT_PROJECT_ID', // Essential for WalletConnect via RainbowKit
// chains: [], // Note: `chains` and `transports` will be overridden by the adapter based on its networkConfig
// Other options like `wallets`, `ssr`, etc., can be set here.
},
// Props for the <RainbowKitProvider {...} /> component
providerProps: {
// theme: darkTheme(),
// initialChain: mainnet,
// modalSize: 'compact',
// appInfo: { appName: 'My dApp', learnMoreUrl: 'https://learnmore.example' }
}, // as RainbowKitProviderProps (or a subset for type safety)
// Enhanced UI customizations using RainbowKit's native prop types
customizations: {
connectButton: {
// Uses RainbowKit's native ConnectButton props - no custom types needed!
// Refer to RainbowKit docs: https://www.rainbowkit.com/docs/connect-button
chainStatus: 'none', // Hide network switcher dropdown
accountStatus: 'full', // Show full account info when connected
label: 'Connect Wallet', // Custom connect button text
showBalance: true, // Show/hide balance display
// Responsive configurations are also supported:
// showBalance: {
// smallScreen: false,
// largeScreen: true,
// },
// accountStatus: {
// smallScreen: 'avatar',
// largeScreen: 'full',
// },
},
},
};
export default rainbowKitAppConfig;
```
- **`wagmiParams`**: Contains parameters for RainbowKit's `getDefaultConfig()` (e.g., `appName`, `projectId`). The adapter will use these but will override `chains` and `transports` based on its own `networkConfig` and `AppConfigService` RPC overrides.
- **`providerProps`**: Contains props to be spread onto the `<RainbowKitProvider />` component (e.g., `theme`, `modalSize`).
- **`customizations`**: Enhanced UI customizations that leverage RainbowKit's native prop types directly. This section allows you to configure the ConnectButton component using RainbowKit's official props.
#### RainbowKit ConnectButton Customizations
The `customizations.connectButton` section supports all native RainbowKit ConnectButton props:
- **`chainStatus`**: Controls network switcher visibility
- `'full'`: Show full chain name and icon (default)
- `'icon'`: Show only chain icon
- `'name'`: Show only chain name
- `'none'`: Hide network switcher completely
- **`accountStatus`**: Controls account info display when connected
- `'full'`: Show full account info (default)
- `'avatar'`: Show only avatar
- `'address'`: Show only address
- **`label`**: Custom text for the connect button when disconnected
- **`showBalance`**: Show/hide balance in the button (boolean or responsive object)
### 3. `loadConfigModule` (Application-Provided Importer)
The consuming application (via the `loadConfigModule` prop on `WalletStateProvider` in `react-core`) must provide a function that can dynamically import the user-native kit-specific configuration file described above. The adapter uses this callback to load the file.
**Example from `packages/builder/src/App.tsx` or exported app's `main.tsx`:**
```typescript
import { logger } from '@openzeppelin/contracts-ui-builder-utils';
const loadAppConfigModule = async (
relativePath: string
): Promise<Record<string, unknown> | null> => {
try {
// Vite resolves paths like './config/wallet/rainbowkit.config' relative to this file.
logger.info('[App] loadAppConfigModule', `Attempting to load: ${relativePath}`);
const module = await import(/* @vite-ignore */ relativePath);
logger.info('[App] loadAppConfigModule', `Successfully loaded: ${relativePath}`, module);
return module.default || module;
} catch (error) {
logger.warn('[App] loadAppConfigModule', `Failed to load ${relativePath}:`, error);
return null; // Expected if file is optional and doesn't exist
}
};
// This function is then passed to <WalletStateProvider loadConfigModule={loadAppConfigModule} />
```
### 4. Programmatic Overrides (via `EvmAdapter.configureUiKit`)
Applications can also call `adapter.configureUiKit(programmaticUiKitConfig, { loadUiKitNativeConfig: loadAppConfigModule })` at runtime to provide specific overrides.
### Configuration Precedence
1. **Programmatic Overrides**: Settings passed directly to `configureUiKit` (for `kitName` or other top-level `UiKitConfiguration` flags, and `kitConfig` properties).
2. **User-Native `.ts` File**: Content from files like `src/config/wallet/rainbowkit.config.ts` (for `kitConfig` properties).
3. **`AppConfigService` (`app.config.json` / Env Vars)**: Baseline settings (for `kitName` and `kitConfig` properties).
4. **Adapter Defaults**: Internal defaults (e.g., `kitName: 'custom'`).
## Automatic CSS Loading (RainbowKit)
When `kitName` is configured to `'rainbowkit'`, the `EvmAdapter` (via `EvmUiKitManager` and `rainbowkitAssetManager.ts`) will automatically attempt to dynamically import and inject RainbowKit's required CSS (`@rainbow-me/rainbowkit/styles.css`). **Manual import of this CSS file in the application's entry point is no longer necessary.**
## Usage within Application
The primary way to interact with wallet functionalities and UI components is through `/contracts-ui-builder-react-core`:
- **`WalletStateProvider`**: Wraps the application (or relevant parts) to provide wallet context.
- **`useWalletState()`**: Hook to access `activeAdapter`, `activeNetworkConfig`, `walletFacadeHooks`, `isAdapterLoading`, etc.
- **Derived Hooks** (e.g., `useDerivedAccountStatus`, `useDerivedConnectStatus`): Provide convenient, memoized state from facade hooks.
**Example: Rendering Wallet UI in a Header**
```typescript
import {
useWalletState,
useDerivedAccountStatus,
} from '/contracts-ui-builder-react-core';
function WalletConnectionHeader() {
const { activeAdapter, isAdapterLoading } = useWalletState();
const { isConnected } = useDerivedAccountStatus();
if (isAdapterLoading && !activeAdapter) {
return <div>Loading Wallet Adapter...</div>;
}
if (!activeAdapter?.supportsWalletConnection()) {
return null; // Or message indicating wallet connection not supported
}
const walletComponents = activeAdapter.getEcosystemWalletComponents?.();
const ConnectButton = walletComponents?.ConnectButton;
const AccountDisplay = walletComponents?.AccountDisplay;
return (
<div>
{isConnected && AccountDisplay ? <AccountDisplay /> : null}
{!isConnected && ConnectButton ? <ConnectButton /> : null}
{/* Add NetworkSwitcher if needed and available */}
</div>
);
}
```
## Notes
- The internal Wagmi implementation (`WagmiWalletImplementation`) and UI kit state management (`EvmUiKitManager`) are encapsulated within this adapter.
- For adding support for new UI kits, refer to the pattern established by the RainbowKit integration. See [Adding New UI Kits](./ADDING_NEW_UI_KITS.md) for detailed instructions.