diff --git a/content/ecosystem-adapters/architecture.mdx b/content/ecosystem-adapters/architecture.mdx new file mode 100644 index 00000000..f9a6e011 --- /dev/null +++ b/content/ecosystem-adapters/architecture.mdx @@ -0,0 +1,192 @@ +--- +title: Architecture +--- + +This page describes the capability-based architecture that underpins all OpenZeppelin Ecosystem Adapters. Understanding this architecture will help you choose the right profile for your application, consume capabilities efficiently, and build your own adapter if you need to. + +## Package Topology + +The adapter system is split across several packages with clear dependency boundaries: + +```mermaid +flowchart TD + App["Consumer Application"] --> Runtime["EcosystemRuntime"] + Runtime --> Caps["Capability Interfaces\n(@openzeppelin/ui-types)"] + + Caps --> Evm["adapter-evm"] + Caps --> Polkadot["adapter-polkadot"] + Caps --> Stellar["adapter-stellar"] + Caps --> Midnight["adapter-midnight"] + Caps --> Solana["adapter-solana"] + + App --> Vite["adapters-vite"] + + Evm --> Core["adapter-evm-core"] + Polkadot --> Core + Core --> Utils["adapter-runtime-utils"] + Stellar --> Utils + + style Caps fill:#e8eaf6,stroke:#3f51b5,color:#000 + style Utils fill:#e0f2f1,stroke:#00897b,color:#000 + style Core fill:#fff3e0,stroke:#ef6c00,color:#000 +``` + +- **`@openzeppelin/ui-types`** defines all 13 capability interfaces. It is the single source of truth. +- **`adapter-runtime-utils`** provides profile composition, lazy capability instantiation, and staged disposal. +- **`adapter-evm-core`** centralizes reusable EVM implementations shared by `adapter-evm` and `adapter-polkadot`. +- Each public adapter exposes an `ecosystemDefinition` conforming to `EcosystemExport`. + +## Capability Tiers + +Adapter functionality is decomposed into **13 capability interfaces** organized across **3 tiers**. The tiers reflect increasing levels of runtime requirements: stateless metadata, network-aware schema operations, and stateful wallet-dependent interactions. + +| Tier | Category | Network | Wallet | Capabilities | +| --- | --- | --- | --- | --- | +| **1** | Lightweight | No | No | `Addressing`, `Explorer`, `NetworkCatalog`, `UiLabels` | +| **2** | Schema | Yes | No | `ContractLoading`, `Schema`, `TypeMapping`, `Query` | +| **3** | Runtime | Yes | Yes | `Execution`, `Wallet`, `UiKit`, `Relayer`, `AccessControl` | + +### Tier Import Rules + +Tier isolation is enforced physically through sub-path exports, not tree-shaking: + +- **Tier 1** modules must not import from Tier 2 or Tier 3 modules +- **Tier 2** modules may import from Tier 1 +- **Tier 3** modules may import from Tier 1 and Tier 2 + +This means importing `@openzeppelin/adapter-evm/addressing` will never pull in wallet SDKs, RPC clients, or access control code, regardless of your bundler configuration. + +### Capability Reference + +| Capability | Interface | Tier | Key Methods | +| --- | --- | --- | --- | +| Addressing | `AddressingCapability` | 1 | `isValidAddress` | +| Explorer | `ExplorerCapability` | 1 | `getExplorerUrl`, `getExplorerTxUrl` | +| NetworkCatalog | `NetworkCatalogCapability` | 1 | `getNetworks` | +| UiLabels | `UiLabelsCapability` | 1 | `getUiLabels` | +| ContractLoading | `ContractLoadingCapability` | 2 | `loadContract`, `getContractDefinitionInputs` | +| Schema | `SchemaCapability` | 2 | `isViewFunction`, `getWritableFunctions` | +| TypeMapping | `TypeMappingCapability` | 2 | `mapParameterTypeToFieldType`, `getTypeMappingInfo` | +| Query | `QueryCapability` | 2 | `queryViewFunction`, `formatFunctionResult`, `getCurrentBlock` | +| Execution | `ExecutionCapability` | 3 | `signAndBroadcast`, `formatTransactionData`, `validateExecutionConfig` | +| Wallet | `WalletCapability` | 3 | `connectWallet`, `disconnectWallet`, `getWalletConnectionStatus` | +| UiKit | `UiKitCapability` | 3 | `getAvailableUiKits`, `configureUiKit` | +| Relayer | `RelayerCapability` | 3 | `getRelayers`, `getNetworkServiceForms` | +| AccessControl | `AccessControlCapability` | 3 | `registerContract`, `grantRole`, and 17 more | + +## Profiles + +Profiles are pre-composed bundles of capabilities that match common application archetypes. They exist for convenience. You can always consume individual capabilities directly via the `CapabilityFactoryMap`. + +Each profile is a strict superset of Declarative. Higher profiles add capabilities incrementally: + +### Profile-Capability Matrix + +| Capability | Declarative | Viewer | Transactor | Composer | Operator | +| --- | --- | --- | --- | --- | --- | +| `Addressing` | ✅ | ✅ | ✅ | ✅ | ✅ | +| `Explorer` | ✅ | ✅ | ✅ | ✅ | ✅ | +| `NetworkCatalog` | ✅ | ✅ | ✅ | ✅ | ✅ | +| `UiLabels` | ✅ | ✅ | ✅ | ✅ | ✅ | +| `ContractLoading` | | ✅ | ✅ | ✅ | ✅ | +| `Schema` | | ✅ | ✅ | ✅ | ✅ | +| `TypeMapping` | | ✅ | ✅ | ✅ | ✅ | +| `Query` | | ✅ | | ✅ | ✅ | +| `Execution` | | | ✅ | ✅ | ✅ | +| `Wallet` | | | ✅ | ✅ | ✅ | +| `UiKit` | | | | ✅ | ✅ | +| `Relayer` | | | | ✅ | | +| `AccessControl` | | | | | ✅ | + +### Profile Selection Guide + +| If your application needs to… | Choose | +| --- | --- | +| Validate addresses, list networks, link to explorers | **Declarative** | +| Read contract state without sending transactions | **Viewer** | +| Send transactions without reading contract state first | **Transactor** | +| Build full contract interaction UIs with relayer support | **Composer** | +| Manage contract roles and permissions | **Operator** | + +## Runtime Lifecycle + +Runtimes are **immutable** and **network-scoped**. When a user switches networks, the consuming application must dispose the current runtime and create a new one. + +```mermaid +sequenceDiagram + participant App as Application + participant ES as EcosystemExport + participant RT as EcosystemRuntime + participant Cap as Capabilities + + App->>ES: createRuntime('composer', networkA) + ES->>RT: Compose capabilities with shared state + RT-->>App: runtime (immutable) + + App->>RT: runtime.query.queryViewFunction(...) + RT->>Cap: Lazy-init Query capability + Cap-->>RT: result + + Note over App,RT: User switches to networkB + + App->>RT: runtime.dispose() + RT->>Cap: Staged cleanup (listeners → subscriptions → capabilities → RPC) + App->>ES: createRuntime('composer', networkB) + ES->>RT: New runtime with fresh state + RT-->>App: newRuntime +``` + +### Dispose Contract + +- `dispose()` is **idempotent**: calling it multiple times is a no-op +- After `dispose()`, any method or property access throws `RuntimeDisposedError` +- Pending async operations (e.g., in-flight `signAndBroadcast`) are rejected with `RuntimeDisposedError` +- Cleanup follows a staged order: mark disposed → reject pending operations → clean up listeners and subscriptions → dispose capabilities → release wallet and RPC resources +- Runtime disposal does **not** disconnect the wallet. Disconnect is always an explicit user action + +## Execution Strategies + +The Execution capability uses a **strategy pattern** to support multiple transaction submission methods. Each adapter can provide its own set of strategies. + +```mermaid +flowchart TD + Exec["ExecutionCapability\nsignAndBroadcast()"] --> Config{"executionConfig.method"} + Config -->|"EOA"| EOA["EoaExecutionStrategy\nDirect wallet signing"] + Config -->|"Relayer"| Relay["RelayerExecutionStrategy\nOpenZeppelin Relayer"] + + EOA --> Wallet["Wallet signs tx"] + Relay --> RelayerSvc["Relayer service submits tx"] + + Wallet --> Confirm["waitForTransactionConfirmation()"] + RelayerSvc --> Confirm + + style Exec fill:#e8eaf6,stroke:#3f51b5,color:#000 + style EOA fill:#e8f5e9,stroke:#388e3c,color:#000 + style Relay fill:#fff3e0,stroke:#f57c00,color:#000 +``` + +The EVM and Stellar adapters ship with both EOA and Relayer strategies. Adapter authors can implement custom strategies by conforming to the `AdapterExecutionStrategy` interface. + +## Sub-Path Exports + +Each adapter publishes every implemented capability and profile as a dedicated sub-path export: + +```ts +// Tier 1: no wallet, no RPC, no heavy dependencies +import { createAddressing } from '@openzeppelin/adapter-stellar/addressing'; +import { createExplorer } from '@openzeppelin/adapter-stellar/explorer'; + +// Tier 2: network-aware +import { createQuery } from '@openzeppelin/adapter-stellar/query'; + +// Tier 3: wallet-dependent +import { createExecution } from '@openzeppelin/adapter-stellar/execution'; + +// Profile runtimes +import { createRuntime } from '@openzeppelin/adapter-stellar/profiles/composer'; + +// Metadata and networks +import { networks } from '@openzeppelin/adapter-stellar/networks'; +``` + +This structure ensures that a Declarative-profile consumer never bundles wallet SDKs, and that individual capabilities can be tested in isolation. diff --git a/content/ecosystem-adapters/building-an-adapter.mdx b/content/ecosystem-adapters/building-an-adapter.mdx new file mode 100644 index 00000000..b6687b65 --- /dev/null +++ b/content/ecosystem-adapters/building-an-adapter.mdx @@ -0,0 +1,338 @@ +--- +title: Building an Adapter +--- + +This guide walks through implementing a new ecosystem adapter from scratch. By the end, you'll have a working adapter that integrates with any application using the OpenZeppelin adapter architecture. + +## How Much Do You Need to Implement? + +Adapters are **incrementally adoptable**. You don't need to implement all 13 capabilities to ship a useful adapter. Start small and add capabilities as your ecosystem's support matures. + +```mermaid +flowchart TD + Start["Start Here"] --> T1["Implement Tier 1\n(4 capabilities)"] + T1 -->|"Unlocks"| Dec["Declarative Profile\nAddress validation, explorer links,\nnetwork catalogs"] + + T1 --> T2["Add Tier 2\n(4 capabilities)"] + T2 -->|"Unlocks"| View["Viewer Profile\nContract reading, schema parsing,\ntype mapping, queries"] + + T2 --> T3a["Add Execution + Wallet"] + T3a -->|"Unlocks"| Trans["Transactor Profile\nTransaction signing\nand broadcasting"] + + T3a --> T3b["Add UiKit + Relayer"] + T3b -->|"Unlocks"| Comp["Composer Profile\nFull UI Builder integration\nwith relayer support"] + + T3a --> T3c["Add UiKit + AccessControl"] + T3c -->|"Unlocks"| Op["Operator Profile\nRole and permission\nmanagement"] + + style Start fill:#e8eaf6,stroke:#3f51b5,color:#000 + style T1 fill:#e3f2fd,stroke:#1976d2,color:#000 + style T2 fill:#fff3e0,stroke:#f57c00,color:#000 + style T3a fill:#fce4ec,stroke:#c62828,color:#000 + style T3b fill:#f3e5f5,stroke:#7b1fa2,color:#000 + style T3c fill:#f3e5f5,stroke:#7b1fa2,color:#000 + style Dec fill:#e8f5e9,stroke:#388e3c,color:#000 + style View fill:#e8f5e9,stroke:#388e3c,color:#000 + style Trans fill:#e8f5e9,stroke:#388e3c,color:#000 + style Comp fill:#e8f5e9,stroke:#388e3c,color:#000 + style Op fill:#e8f5e9,stroke:#388e3c,color:#000 +``` + +## Package Structure + +Follow the standard adapter package layout: + +``` +packages/adapter-/ +├── src/ +│ ├── capabilities/ # Capability factory functions +│ │ ├── addressing.ts +│ │ ├── explorer.ts +│ │ ├── network-catalog.ts +│ │ ├── ui-labels.ts +│ │ ├── contract-loading.ts # Tier 2+ +│ │ ├── schema.ts +│ │ ├── type-mapping.ts +│ │ ├── query.ts +│ │ ├── execution.ts # Tier 3+ +│ │ ├── wallet.ts +│ │ ├── ui-kit.ts +│ │ ├── relayer.ts +│ │ ├── access-control.ts +│ │ └── index.ts +│ ├── profiles/ # Profile runtime factories +│ │ ├── shared-state.ts +│ │ └── index.ts +│ ├── networks/ # Network configurations +│ │ └── index.ts +│ ├── index.ts # ecosystemDefinition export +│ ├── metadata.ts +│ └── vite-config.ts +├── package.json +├── tsconfig.json +├── tsdown.config.ts +└── vitest.config.ts +``` + +Not every adapter needs every file. A Tier 1-only adapter may only have `addressing.ts`, `explorer.ts`, `network-catalog.ts`, and `ui-labels.ts` in `capabilities/`. + +## Implementation Guide + +### Step 1: Implement Tier 1 Capabilities + +Start with the four stateless capabilities that every adapter must provide: + +```ts +// capabilities/addressing.ts +import type { AddressingCapability } from '@openzeppelin/ui-types'; +import { isAddress } from 'viem'; + +export function createAddressing(): AddressingCapability { + return { + isValidAddress(address: string): boolean { + // EVM: prefer viem's isAddress (as in @openzeppelin/adapter-evm) over a hand-rolled regex; + // it covers checksums and library-maintained rules. + // Other chains, for example: + // Stellar StrKey (account): /^G[A-Z0-9]{55}$/ (simplified; use StrKey helpers in production) + // Polkadot SS58: @polkadot/util-crypto validateAddress / decodeAddress + return isAddress(address); + }, + }; +} +``` + +```ts +// capabilities/explorer.ts +import type { ExplorerCapability, NetworkConfig } from '@openzeppelin/ui-types'; + +export function createExplorer(config?: NetworkConfig): ExplorerCapability { + const baseUrl = config?.explorerUrl ?? 'https://explorer.example.com'; + return { + getExplorerUrl: (address) => `${baseUrl}/address/${address}`, + getExplorerTxUrl: (txHash) => `${baseUrl}/tx/${txHash}`, + }; +} +``` + +```ts +// capabilities/network-catalog.ts +import type { NetworkCatalogCapability } from '@openzeppelin/ui-types'; +import { networks } from '../networks'; + +export function createNetworkCatalog(): NetworkCatalogCapability { + return { getNetworks: () => networks }; +} +``` + +```ts +// capabilities/ui-labels.ts +import type { UiLabelsCapability } from '@openzeppelin/ui-types'; + +export function createUiLabels(): UiLabelsCapability { + return { + getUiLabels: () => ({ transactionLabel: 'Transaction' }), + }; +} +``` + +### Step 2: Define the CapabilityFactoryMap + +Wire all capability factories into the capability map: + +```ts +// capabilities/index.ts +import type { CapabilityFactoryMap } from '@openzeppelin/ui-types'; +import { createAddressing } from './addressing'; +import { createExplorer } from './explorer'; +import { createNetworkCatalog } from './network-catalog'; +import { createUiLabels } from './ui-labels'; + +export const capabilities: CapabilityFactoryMap = { + addressing: createAddressing, + explorer: createExplorer, + networkCatalog: createNetworkCatalog, + uiLabels: createUiLabels, +}; +``` + +### Step 3: Wire Profile Runtimes + +Use `adapter-runtime-utils` to compose capabilities into profile runtimes: + +```ts +// profiles/index.ts +import type { NetworkConfig, ProfileName } from '@openzeppelin/ui-types'; +import { createRuntimeFromFactories } from '@openzeppelin/adapter-runtime-utils'; +import { capabilities } from '../capabilities'; + +export function createRuntime( + profile: ProfileName, + config: NetworkConfig, + factoryMap = capabilities, + options?: { uiKit?: string } +) { + return createRuntimeFromFactories(profile, config, factoryMap, options); +} +``` + +`createRuntimeFromFactories` handles: +- Validating that all required capabilities for the profile are present +- Throwing `UnsupportedProfileError` with a list of missing capabilities if validation fails +- Lazy capability instantiation with caching +- Staged disposal + +### Step 4: Export the Ecosystem Definition + +The `ecosystemDefinition` is the public entry point that consuming applications use: + +```ts +// index.ts +import type { EcosystemExport } from '@openzeppelin/ui-types'; +import { capabilities } from './capabilities'; +import { metadata } from './metadata'; +import { networks } from './networks'; +import { createRuntime } from './profiles'; + +export const ecosystemDefinition: EcosystemExport = { + ...metadata, + networks, + capabilities, + createRuntime: (profile, config, options) => + createRuntime(profile, config, capabilities, options), +}; +``` + +### Step 5: Configure Sub-Path Exports + +Add sub-path exports to `package.json` for physical tier isolation: + +```json +{ + "exports": { + ".": { "import": "./dist/index.mjs" }, + "./addressing": { "import": "./dist/capabilities/addressing.mjs" }, + "./explorer": { "import": "./dist/capabilities/explorer.mjs" }, + "./network-catalog": { "import": "./dist/capabilities/network-catalog.mjs" }, + "./ui-labels": { "import": "./dist/capabilities/ui-labels.mjs" }, + "./metadata": { "import": "./dist/metadata.mjs" }, + "./networks": { "import": "./dist/networks.mjs" }, + "./vite-config": { "import": "./dist/vite-config.mjs" } + } +} +``` + +Add more entries as you implement higher-tier capabilities. + +### Step 6: Add a Vite Config Export + +Every adapter must publish a `./vite-config` entry that returns build-time requirements: + +```ts +// vite-config.ts +import type { UserConfig } from 'vite'; + +export function getViteConfig(): UserConfig { + return { + resolve: { + dedupe: ['@your-chain/sdk'], + }, + optimizeDeps: { + include: ['@your-chain/sdk'], + }, + }; +} +``` + +## Adding Higher-Tier Capabilities + +Once Tier 1 is working, add capabilities incrementally. Each Tier 2+ factory function must: + +1. Accept a `NetworkConfig` parameter +2. Return a capability object that includes a `dispose()` method +3. Follow the tier import rules (Tier 2 may import Tier 1, Tier 3 may import Tier 1 and 2) + +Here's an example Query capability: + +```ts +// capabilities/query.ts +import type { QueryCapability, NetworkConfig } from '@openzeppelin/ui-types'; + +export function createQuery(config: NetworkConfig): QueryCapability { + let client: YourRpcClient | null = null; + + function getClient() { + if (!client) client = new YourRpcClient(config.rpcUrl); + return client; + } + + return { + networkConfig: config, + + async queryViewFunction(address, functionId, args) { + return getClient().call(address, functionId, args); + }, + + formatFunctionResult(result, schema, functionId) { + return { raw: result, formatted: String(result) }; + }, + + async getCurrentBlock() { + return getClient().getBlockNumber(); + }, + + dispose() { + client?.close(); + client = null; + }, + }; +} +``` + +## Shared EVM Core + +If your chain is EVM-compatible, you don't need to reimplement everything. Depend on `@openzeppelin/adapter-evm-core` and reuse its capability implementations: + +```ts +import { createExecution } from '@openzeppelin/adapter-evm-core/execution'; +import { createQuery } from '@openzeppelin/adapter-evm-core/query'; +``` + +This is exactly what `adapter-polkadot` does. It delegates ABI loading, queries, transaction execution, and wallet infrastructure to the shared EVM core while adding Polkadot-specific metadata and network configurations. + +## Lazy Capability Factories + +For capabilities that depend on each other (e.g., Query needs ContractLoading for schema resolution), use `createLazyRuntimeCapabilityFactories` from `adapter-runtime-utils`: + +```ts +import { + createLazyRuntimeCapabilityFactories, +} from '@openzeppelin/adapter-runtime-utils'; + +const lazyFactories = createLazyRuntimeCapabilityFactories(config, { + contractLoading: () => createContractLoading(config), + query: (deps) => createQuery(config, { + getContractLoading: () => deps.contractLoading, + }), +}); +``` + +This avoids circular dependency issues while maintaining shared state within a profile runtime. + +## Contribution Checklist + +Before submitting your adapter: + +1. All Tier 1 capabilities are implemented and exported via sub-path exports +2. `ecosystemDefinition` conforms to `EcosystemExport` with `capabilities` and `createRuntime` +3. `metadata`, `networks`, and `vite-config` exports are present +4. Profile runtimes use `adapter-runtime-utils` for composition +5. Unsupported profiles throw `UnsupportedProfileError` with missing capabilities listed +6. Tier isolation is verified: Tier 1 sub-path imports don't pull Tier 2/3 dependencies +7. Tests cover each capability factory and profile runtime creation +8. `package.json` includes sub-path exports for every implemented capability and profile +9. `tsdown.config.ts` includes entry points for all sub-path exports +10. Build-time requirements are validated with `pnpm validate:vite-configs` + + +The `pnpm lint:adapters` CI check validates tier isolation and export structure automatically. Run it locally before submitting your PR. + diff --git a/content/ecosystem-adapters/getting-started.mdx b/content/ecosystem-adapters/getting-started.mdx new file mode 100644 index 00000000..0765d58e --- /dev/null +++ b/content/ecosystem-adapters/getting-started.mdx @@ -0,0 +1,396 @@ +--- +title: Getting Started +--- + +This guide walks you through installing an adapter, creating a runtime, and performing your first on-chain interaction. + +## Prerequisites + +- Node.js >= 20.19.0 +- A package manager: pnpm (recommended), npm, or yarn + +## Installation + +Install the adapter for your target ecosystem along with the shared types package: + +**EVM (Ethereum, Polygon, etc.)** +```bash +pnpm add @openzeppelin/adapter-evm @openzeppelin/ui-types +``` + +**Stellar (Soroban)** +```bash +pnpm add @openzeppelin/adapter-stellar @openzeppelin/ui-types +``` + +**Polkadot** +```bash +pnpm add @openzeppelin/adapter-polkadot @openzeppelin/ui-types +``` + +If you're consuming multiple ecosystems, install all of them. Each adapter is independent and tree-shakeable. + +## Quick Start + +### 1. Choose a Profile + +Pick the [profile](/ecosystem-adapters/architecture#profiles) that matches your application's needs: + +| Profile | What it gives you | +| --- | --- | +| `declarative` | Address validation, explorer links, network lists | +| `viewer` | + Contract reading, schema parsing, type mapping | +| `transactor` | + Transaction signing and broadcasting | +| `composer` | + Full UI integration with wallet and relayer | +| `operator` | + Role and access control management | + +### 2. Create a Runtime + +Every adapter exports an `ecosystemDefinition` object. Use its `createRuntime` method to compose a profile runtime: + +```ts +import { ecosystemDefinition } from '@openzeppelin/adapter-evm'; + +const network = ecosystemDefinition.networks[0]; // e.g., Ethereum Mainnet + +const runtime = ecosystemDefinition.createRuntime('viewer', network); +``` + +The `createRuntime` call is **synchronous**. It assembles capability instances immediately. Expensive initialization (RPC connections, wallet discovery) happens lazily on first use. + +If you request a profile the adapter doesn't fully support, `createRuntime` throws an `UnsupportedProfileError` listing the missing capabilities. + +### 3. Use Capabilities + +Access capabilities directly from the runtime object: + +```ts +// Validate an address (Tier 1: instant, no network call) +const isValid = runtime.addressing.isValidAddress('0x1234...'); + +// Read contract state (Tier 2: triggers RPC on first call) +const result = await runtime.query.queryViewFunction( + '0xContractAddress', + 'balanceOf', + ['0xOwnerAddress'] +); +const formatted = runtime.query.formatFunctionResult(result, schema, 'balanceOf'); +``` + +### 4. Dispose When Done + +Runtimes hold network connections and event listeners. Dispose them when switching networks or unmounting: + +```ts +runtime.dispose(); +// Any further access throws RuntimeDisposedError +``` + + +**Live example**: For an end-to-end reference that combines adapters with OpenZeppelin UI in the browser, open the hosted demo at [**openzeppelin-ui.netlify.app**](https://openzeppelin-ui.netlify.app). + + +## Consuming Individual Capabilities + +You don't have to use profiles. Import individual capability factories via sub-path exports for maximum control: + +```ts +import { createAddressing } from '@openzeppelin/adapter-evm/addressing'; +import { createExplorer } from '@openzeppelin/adapter-evm/explorer'; + +const addressing = createAddressing(); +const explorer = createExplorer(networkConfig); + +console.log(addressing.isValidAddress('0xABC...')); // true or false +console.log(explorer.getExplorerUrl('0xABC...')); // https://etherscan.io/address/0xABC... +``` + + +Standalone capability instances created from factory functions are fully isolated. They do not share state with profile runtimes or with each other. + + +## Vite / Vitest Integration + +Applications that consume multiple adapter packages should use `@openzeppelin/adapters-vite` to merge adapter-owned build configuration: + +```bash +pnpm add -D @openzeppelin/adapters-vite +``` + +### Vite Config + +```ts +import { defineOpenZeppelinAdapterViteConfig } from '@openzeppelin/adapters-vite'; +import react from '@vitejs/plugin-react'; + +export default defineOpenZeppelinAdapterViteConfig({ + ecosystems: ['evm', 'stellar', 'polkadot'], + config: { + plugins: [react()], + }, +}); +``` + +### Vitest Config + +```ts +import { defineOpenZeppelinAdapterVitestConfig } from '@openzeppelin/adapters-vite'; +import { mergeConfig } from 'vitest/config'; + +export default mergeConfig( + sharedVitestConfig, + await defineOpenZeppelinAdapterVitestConfig({ + ecosystems: ['evm', 'stellar'], + importMetaUrl: import.meta.url, + config: { + test: { environment: 'jsdom' }, + }, + }) +); +``` + +This centralizes `resolve.dedupe`, `optimizeDeps`, `ssr.noExternal`, and any adapter-specific Vite plugins. Adapters remain the source of truth for their own build requirements. + +## Network Switching + +Runtimes are network-scoped and immutable. To switch networks, dispose and recreate: + +```ts +let runtime = ecosystemDefinition.createRuntime('composer', ethereumMainnet); + +// User switches to Sepolia +runtime.dispose(); +runtime = ecosystemDefinition.createRuntime('composer', sepoliaTestnet); +``` + + +Wallet sessions survive network changes. Runtime disposal cleans up runtime-owned resources (listeners, subscriptions, RPC connections) but does **not** disconnect the wallet. Disconnect is always an explicit user action. + + +## Error Handling + +The adapter system uses two primary error classes: + +| Error | When it's thrown | +| --- | --- | +| `UnsupportedProfileError` | `createRuntime` is called with a profile the adapter can't fully support. The error message lists the missing capabilities. | +| `RuntimeDisposedError` | Any method or property is accessed on a disposed runtime, or an in-flight async operation is interrupted by disposal. | + +```ts +import { UnsupportedProfileError, RuntimeDisposedError } from '@openzeppelin/ui-types'; + +try { + const runtime = ecosystemDefinition.createRuntime('operator', network); +} catch (error) { + if (error instanceof UnsupportedProfileError) { + console.log('Missing capabilities:', error.message); + } +} +``` + +## Using with React + +Adapters integrate with React through three layers: a **wallet context provider** that manages wallet SDK state, **`configureUiKit`** that selects and configures the wallet UI kit, and a **hook facade** that exposes a uniform set of React hooks across every ecosystem. + +### Wallet Context Provider + +Each adapter exports a root React component via `getEcosystemReactUiContextProvider()`. This component wraps your application (or the relevant subtree) and bootstraps the wallet SDK context. For example, EVM adapters render `WagmiProvider` and `QueryClientProvider` internally, while the Stellar adapter renders its own `StellarWalletContext.Provider`. + +You don't render these providers yourself. Instead, pass the runtime to a shared `WalletStateProvider` from `@openzeppelin/ui-react`, which delegates to the correct ecosystem provider automatically: + +```tsx +import { WalletStateProvider } from '@openzeppelin/ui-react'; + +function App() { + return ( + + + + ); +} +``` + +The `loadConfigModule` callback lets the adapter lazily load user-authored wallet configuration files (e.g. `src/config/wallet/rainbowkit.config.ts` for a RainbowKit setup). If you don't use a native config file, pass a function that returns `null`. + +### Configuring the UI Kit + +Before rendering wallet UI components, configure the UI kit on the runtime's `uiKit` capability. This selects which wallet UI library to use (RainbowKit, a custom theme, or none) and passes kit-specific options: + +```ts +await runtime.uiKit?.configureUiKit( + { + kitName: 'rainbowkit', // or 'custom', 'none' + kitConfig: { + // kit-specific options, e.g., showInjectedConnector: true + }, + }, + { + loadUiKitNativeConfig: loadAppConfigModule, + } +); +``` + +After configuration, retrieve the wallet UI components from the runtime: + +```ts +const walletComponents = runtime.uiKit?.getEcosystemWalletComponents?.(); +const ConnectButton = walletComponents?.ConnectButton; +const AccountDisplay = walletComponents?.AccountDisplay; +const NetworkSwitcher = walletComponents?.NetworkSwitcher; +``` + +### Connecting and Disconnecting Wallets + +The `WalletCapability` on the runtime exposes imperative methods for wallet lifecycle management: + +```ts +// List available wallet connectors +const connectors = runtime.wallet.getAvailableConnectors(); + +// Connect using a specific connector +await runtime.wallet.connectWallet(connectors[0].id); + +// Check connection status +const status = runtime.wallet.getWalletConnectionStatus(); + +// Listen for connection changes +const unsubscribe = runtime.wallet.onWalletConnectionChange?.((newStatus) => { + console.log('Wallet status changed:', newStatus); +}); + +// Disconnect +await runtime.wallet.disconnectWallet(); +``` + + +`connectWallet` and `disconnectWallet` are imperative calls on the runtime. They are independent of the React rendering layer. You can call them from event handlers, effects, or outside React entirely. + + +### Hook Facade Pattern + +Every adapter exports a **facade hooks** object that conforms to the `EcosystemSpecificReactHooks` interface. This gives consuming applications a uniform set of React hooks regardless of the underlying wallet library. + +The EVM adapter maps its facade to [wagmi](https://wagmi.sh/) hooks, the Stellar adapter provides custom implementations, and other adapters follow the same pattern: + +```ts +// EVM facade (wraps wagmi) +import { evmFacadeHooks } from '@openzeppelin/adapter-evm'; + +// Stellar facade (custom hooks) +import { stellarFacadeHooks } from '@openzeppelin/adapter-stellar'; +``` + +Each facade object includes these hooks: + +| Hook | Purpose | +| --- | --- | +| `useAccount` | Returns the connected account address, connection status, and chain info | +| `useConnect` | Provides a `connect` function and available connectors | +| `useDisconnect` | Provides a `disconnect` function | +| `useSwitchChain` | Returns a `switchChain` function (EVM-only; stubs on other ecosystems) | +| `useChainId` | Returns the currently active chain ID | +| `useChains` | Returns the list of configured chains | +| `useBalance` | Returns the native token balance for the connected account | + +In practice, you access facade hooks through the runtime rather than importing them directly. The runtime's `uiKit.getEcosystemReactHooks()` method returns the correct facade for the active ecosystem: + +```tsx +import { useWalletState } from '@openzeppelin/ui-react'; + +function AccountInfo() { + const { walletFacadeHooks } = useWalletState(); + + const { address, isConnected } = walletFacadeHooks.useAccount(); + const { switchChain } = walletFacadeHooks.useSwitchChain(); + + if (!isConnected) return

Not connected

; + + return ( +
+

Connected: {address}

+ +
+ ); +} +``` + + +Hooks that don't apply to a given ecosystem return safe stubs. For example, the Stellar facade's `useSwitchChain` returns `{ switchChain: undefined }`. Your components can feature-detect this without conditional imports. + + +## Multi-Ecosystem Apps + +Applications that interact with multiple blockchains create one runtime per ecosystem. Each runtime is independent. It manages its own wallet session, RPC connections, and lifecycle. + +### Setup + +Install the adapters you need and the shared Vite plugin: + +```bash +pnpm add @openzeppelin/adapter-evm @openzeppelin/adapter-stellar @openzeppelin/ui-types +pnpm add -D @openzeppelin/adapters-vite +``` + +Merge adapter build requirements with `defineOpenZeppelinAdapterViteConfig`: + +```ts +// vite.config.ts +import { defineOpenZeppelinAdapterViteConfig } from '@openzeppelin/adapters-vite'; +import react from '@vitejs/plugin-react'; + +export default defineOpenZeppelinAdapterViteConfig({ + ecosystems: ['evm', 'stellar'], + config: { + plugins: [react()], + }, +}); +``` + +### Creating Runtimes + +Instantiate a runtime from each adapter's `ecosystemDefinition`: + +```ts +import { ecosystemDefinition as evmDefinition } from '@openzeppelin/adapter-evm'; +import { ecosystemDefinition as stellarDefinition } from '@openzeppelin/adapter-stellar'; + +// EVM runtime (e.g. Ethereum Mainnet) +const evmNetwork = evmDefinition.networks[0]; +const evmRuntime = evmDefinition.createRuntime('composer', evmNetwork); + +// Stellar runtime (e.g. Stellar Mainnet) +const stellarNetwork = stellarDefinition.networks[0]; +const stellarRuntime = stellarDefinition.createRuntime('viewer', stellarNetwork); +``` + +Each runtime is fully isolated. The EVM runtime connects to Ethereum via wagmi, the Stellar runtime connects to Horizon/Soroban. They share no state and can be disposed independently: + +```ts +// Use both runtimes side by side +const evmValid = evmRuntime.addressing.isValidAddress('0x1234...'); +const stellarValid = stellarRuntime.addressing.isValidAddress('G...'); + +const evmBalance = await evmRuntime.query.queryViewFunction( + '0xToken', 'balanceOf', ['0xOwner'] +); + +// Dispose one without affecting the other +evmRuntime.dispose(); +// stellarRuntime continues working +``` + + +Wallet sessions are ecosystem-scoped, not runtime-scoped. Disposing an EVM runtime to switch from Mainnet to Sepolia does not disconnect the user's MetaMask. The wallet session persists across runtime recreation within the same ecosystem. + + +## Next Steps + +- Read the [Architecture](/ecosystem-adapters/architecture) page for a deep dive into tiers, profiles, and lifecycle management +- Explore [Supported Ecosystems](/ecosystem-adapters/supported-ecosystems) to see what each adapter provides +- Follow the [Building an Adapter](/ecosystem-adapters/building-an-adapter) guide to add support for a new chain +- Browse the [adapter source code on GitHub](https://github.com/OpenZeppelin/openzeppelin-adapters) for implementation details diff --git a/content/ecosystem-adapters/index.mdx b/content/ecosystem-adapters/index.mdx new file mode 100644 index 00000000..14eba64b --- /dev/null +++ b/content/ecosystem-adapters/index.mdx @@ -0,0 +1,72 @@ +--- +title: Ecosystem Adapters +--- + +**OpenZeppelin Ecosystem Adapters** are a set of modular, chain-specific integration packages that let applications interact with any supported blockchain through a single, unified interface. Built on 13 composable capability interfaces organized in 3 tiers, each adapter encapsulates contract loading, type mapping, transaction execution, wallet connection, and network configuration in one place, while keeping consuming applications completely chain-agnostic. + + +**Source code**: The adapters are open-source. Browse the implementation, open issues, and contribute at [**github.com/OpenZeppelin/openzeppelin-adapters**](https://github.com/OpenZeppelin/openzeppelin-adapters). + + + + + Understand the capability-based architecture: tiers, profiles, and the runtime lifecycle. + + + Install adapters, configure profiles, and run your first cross-chain interaction. + + + Explore the production-ready EVM, Stellar, Polkadot, and Midnight adapters. + + + Step-by-step guide to implementing a new adapter for your blockchain. + + + +## Why Adapters? + +Building cross-chain tooling traditionally forces developers into one of two traps: a monolithic abstraction that leaks chain details, or per-chain forks that drift apart over time. Adapters solve this with a **capability-based decomposition**. Each chain implements only the interfaces it supports, and consuming applications pull in only what they need. + +```mermaid +flowchart LR + App["Your Application"] --> Runtime["EcosystemRuntime"] + Runtime --> T1["Tier 1 (Lightweight)\n4 capabilities\nAddressing, Explorer, ..."] + Runtime --> T2["Tier 2 (Schema)\n4 capabilities\nContractLoading, Query, ..."] + Runtime --> T3["Tier 3 (Runtime)\n5 capabilities\nExecution, Wallet, ..."] + + style T1 fill:#e3f2fd,stroke:#1976d2,color:#000 + style T2 fill:#fff3e0,stroke:#f57c00,color:#000 + style T3 fill:#fce4ec,stroke:#c62828,color:#000 +``` + +## Key Design Principles + +- **Pay for what you use.** Tier 1 capabilities are stateless and never pull in wallet SDKs or RPC clients. Import `addressing` and you get only address validation; nothing else is bundled. +- **Profiles simplify consumption.** Five pre-composed profiles (Declarative, Viewer, Transactor, Composer, Operator) match common application archetypes so you don't have to assemble capabilities manually. +- **Adapters own their chains.** All chain-specific logic (ABI parsing, Soroban type mapping, ZK proof orchestration) stays inside the adapter package. The consuming application never touches it. +- **Runtime lifecycle is explicit.** Runtimes are immutable and network-scoped. Switching networks means disposing the old runtime and creating a new one. There are no hidden state mutations. + +## Packages + +| Package | Description | Status | +| --- | --- | --- | +| `@openzeppelin/adapter-evm` | Ethereum, Polygon, and EVM-compatible chains | Production | +| `@openzeppelin/adapter-stellar` | Stellar / Soroban | Production | +| `@openzeppelin/adapter-polkadot` | Polkadot Hub, Moonbeam (EVM path) | Production | +| `@openzeppelin/adapter-midnight` | Midnight Network (ZK artifacts, Lace wallet) | Production | +| `@openzeppelin/adapter-solana` | Solana (scaffolding) | In Progress | +| `@openzeppelin/adapter-evm-core` | Shared EVM capability implementations (internal) | Internal | +| `@openzeppelin/adapter-runtime-utils` | Profile composition and lifecycle utilities (internal) | Internal | +| `@openzeppelin/adapters-vite` | Shared Vite/Vitest build integration | Utility | + +## Who Uses Adapters? + +Adapters are consumed by several OpenZeppelin products: + +- [**UI Builder**](https://github.com/OpenZeppelin/ui-builder): full-featured smart contract interaction UI +- [**OpenZeppelin UI**](https://github.com/OpenZeppelin/openzeppelin-ui): shared UI components and React integration +- [**UIKit example app (live)**](https://openzeppelin-ui.netlify.app): hosted demo of OpenZeppelin UI with ecosystem adapters +- [**Role Manager**](https://github.com/OpenZeppelin/role-manager): role and permission management tool +- [**RWA Wizard**](https://github.com/OpenZeppelin/rwa-wizard): real-world asset token generation + +Any TypeScript application that needs chain-agnostic blockchain interaction can use adapters directly. diff --git a/content/ecosystem-adapters/supported-ecosystems.mdx b/content/ecosystem-adapters/supported-ecosystems.mdx new file mode 100644 index 00000000..6da1d002 --- /dev/null +++ b/content/ecosystem-adapters/supported-ecosystems.mdx @@ -0,0 +1,149 @@ +--- +title: Supported Ecosystems +--- + +Each adapter implements the subset of the [13 capability interfaces](/ecosystem-adapters/architecture#capability-tiers) that its blockchain supports. This page summarizes what each production adapter provides. + +| Adapter | Networks | Capabilities | Status | +| --- | --- | --- | --- | +| **EVM** | Ethereum, Polygon, Arbitrum, Base, Optimism, ... | 13/13 | Production | +| **Stellar** | Stellar Public, Stellar Testnet | 13/13 | Production | +| **Polkadot** | Polkadot Hub, Moonbeam, Moonriver | 13/13 (EVM path) | Production | +| **Midnight** | Midnight Testnet | 11/13 | Production | +| **Solana** | Devnet, Testnet, Mainnet Beta | 4/13 (Tier 1 only) | Scaffolding | + +## EVM (`@openzeppelin/adapter-evm`) + +The EVM adapter targets Ethereum and all EVM-compatible chains. It implements the full set of 13 capabilities and supports all 5 profiles. + +**Supported Networks**: Ethereum Mainnet, Sepolia, Polygon, Polygon Amoy, Arbitrum One, Arbitrum Sepolia, Base, Base Sepolia, Optimism, Optimism Sepolia, and more. + +**Profiles Supported**: Declarative, Viewer, Transactor, Composer, Operator + +### Highlights + +- **Contract Loading**: Fetches ABIs from Etherscan and Sourcify with automatic fallback ordering. Detects proxy contracts and resolves implementation ABIs. +- **Type Mapping**: Maps all Solidity types (`uint256`, `address`, `bytes32`, tuples, dynamic arrays) to UI-friendly form fields. +- **Execution Strategies**: Pluggable EOA (direct wallet signing via Wagmi/Viem) and OpenZeppelin Relayer strategies. +- **Wallet Integration**: Built on Wagmi and RainbowKit with React context providers and hooks. +- **Access Control**: Full role management including `grantRole`, `revokeRole`, `renounceRole`, ownership transfers, and role enumeration. + +### Configuration Resolution + +The EVM adapter resolves RPC URLs and explorer API keys through a layered priority system: + +1. **User-provided config**: settings from the end-user (stored in localStorage) +2. **Application config**: deployer settings via `app.config.json` or environment variables +3. **Default config**: values from the `NetworkConfig` the adapter was instantiated with + +### Internal Architecture + +`@openzeppelin/adapter-evm` is a thin public layer that re-exports capabilities from `@openzeppelin/adapter-evm-core`. The core package centralizes all reusable EVM implementations: + +| Core Module | Responsibility | +| --- | --- | +| ABI loading | Etherscan / Sourcify fetching, proxy detection | +| Schema transformation | ABI → `ContractSchema` conversion | +| Input/output conversion | Solidity type ↔ form field mapping | +| Query helpers | Public client creation, view function calls | +| Transaction execution | Calldata formatting, strategy dispatch | +| Wallet infrastructure | Wagmi config, RainbowKit setup, session management | +| Relayer | Relayer SDK integration, network service forms | +| Access Control | Role queries, grant/revoke operations | + +## Stellar (`@openzeppelin/adapter-stellar`) + +The Stellar adapter provides a complete Soroban implementation with all 13 capabilities. + +**Supported Networks**: Stellar Public, Stellar Testnet + +**Profiles Supported**: Declarative, Viewer, Transactor, Composer, Operator + +### Highlights + +- **Contract Loading**: Loads Soroban contract specifications and transforms them into the shared `ContractSchema`. +- **Type Mapping**: Maps Soroban types (`Address`, `i128`, `Bytes`, `Vec`, `Map`) to form fields with input validation. +- **Execution**: Parses form values into Soroban `ScVal` arguments. Supports both EOA and Relayer execution strategies. +- **Wallet Integration**: Uses Stellar Wallets Kit with React UI providers and hooks. +- **Access Control**: First-class support for Ownable and AccessControl patterns, including role queries, ownership actions, and optional indexer-backed historical lookups. +- **SAC Support**: Detects Stellar Asset Contracts and dynamically loads their specifications. + +## Polkadot (`@openzeppelin/adapter-polkadot`) + +The Polkadot adapter targets EVM-compatible parachains and relay chains, reusing the shared EVM core for most capabilities. + +**Supported Networks**: Polkadot Hub, Kusama Hub, Moonbeam, Moonriver + +**Profiles Supported**: Declarative, Viewer, Transactor, Composer (EVM execution path) + +### Highlights + +- **EVM Core Reuse**: Delegates ABI loading, queries, transaction execution, and wallet infrastructure to `adapter-evm-core`. +- **Polkadot Metadata**: Exposes relay chain associations and network category distinctions. +- **React Wallet Provider**: Ships wallet provider utilities for consumer applications. +- **Future Substrate Path**: Structured to support native Substrate/Wasm modules alongside the current EVM path. + + +Non-EVM execution paths (native Substrate) are not yet implemented. Requesting a profile that requires execution on a non-EVM network will throw `UnsupportedProfileError`. + + +## Midnight (`@openzeppelin/adapter-midnight`) + +The Midnight adapter enables browser-based interaction with Midnight contracts using zero-knowledge proof workflows. + +**Supported Networks**: Midnight Testnet + +### Highlights + +- **Artifact Ingestion**: Supports ZIP-based artifact bundles for contract evaluation and ZK proof orchestration entirely in the browser. +- **Lace Wallet**: Integrates with the Lace wallet for signing and execution. +- **Runtime Secrets**: Handles organizer-only secrets as in-memory execution-time inputs, not persisted configuration. +- **Lazy Polyfills**: Midnight-specific browser polyfills are lazy-loaded so they don't affect other ecosystem bundles. +- **Export Bootstrap**: Supports exporting self-contained applications that bundle Midnight contract artifacts for standalone use. + +### Build Requirements + +Midnight requires host-provided plugin factories for WASM and top-level await: + +```ts +import { loadOpenZeppelinAdapterViteConfig } from '@openzeppelin/adapters-vite'; + +const adapterConfigs = await loadOpenZeppelinAdapterViteConfig({ + ecosystems: ['midnight'], + pluginFactories: { + midnight: { wasm, topLevelAwait }, + }, +}); +``` + +## Solana (`@openzeppelin/adapter-solana`) + + +The Solana adapter is scaffolding only and is not yet production-ready. + + +The Solana package defines the package boundaries and network configurations for a future adapter. It currently provides: + +- Solana network configurations (Devnet, Testnet, Mainnet Beta) +- Package structure and sub-path export scaffolding +- Tier 1 capability implementations (Addressing, Explorer, NetworkCatalog, UiLabels) + +The Operator profile is explicitly unsupported. Calling `createRuntime('operator', ...)` throws `UnsupportedProfileError` because `accessControl` factories are not yet implemented. + +## Capability Support Matrix + +| Capability | EVM | Stellar | Polkadot | Midnight | Solana | +| --- | --- | --- | --- | --- | --- | +| Addressing | ✅ | ✅ | ✅ | ✅ | ✅ | +| Explorer | ✅ | ✅ | ✅ | ✅ | ✅ | +| NetworkCatalog | ✅ | ✅ | ✅ | ✅ | ✅ | +| UiLabels | ✅ | ✅ | ✅ | ✅ | ✅ | +| ContractLoading | ✅ | ✅ | ✅ | ✅ | - | +| Schema | ✅ | ✅ | ✅ | ✅ | - | +| TypeMapping | ✅ | ✅ | ✅ | ✅ | - | +| Query | ✅ | ✅ | ✅ | ✅ | - | +| Execution | ✅ | ✅ | ✅ (EVM) | ✅ | - | +| Wallet | ✅ | ✅ | ✅ | ✅ | - | +| UiKit | ✅ | ✅ | ✅ | ✅ | - | +| Relayer | ✅ | ✅ | ✅ | - | - | +| AccessControl | ✅ | ✅ | ✅ | - | - | diff --git a/content/tools/uikit/architecture.mdx b/content/tools/uikit/architecture.mdx new file mode 100644 index 00000000..0177eaad --- /dev/null +++ b/content/tools/uikit/architecture.mdx @@ -0,0 +1,216 @@ +--- +title: Architecture +--- + +OpenZeppelin UIKit is built as a layered stack of independently installable packages. This page explains how those layers fit together, how the capability-driven adapter model works, and how runtimes manage lifecycle across multiple ecosystems. + +## Package Layers + +The packages form a dependency chain where each layer builds on the ones below it. Lower layers are lighter and more generic; higher layers add React-specific and domain-specific behavior. + +```mermaid +%%{init: {'flowchart': {'nodeSpacing': 20, 'rankSpacing': 40}} }%% +flowchart TD + Storage(["7 · ui-storage"]) + Renderer(["6 · ui-renderer"]) + ReactPkg(["5 · ui-react"]) + Components(["4 · ui-components"]) + Styles(["3 · ui-styles"]) + Utils(["2 · ui-utils"]) + Types(["1 · ui-types"]) + + Storage --> Utils + Renderer --> Components + ReactPkg --> Components + Components --> Styles + Components --> Utils + Utils --> Types + + style Storage fill:#e8eaf6,stroke:#5c6bc0,color:#1a237e + style Renderer fill:#e8eaf6,stroke:#5c6bc0,color:#1a237e + style ReactPkg fill:#e8eaf6,stroke:#5c6bc0,color:#1a237e + style Components fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style Styles fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style Utils fill:#fff3e0,stroke:#ff9800,color:#e65100 + style Types fill:#fff3e0,stroke:#ff9800,color:#e65100 +``` + +**Color key:** Application layers (5–7) · UI & design (3–4) · Foundation (1–2) + +| Layer | Package | Responsibility | +| --- | --- | --- | +| 1 | `@openzeppelin/ui-types` | TypeScript interfaces for capabilities, schemas, form models, networks, transactions, and execution config. No runtime code: pure type definitions. | +| 2 | `@openzeppelin/ui-utils` | Framework-agnostic helpers: `AppConfigService` for environment/config loading, structured logger, validation utilities, and routing helpers. | +| 3 | `@openzeppelin/ui-styles` | Tailwind CSS 4 theme tokens using OKLCH color space. Ships CSS variables and custom variants (dark mode). No JavaScript. | +| 4 | `@openzeppelin/ui-components` | React UI primitives (buttons, dialogs, cards, tabs) and blockchain-aware form fields (address, amount, bytes, enum, map). Built on Radix UI + shadcn/ui patterns. | +| 5 | `@openzeppelin/ui-react` | `RuntimeProvider` for managing `EcosystemRuntime` instances per network. `WalletStateProvider` for global wallet state. Derived hooks for cross-ecosystem wallet abstraction. | +| 6 | `@openzeppelin/ui-renderer` | `TransactionForm` for schema-driven transaction forms. `ContractStateWidget` for view function queries. `ExecutionConfigDisplay`, `AddressBookWidget`, address book components. | +| 7 | `@openzeppelin/ui-storage` | `EntityStorage` and `KeyValueStorage` base classes on Dexie.js/IndexedDB. Account alias plugin for address-to-name mapping. | + + +## Capabilities + +The UIKit type system defines 13 **capabilities**: small, focused interfaces that describe what an adapter can do. + +Capabilities are organized into three tiers based on their requirements: + +```mermaid +%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 30}} }%% +flowchart TD + T1["Tier 1 (Lightweight)
Addressing · Explorer · NetworkCatalog · UiLabels"] + T2["Tier 2 (Network-Aware)
ContractLoading · Schema · TypeMapping · Query"] + T3["Tier 3 (Stateful)
Execution · Wallet · UiKit · Relayer · AccessControl"] + + T1 --"may import"--> T2 --"may import"--> T3 + + style T1 fill:#e8eaf6,stroke:#5c6bc0,color:#000 + style T2 fill:#e0f2f1,stroke:#26a69a,color:#000 + style T3 fill:#fff3e0,stroke:#ff9800,color:#000 +``` + +**Tier 1** requires no runtime context: safe to import anywhere. **Tier 2** needs a `networkConfig`. **Tier 3** additionally needs wallet state and participates in the `dispose()` lifecycle. Each higher tier may import from lower tiers, but never the reverse. + +| Capability | Tier | Purpose | +| --- | --- | --- | +| `Addressing` | 1 | Address validation, formatting, checksumming | +| `Explorer` | 1 | Block explorer URL generation | +| `NetworkCatalog` | 1 | Available network listing and metadata | +| `UiLabels` | 1 | Human-readable labels for ecosystem-specific terms | +| `ContractLoading` | 2 | Fetch and parse contract ABIs/IDLs | +| `Schema` | 2 | Transform contract definitions into form-renderable schemas | +| `TypeMapping` | 2 | Map blockchain types (e.g. `uint256`) to form field types | +| `Query` | 2 | Execute read-only contract calls (view functions) | +| `Execution` | 3 | Sign, broadcast, and track transactions | +| `Wallet` | 3 | Connect/disconnect wallets, account state, chain switching | +| `UiKit` | 3 | Ecosystem-specific React components and hooks | +| `Relayer` | 3 | Gas-sponsored transaction execution via relayers | +| `AccessControl` | 3 | Role-based access control queries and snapshots | + +### Capability Bundles + +Higher-level components request specific **bundles** of capabilities rather than the full set. For example, `TransactionForm` expects a `TransactionFormCapabilities` type: an intersection of the capabilities needed for form rendering, execution, and status tracking. + +This means you can pass a partial adapter that only implements what the component actually needs. + + +## Runtimes and Profiles + +### Ecosystem Runtimes + +An `EcosystemRuntime` is a live instance that bundles capabilities for a specific network. Capabilities created within the same runtime share runtime-scoped state (network config, wallet connection, caches) and are disposed together. + +Runtimes are created by ecosystem adapter packages: + +```tsx +import { ecosystemDefinition } from '@openzeppelin/adapter-evm'; + +const runtime = await ecosystemDefinition.createRuntime( + 'composer', // profile name + ethereumMainnetConfig // network config +); + +// Access capabilities from the runtime +const address = runtime.addressing.formatAddress('0x...'); +const schema = await runtime.schema.generateFormSchema(contractDef); +const txHash = await runtime.execution.signAndBroadcast(txData, execConfig); + +// Clean up when done +runtime.dispose(); +``` + +### Profiles + +Adapters support five standard profiles that define which capabilities are included: + +| Profile | Use Case | Tier 1 | Tier 2 | Tier 3 | +| --- | --- | --- | --- | --- | +| `declarative` | Address formatting, explorer links | ✓ | - | - | +| `viewer` | Read contract state, no wallet needed | ✓ | ✓ | - | +| `transactor` | Execute transactions, basic wallet | ✓ | ✓ | Execution, Wallet | +| `composer` | Full UI with form rendering | ✓ | ✓ | ✓ (most) | +| `operator` | Administrative tools, access control | ✓ | ✓ | ✓ (all) | + +Choose the lightest profile that fits your use case. A dashboard that only displays contract state can use `viewer`; a full transaction builder should use `composer` or `operator`. + +### Runtime Lifecycle + +```mermaid +sequenceDiagram + participant App as Application + participant RP as RuntimeProvider + participant Adapter as Ecosystem Adapter + + App->>RP: Mount with resolveRuntime + App->>RP: setActiveNetworkId("ethereum-mainnet") + RP->>Adapter: createRuntime("composer", networkConfig) + Adapter-->>RP: EcosystemRuntime + + Note over RP: Cached by network ID + + App->>RP: setActiveNetworkId("polygon-mainnet") + RP->>Adapter: createRuntime("composer", polygonConfig) + Adapter-->>RP: New EcosystemRuntime + + Note over RP: Both runtimes cached + + App->>RP: Unmount + RP->>RP: dispose() all cached runtimes +``` + +`RuntimeProvider` maintains a **per-network-id registry** of runtimes. When a network is selected for the first time, the runtime is created asynchronously and cached. On unmount, all runtimes are disposed, releasing any wallet connections, subscriptions, or internal state. + +## Execution Strategies + +The execution system supports multiple ways to submit a transaction. The adapter selects the appropriate strategy based on the `ExecutionConfig`: + +```mermaid +flowchart LR + Form["TransactionForm"] --> Exec["ExecutionCapability"] + Exec --> Strategy{"Method?"} + Strategy -->|EOA| EOA["Direct wallet signing"] + Strategy -->|Relayer| Relay["Gas-sponsored via Relayer"] + EOA --> Chain["Blockchain"] + Relay --> Chain +``` + +Each ecosystem adapter defines which execution methods it supports. The EVM adapter supports both EOA and Relayer; other ecosystems may support only EOA. + +## How It Connects + +Putting it all together, here is how a typical application uses UIKit with an Ecosystem Adapter: + +```mermaid +%%{init: {'flowchart': {'nodeSpacing': 20, 'rankSpacing': 40}} }%% +flowchart TD + App(["Your React App"]) + RP(["RuntimeProvider"]) + WSP(["WalletStateProvider"]) + TF(["TransactionForm"]) + CSW(["ContractStateWidget"]) + AdapterEVM(["adapter-evm"]) + AdapterStellar(["adapter-stellar"]) + Ethereum(["Ethereum"]) + Stellar(["Stellar"]) + + App --> RP --> WSP + WSP --> TF & CSW + RP -.->|createRuntime| AdapterEVM & AdapterStellar + AdapterEVM --> Ethereum + AdapterStellar --> Stellar + + style App fill:#e8eaf6,stroke:#5c6bc0,color:#1a237e + style RP fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style WSP fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style TF fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style CSW fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style AdapterEVM fill:#fff3e0,stroke:#ff9800,color:#e65100 + style AdapterStellar fill:#fff3e0,stroke:#ff9800,color:#e65100 + style Ethereum fill:#fce4ec,stroke:#e91e63,color:#880e4f + style Stellar fill:#fce4ec,stroke:#e91e63,color:#880e4f +``` + +## Next Steps + +- [Components](/tools/uikit/components): Explore all available UI primitives and form fields +- [React Integration](/tools/uikit/react-integration): Deep dive into providers, hooks, and wallet state +- [Building an adapter](/ecosystem-adapters/building-an-adapter): Background on the adapter pattern and ecosystem integrations diff --git a/content/tools/uikit/components.mdx b/content/tools/uikit/components.mdx new file mode 100644 index 00000000..7c26f952 --- /dev/null +++ b/content/tools/uikit/components.mdx @@ -0,0 +1,267 @@ +--- +title: Components +--- + +OpenZeppelin UIKit ships two categories of components: **UI primitives** from `@openzeppelin/ui-components` and **renderer widgets** from `@openzeppelin/ui-renderer`. This page catalogs both and shows how to use them. + +## UI Primitives + +`@openzeppelin/ui-components` provides foundational React components built on [Radix UI](https://www.radix-ui.com/) and [shadcn/ui](https://ui.shadcn.com/) patterns. All components are styled with Tailwind CSS and support dark mode. + +### General Components + +| Component | Description | +| --- | --- | +| `Button` / `LoadingButton` | Action buttons with multiple variants (`default`, `destructive`, `outline`, `secondary`, `ghost`, `link`) | +| `Input` / `Textarea` | Text input primitives | +| `Label` | Accessible form labels | +| `Card` | Container with `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` | +| `Dialog` | Modal dialogs with `DialogTrigger`, `DialogContent`, `DialogHeader`, `DialogFooter` | +| `Alert` | Alert messages with `AlertTitle` and `AlertDescription` | +| `Checkbox` / `RadioGroup` | Selection inputs | +| `Select` | Dropdown select with `SelectTrigger`, `SelectContent`, `SelectItem` | +| `DropdownMenu` | Context menu with `DropdownMenuTrigger`, `DropdownMenuContent`, `DropdownMenuItem` | +| `Popover` | Floating content anchored to a trigger element | +| `Progress` | Progress bar indicator | +| `Tabs` | Tab navigation with `TabsList`, `TabsTrigger`, `TabsContent` | +| `Tooltip` | Hover tooltips | +| `Accordion` | Collapsible content sections | +| `Banner` | Full-width notification banners | +| `Calendar` / `DateRangePicker` | Date selection and date-range picker components | +| `EmptyState` | Placeholder UI for empty lists or initial states | +| `ExternalLink` | Anchor that opens in a new tab with appropriate `rel` attributes | +| `Form` | Form wrapper integrating react-hook-form with accessible error messages | +| `Header` / `Footer` | Page-level header and footer layout components | +| `Sonner` | Toast notification system (powered by [sonner](https://sonner.emilkowal.dev/)) | +| `Sidebar` | Sidebar navigation layout | +| `Wizard` | Multi-step wizard flow | + +### Blockchain-Specific Components + +| Component | Description | +| --- | --- | +| `NetworkSelector` | Searchable network dropdown with optional multi-select mode | +| `NetworkIcon` | Network-specific icon (renders the correct logo for a given network ID) | +| `NetworkStatusBadge` | Colored badge showing network status (e.g. connected, syncing) | +| `EcosystemDropdown` | Ecosystem selection with chain icons | +| `EcosystemIcon` | Chain-specific icons (Ethereum, Stellar, Polkadot, etc.) | +| `AddressDisplay` | Formatted address with optional alias labels and edit controls | +| `ViewContractStateButton` | Quick-action button to open the contract state widget | +| `OverflowMenu` | Compact "..." dropdown for secondary actions | + +### Usage + +```tsx +import { Button, Card, CardContent, CardHeader, CardTitle } from '@openzeppelin/ui-components'; + +function ContractCard({ name, address }) { + return ( + + + {name} + + +

{address}

+ +
+
+ ); +} +``` + +## Form Fields + +The field components are designed for use with [react-hook-form](https://react-hook-form.com/). Each field handles its own validation, formatting, and blockchain-specific behavior. + +| Field | Input Type | Use Case | +| --- | --- | --- | +| `AddressField` | Blockchain address | Contract addresses, recipient addresses; validates format per ecosystem | +| `AmountField` | Token amounts | Token transfers; handles decimals and BigInt formatting | +| `BigIntField` | Large integers | Raw `uint256` values, timestamps, token IDs | +| `BytesField` | Hex byte strings | Calldata, hashes, signatures | +| `TextField` | Free-form text | String parameters, names, URIs | +| `UrlField` | URL | Endpoint URLs, metadata URIs; validates URL format | +| `PasswordField` | Masked text | API keys, secrets; input is hidden by default | +| `NumberField` | Numeric values | Counts, percentages, indices | +| `BooleanField` | True/false | Feature flags, approvals | +| `EnumField` | Enum selection | Contract enum parameters | +| `SelectField` | Dropdown | Predefined option lists | +| `SelectGroupedField` | Grouped dropdown | Categorized options | +| `RadioField` | Radio buttons | Small option sets | +| `TextAreaField` | Multi-line text | ABI input, JSON data | +| `CodeEditorField` | Code / JSON editor | Contract source code, ABI JSON, configuration files | +| `DateTimeField` | Date and time | Timestamps, scheduling, time-locked parameters | +| `FileUploadField` | File upload | Importing ABIs, uploading contract artifacts | +| `ArrayField` | Dynamic arrays | Array parameters (e.g. `address[]`, `uint256[]`) | +| `ArrayObjectField` | Array of structs | Repeated struct entries (e.g. batch transfers, multi-recipient calls) | +| `MapField` | Key-value pairs | Mapping-like structures | +| `ObjectField` | Nested objects | Struct/tuple parameters | + +### Field Example + +```tsx +import { useForm } from 'react-hook-form'; +import { AddressField, AmountField, Button } from '@openzeppelin/ui-components'; + +function TransferForm() { + const { control, handleSubmit } = useForm({ + defaultValues: { to: '', amount: '' }, + }); + + return ( +
+ + + + + ); +} +``` + +### Address Label & Suggestion Contexts + +UIKit provides context-driven address resolution that works automatically with `AddressField` and `AddressDisplay`: + +- **`AddressLabelProvider`**: When mounted, all `AddressDisplay` instances in the subtree auto-resolve human-readable labels (e.g. "Treasury Multisig" instead of `0x1234...abcd`). +- **`AddressSuggestionProvider`**: When mounted, all `AddressField` instances show autocomplete suggestions as the user types. +- **`useAddressLabel(address, networkId?)`**: Hook to resolve a label from the nearest provider. + +## Renderer Widgets + +`@openzeppelin/ui-renderer` provides high-level, ready-to-use widgets for common blockchain UI patterns. These compose the lower-level components with adapter capabilities. + +![TransactionForm and dynamic fields in the example app](/uikit/form-renderer.png) + +### TransactionForm + +The primary widget for rendering blockchain transaction forms. Accepts a declarative schema and an adapter capabilities bundle: + +```tsx +import { TransactionForm } from '@openzeppelin/ui-renderer'; +import type { RenderFormSchema, TransactionFormCapabilities } from '@openzeppelin/ui-types'; + +const schema: RenderFormSchema = { + id: 'approve-form', + title: 'Approve Spending', + fields: [ + { id: 'spender', name: 'spender', type: 'address', label: 'Spender' }, + { id: 'amount', name: 'amount', type: 'amount', label: 'Amount' }, + ], + layout: { columns: 1, spacing: 'normal', labelPosition: 'top' }, + submitButton: { text: 'Approve', loadingText: 'Approving...' }, +}; + +function ApprovePage({ adapter }: { adapter: TransactionFormCapabilities }) { + return ( + console.log('Approved:', result)} + /> + ); +} +``` + +**Key props:** + +| Prop | Type | Description | +| --- | --- | --- | +| `schema` | `RenderFormSchema` | Form layout, fields, and submit button config | +| `contractSchema` | `ContractSchema` | ABI / function metadata for the target contract | +| `adapter` | `TransactionFormCapabilities` | Capability bundle from the active runtime | +| `executionConfig` | `ExecutionConfig` | EOA vs. relayer configuration | +| `onTransactionSuccess` | callback | Fires after successful transaction | +| `isWalletConnected` | `boolean` | Whether a wallet session is active | + +### DynamicFormField + +Renders individual form fields dynamically based on type configuration. Used internally by `TransactionForm` but available for custom form layouts: + +```tsx +import { DynamicFormField } from '@openzeppelin/ui-renderer'; + + +``` + +### ContractStateWidget + +
+
+ +![ContractStateWidget](/uikit/contract-state-widget.png) + +
+
+ +Displays read-only contract state by querying view functions. The widget automatically discovers all view functions from the contract schema and renders their return types. Supports auto-refresh to keep values current. + +Each row shows the function name, return type, and live result. No wallet connection is required. Pass `query` and `schema` capabilities from the active runtime: + +```tsx +import { ContractStateWidget } from '@openzeppelin/ui-renderer'; + + +``` + +
+
+ +### AddressBookWidget + +A full-featured address book UI for managing saved addresses and their human-readable aliases. Supports CRUD operations, search, and import/export. + +![AddressBookWidget](/uikit/address-book-widget.png) + +`AddressBookWidget` works with the [`@openzeppelin/ui-storage`](/tools/uikit/storage) package, specifically the **account alias plugin** which persists address-to-name mappings in IndexedDB via Dexie.js. When mounted alongside `AddressLabelProvider` and `AddressSuggestionProvider`, saved aliases automatically appear in all `AddressDisplay` and `AddressField` components throughout the app. + +```tsx +import { AddressBookWidget } from '@openzeppelin/ui-renderer'; +import { createDexieDatabase, ALIAS_SCHEMA, useAddressBookWidgetProps } from '@openzeppelin/ui-storage'; +import Dexie from 'dexie'; + +const db = createDexieDatabase(new Dexie('my-app'), ALIAS_SCHEMA); + +function AddressBook({ addressing }) { + const widgetProps = useAddressBookWidgetProps(db, { networkId: 'ethereum-mainnet' }); + return ; +} +``` + +See the [Storage](/tools/uikit/storage) page for setup and plugin configuration. + +### Other Renderer Components + +| Component | Description | +| --- | --- | +| `ContractActionBar` | Network status display with contract state toggle | +| `ExecutionConfigDisplay` | Configure execution method (EOA / Relayer) | +| `NetworkSettingsDialog` | Configure RPC endpoints and indexer URLs | +| `WalletConnectionWithSettings` | Wallet connection UI with integrated settings | +| `AliasEditPopover` | Inline popover for editing address aliases | + +## Next Steps + +- [React Integration](/tools/uikit/react-integration): Connect components to wallet state and runtime capabilities +- [Theming & Styling](/tools/uikit/theming): Customize the visual appearance +- [Getting Started](/tools/uikit/getting-started): Step-by-step setup guide diff --git a/content/tools/uikit/getting-started.mdx b/content/tools/uikit/getting-started.mdx new file mode 100644 index 00000000..d797e5b6 --- /dev/null +++ b/content/tools/uikit/getting-started.mdx @@ -0,0 +1,222 @@ +--- +title: Getting Started +--- + +This guide walks you through installing OpenZeppelin UIKit, configuring styles, and rendering your first blockchain transaction form. + + +**Live example**: See UIKit and ecosystem adapters working together in the browser at [**openzeppelin-ui.netlify.app**](https://openzeppelin-ui.netlify.app). + + +## Prerequisites + +- **Node.js** >= 20.19.0 +- **React** 19 +- **Tailwind CSS** 4 +- A package manager: `pnpm` (recommended), `npm`, or `yarn` + +## Installation + +Install only the packages your application needs. The packages are designed to be incrementally adopted. + +### Minimal Setup (Types + Components) + +For projects that only need the component library and type system: + +```bash +pnpm add @openzeppelin/ui-types @openzeppelin/ui-utils @openzeppelin/ui-components @openzeppelin/ui-styles +``` + +### Full Setup (With Rendering + React Integration) + +For applications that need transaction form rendering and wallet integration: + +```bash +pnpm add @openzeppelin/ui-types @openzeppelin/ui-utils @openzeppelin/ui-styles \ + @openzeppelin/ui-components @openzeppelin/ui-react @openzeppelin/ui-renderer +``` + +### Optional Packages + +```bash +# IndexedDB persistence (address book, settings, etc.) +pnpm add @openzeppelin/ui-storage + +# Dev CLI for Tailwind wiring and local development +pnpm add -D @openzeppelin/ui-dev-cli +``` + +### Ecosystem Adapters + +You also need at least one ecosystem adapter package for the blockchain(s) your app supports: + +```bash +# EVM (Ethereum, Polygon, Arbitrum, etc.) +pnpm add @openzeppelin/adapter-evm + +# Stellar / Soroban +pnpm add @openzeppelin/adapter-stellar + +# Polkadot (EVM-compatible path) +pnpm add @openzeppelin/adapter-polkadot +``` + +## Step 1: Configure Tailwind CSS + +UIKit uses Tailwind CSS 4 for styling. Components ship class names but **not** compiled CSS. Your application's Tailwind build must know where to find them. + +### Automated Setup (Recommended) + +The dev CLI handles Tailwind configuration automatically: + +```bash +pnpm add -D @openzeppelin/ui-dev-cli +pnpm exec oz-ui-dev tailwind doctor --project "$PWD" +pnpm exec oz-ui-dev tailwind fix --project "$PWD" +``` + +This generates an `oz-tailwind.generated.css` file with the correct `@source` directives for all installed OpenZeppelin packages. + +### Manual Setup + +If you prefer manual configuration, your entry CSS must register the OpenZeppelin package sources: + +```css +@layer base, components, utilities; + +@import 'tailwindcss' source(none); +@source "./"; +@source "../"; +@source "../node_modules/@openzeppelin/ui-components"; +@source "../node_modules/@openzeppelin/ui-react"; +@source "../node_modules/@openzeppelin/ui-renderer"; +@source "../node_modules/@openzeppelin/ui-styles"; +@source "../node_modules/@openzeppelin/ui-utils"; +@import '@openzeppelin/ui-styles/global.css'; +``` + + +A bare Tailwind import is not enough. Tailwind v4 must be told to scan the OpenZeppelin `node_modules` paths, or component classes will be missing from the final CSS. + + +## Step 2: Use Components + +UIKit components work with [react-hook-form](https://react-hook-form.com/) for form state management. Here is a simple form with an address field and a submit button: + +```tsx +import { useForm } from 'react-hook-form'; +import { Button, TextField, AddressField } from '@openzeppelin/ui-components'; + +function SimpleForm() { + const { control, handleSubmit } = useForm(); + + const onSubmit = (data) => { + console.log('Form data:', data); + }; + + return ( +
+ + + + + ); +} +``` + +## Step 3: Render a Transaction Form + +The renderer package provides a declarative way to build transaction forms from a schema. The form fields, layout, and submission are all driven by data. + +```tsx +import { TransactionForm } from '@openzeppelin/ui-renderer'; +import type { RenderFormSchema } from '@openzeppelin/ui-types'; + +const schema: RenderFormSchema = { + id: 'transfer-form', + title: 'Transfer Tokens', + fields: [ + { id: 'to', name: 'to', type: 'address', label: 'Recipient' }, + { id: 'amount', name: 'amount', type: 'amount', label: 'Amount' }, + ], + layout: { columns: 1, spacing: 'normal', labelPosition: 'top' }, + submitButton: { text: 'Transfer', loadingText: 'Transferring...' }, +}; + +function TransferPage({ adapter, contractSchema }) { + return ( + { + console.log('Transaction successful:', result); + }} + /> + ); +} +``` + +The `adapter` prop accepts a `TransactionFormCapabilities` object: a bundle of capabilities from your active [Ecosystem Runtime](/tools/uikit/architecture#runtimes). + +## Step 4: Wire Up React Providers + +For wallet integration and multi-network support, wrap your app with `RuntimeProvider` and `WalletStateProvider`: + +```tsx +import { RuntimeProvider, WalletStateProvider } from '@openzeppelin/ui-react'; +import { ecosystemDefinition } from '@openzeppelin/adapter-evm'; + +async function resolveRuntime(networkConfig) { + return ecosystemDefinition.createRuntime('composer', networkConfig); +} + +function App() { + return ( + + + + + + ); +} +``` + +Then access wallet state and runtime capabilities from any component: + +```tsx +import { useWalletState } from '@openzeppelin/ui-react'; + +function WalletInfo() { + const { activeNetworkConfig, activeRuntime, isRuntimeLoading } = useWalletState(); + + if (isRuntimeLoading || !activeRuntime) { + return

Loading...

; + } + + return

Connected to {activeNetworkConfig?.name}

; +} +``` + +For the full React integration guide, see [React Integration](/tools/uikit/react-integration). + +## Next Steps + +- [Architecture](/tools/uikit/architecture): Understand the package layers, capability tiers, and runtime model +- [Components](/tools/uikit/components): Explore UI primitives and blockchain-aware form fields +- [React Integration](/tools/uikit/react-integration): Deep dive into providers, hooks, and wallet state +- [Theming & Styling](/tools/uikit/theming): Customize tokens, colors, and dark mode +- [Building an adapter](/ecosystem-adapters/building-an-adapter): Background on adapter packages and ecosystem integrations diff --git a/content/tools/uikit/index.mdx b/content/tools/uikit/index.mdx new file mode 100644 index 00000000..1ebcf511 --- /dev/null +++ b/content/tools/uikit/index.mdx @@ -0,0 +1,120 @@ +--- +title: OpenZeppelin UIKit +--- + +A modular React component library for building blockchain transaction interfaces. It is chain-agnostic, capability-driven, and designed for multi-ecosystem applications. + + +**Source code**: OpenZeppelin UIKit is open-source. Browse the implementation, open issues, and contribute at [**github.com/OpenZeppelin/openzeppelin-ui**](https://github.com/OpenZeppelin/openzeppelin-ui). + +**Live example**: Explore a hosted demo of UIKit with ecosystem adapters at [**openzeppelin-ui.netlify.app**](https://openzeppelin-ui.netlify.app). + + + + + + + + + + + +## What is OpenZeppelin UIKit? + +OpenZeppelin UIKit is a set of modular npm packages that provide everything needed to build rich blockchain UIs in React. Instead of a monolithic library, it ships as a **layered stack** from low-level types and utilities up to high-level form renderers and wallet integration. + +Each layer is independently installable. Use only the pieces you need: the type system for a headless integration, the component library for a design system, or the full renderer for turnkey transaction forms. + +```mermaid +%%{init: {'flowchart': {'nodeSpacing': 20, 'rankSpacing': 40}} }%% +flowchart TD + App(["Your Application"]) + + Storage(["ui-storage"]) + Renderer(["ui-renderer"]) + ReactPkg(["ui-react"]) + Components(["ui-components"]) + Utils(["ui-utils"]) + Types(["ui-types"]) + + EVM(["adapter-evm"]) + Stellar(["adapter-stellar"]) + Polkadot(["adapter-polkadot"]) + + App --> Storage & Renderer & ReactPkg + Storage --> Utils + Renderer --> Components + ReactPkg --> Components + Components --> Utils + Utils --> Types + ReactPkg -.->|consumes| EVM & Stellar & Polkadot + + style App fill:#e8eaf6,stroke:#5c6bc0,color:#1a237e + style Storage fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style Renderer fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style ReactPkg fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style Components fill:#e0f2f1,stroke:#26a69a,color:#004d40 + style Utils fill:#fff3e0,stroke:#ff9800,color:#e65100 + style Types fill:#fff3e0,stroke:#ff9800,color:#e65100 + style EVM fill:#fce4ec,stroke:#e91e63,color:#880e4f + style Stellar fill:#fce4ec,stroke:#e91e63,color:#880e4f + style Polkadot fill:#fce4ec,stroke:#e91e63,color:#880e4f +``` + +## Packages + +| Package | Description | Layer | +| --- | --- | --- | +| [`@openzeppelin/ui-types`](https://www.npmjs.com/package/@openzeppelin/ui-types) | Shared TypeScript type definitions: capabilities, schemas, form models | 1 | +| [`@openzeppelin/ui-utils`](https://www.npmjs.com/package/@openzeppelin/ui-utils) | Framework-agnostic utilities: config, logging, validation, routing | 2 | +| [`@openzeppelin/ui-styles`](https://www.npmjs.com/package/@openzeppelin/ui-styles) | Centralized Tailwind CSS 4 theme with OKLCH tokens and dark mode | 3 | +| [`@openzeppelin/ui-components`](https://www.npmjs.com/package/@openzeppelin/ui-components) | React UI primitives and blockchain-aware form fields (shadcn/ui based) | 4 | +| [`@openzeppelin/ui-react`](https://www.npmjs.com/package/@openzeppelin/ui-react) | React context providers, runtime management, and wallet hooks | 5 | +| [`@openzeppelin/ui-renderer`](https://www.npmjs.com/package/@openzeppelin/ui-renderer) | Transaction form rendering engine and contract state widgets | 6 | +| [`@openzeppelin/ui-storage`](https://www.npmjs.com/package/@openzeppelin/ui-storage) | IndexedDB storage abstraction with Dexie.js and address book plugin | 7 | + +## Key Design Principles + +**Chain-agnostic core.** UIKit packages never import chain-specific logic. Blockchain details are handled entirely by ecosystem adapter packages. + +**Capability-driven, not monolithic.** Instead of one large adapter interface, the system defines small, focused [capabilities](/tools/uikit/architecture#capabilities) (addressing, query, execution, wallet, etc.) organized into tiers. Components request only the capabilities they need. + +**Pay for what you use.** Install only the layers your app requires. A simple form builder can use just `ui-types` + `ui-components`. A full transaction dashboard can add `ui-renderer` + `ui-react` + `ui-storage`. + +**Multi-ecosystem ready.** A single app can support EVM, Stellar, Polkadot, and more simultaneously. The runtime system manages per-network adapter instances with proper lifecycle and disposal. + +## Ecosystem Adapter Integration + +UIKit connects to blockchains through ecosystem adapter packages, standalone packages that translate chain-specific operations into the shared capability model. + +```mermaid +sequenceDiagram + participant App as Your App + participant React as UIKit React + participant Adapter as Ecosystem Adapter + participant Chain as Blockchain + + App->>React: RuntimeProvider + WalletStateProvider + React->>Adapter: createRuntime(profile, networkConfig) + Adapter-->>React: EcosystemRuntime (capabilities) + React-->>App: useWalletState() → activeRuntime + + App->>React: TransactionForm adapter={capabilities} + React->>Adapter: execution.signAndBroadcast(...) + Adapter->>Chain: Submit transaction + Chain-->>Adapter: Transaction hash + Adapter-->>React: Status updates + React-->>App: UI reflects tx status +``` + +## Requirements + +- **Node.js** >= 20.19.0 +- **React** 19 +- **Tailwind CSS** 4 + +## Next Steps + +- [Getting Started](/tools/uikit/getting-started): Install, configure, and render your first form +- [Architecture](/tools/uikit/architecture): Deep dive into the capability model and runtime lifecycle +- [Building an adapter](/ecosystem-adapters/building-an-adapter): How chain-specific logic is decoupled from the UI diff --git a/content/tools/uikit/react-integration.mdx b/content/tools/uikit/react-integration.mdx new file mode 100644 index 00000000..3a745821 --- /dev/null +++ b/content/tools/uikit/react-integration.mdx @@ -0,0 +1,272 @@ +--- +title: React Integration +--- + +The `@openzeppelin/ui-react` package provides the runtime and wallet infrastructure that connects UIKit components to blockchain ecosystems. This page covers providers, hooks, wallet state management, and multi-ecosystem support. + + +**Live example**: See these providers and hooks in a running app at [**openzeppelin-ui.netlify.app**](https://openzeppelin-ui.netlify.app). + + +## Provider Setup + +Two providers form the foundation of a UIKit-powered React app: + +```mermaid +%%{init: {'flowchart': {'nodeSpacing': 30, 'rankSpacing': 50, 'wrappingWidth': 400}} }%% +flowchart TD + RP["RuntimeProvider
manages EcosystemRuntime instances"] + WSP["WalletStateProvider
active network · wallet state · hooks"] + App["Your Components"] + + RP --> WSP --> App + + classDef provider fill:#e0f2f1,stroke:#26a69a,color:#004d40,width:350px + classDef component fill:#e8eaf6,stroke:#5c6bc0,color:#1a237e + + class RP,WSP provider + class App component +``` + +### RuntimeProvider + +`RuntimeProvider` maintains a **registry of `EcosystemRuntime` instances** (one per network ID). It creates runtimes on demand, caches them, and disposes all of them on unmount. + +```tsx +import { RuntimeProvider } from '@openzeppelin/ui-react'; +import { ecosystemDefinition } from '@openzeppelin/adapter-evm'; + +async function resolveRuntime(networkConfig) { + return ecosystemDefinition.createRuntime('composer', networkConfig); +} + +function App() { + return ( + + {/* children */} + + ); +} +``` + +**Props:** + +| Prop | Type | Description | +| --- | --- | --- | +| `resolveRuntime` | `(networkConfig) => Promise` | Factory function that creates a runtime for a given network | + +### WalletStateProvider + +`WalletStateProvider` builds on `RuntimeProvider` to manage: + +- The **active network** ID and config +- The **active runtime** and its loading state +- **Wallet facade hooks** from the active runtime's `UiKitCapability` +- The ecosystem's **React UI provider** component (e.g. wagmi's `WagmiProvider`) + +```tsx +import { RuntimeProvider, WalletStateProvider } from '@openzeppelin/ui-react'; + +function App() { + return ( + + + + + + ); +} +``` + +**Props:** + +| Prop | Type | Description | +| --- | --- | --- | +| `initialNetworkId` | `string` | Network to activate on mount | +| `getNetworkConfigById` | `(id: string) => NetworkConfig` | Resolver for network configurations | +| `loadConfigModule` | `(runtime) => Promise` | Optional: load UI kit config modules for the runtime | + +## Hooks + +### useWalletState + +The primary hook for accessing global wallet and runtime state: + +```tsx +import { useWalletState } from '@openzeppelin/ui-react'; + +function Dashboard() { + const { + activeNetworkId, // Current network ID string + activeNetworkConfig, // Full NetworkConfig object + activeRuntime, // EcosystemRuntime instance (or null) + isRuntimeLoading, // True while runtime is being created + walletFacadeHooks, // Ecosystem-specific React hooks + setActiveNetworkId, // Switch networks + reconfigureActiveUiKit // Re-initialize UI kit config + } = useWalletState(); + + if (isRuntimeLoading || !activeRuntime) { + return

Loading runtime...

; + } + + return ( +
+

Network: {activeNetworkConfig?.name}

+

Ecosystem: {activeRuntime.ecosystem}

+
+ ); +} +``` + +### useRuntimeContext + +Low-level hook for direct access to the runtime registry: + +```tsx +import { useRuntimeContext } from '@openzeppelin/ui-react'; + +function AdvancedComponent() { + const { getRuntimeForNetwork } = useRuntimeContext(); + + const handleQuery = async () => { + const { runtime, isLoading } = getRuntimeForNetwork(polygonConfig); + if (isLoading || !runtime) return; + const result = await runtime.query.queryViewFunction(/* ... */); + }; +} +``` + +### Derived Wallet Hooks + +These hooks abstract wallet interactions across ecosystems, providing a consistent API regardless of whether the user is connected to EVM, Stellar, or any other chain: + +| Hook | Returns | +| --- | --- | +| `useDerivedAccountStatus()` | `{ isConnected, address, chainId }` | +| `useDerivedConnectStatus()` | `{ connect, isPending, error }` | +| `useDerivedDisconnect()` | `{ disconnect }` | +| `useDerivedSwitchChainStatus()` | `{ switchChain, isPending }` | +| `useDerivedChainInfo()` | `{ chainId, chains }` | +| `useWalletReconnectionHandler()` | Manages automatic wallet reconnection | + +These are built on top of the `walletFacadeHooks` from the active runtime's `UiKitCapability`, which wraps the ecosystem's native wallet library (e.g. `wagmi` for EVM, Stellar Wallets Kit for Stellar). + +## Wallet Components + +`@openzeppelin/ui-react` ships ready-to-use wallet UI components: + +| Component | Description | +| --- | --- | +| `WalletConnectionHeader` | Compact header bar with wallet status and connect/disconnect | +| `WalletConnectionUI` | Full wallet connection interface | +| `WalletConnectionWithSettings` | Wallet connection with integrated network/RPC settings | +| `NetworkSwitchManager` | Handles programmatic network switching with user confirmation | + +### NetworkSwitchManager + +Pass capabilities from the active runtime, not a monolithic adapter instance: + +```tsx +import { useState } from 'react'; +import { NetworkSwitchManager, useWalletState } from '@openzeppelin/ui-react'; + +function MyApp() { + const [targetNetwork, setTargetNetwork] = useState(null); + const { activeRuntime } = useWalletState(); + + const wallet = activeRuntime?.wallet; + const networkCatalog = activeRuntime?.networkCatalog; + + return ( + <> + {wallet && networkCatalog && targetNetwork && ( + setTargetNetwork(null)} + /> + )} + + ); +} +``` + +### Ecosystem Wallet Components + +Each adapter can provide its own wallet components via the `UiKitCapability`. These are accessed through the runtime: + +```tsx +const walletComponents = runtime.uiKit?.getEcosystemWalletComponents(); +// { ConnectButton, AccountDisplay, NetworkSwitcher } +``` + +For EVM, this integrates with [RainbowKit](https://www.rainbowkit.com/). For Stellar, it integrates with [Stellar Wallets Kit](https://stellarwalletskit.dev/). Each adapter maps its ecosystem's wallet library into the standardized component interface. + +## Multi-Ecosystem Apps + +A single app can support multiple blockchain ecosystems. The key is the `resolveRuntime` function, which determines which adapter to use based on the network config: + +```tsx +import { ecosystemDefinition as evmDef } from '@openzeppelin/adapter-evm'; +import { ecosystemDefinition as stellarDef } from '@openzeppelin/adapter-stellar'; + +async function resolveRuntime(networkConfig) { + switch (networkConfig.ecosystem) { + case 'evm': + return evmDef.createRuntime('composer', networkConfig); + case 'stellar': + return stellarDef.createRuntime('composer', networkConfig); + default: + throw new Error(`Unsupported ecosystem: ${networkConfig.ecosystem}`); + } +} + +function App() { + return ( + + + + + + ); +} +``` + +When the user switches networks, `WalletStateProvider` automatically resolves the correct runtime and updates the wallet facade hooks, UI provider, and wallet components for the new ecosystem. + +```mermaid +sequenceDiagram + participant User + participant WSP as WalletStateProvider + participant RP as RuntimeProvider + participant EVM as EVM Adapter + participant Stellar as Stellar Adapter + + User->>WSP: setActiveNetworkId("ethereum-mainnet") + WSP->>RP: getRuntimeForNetwork(ethConfig) + RP->>EVM: createRuntime("composer", ethConfig) + EVM-->>RP: EVM Runtime + RP-->>WSP: EVM Runtime + Note over WSP: Wallet hooks: wagmi / UI: RainbowKit + + User->>WSP: setActiveNetworkId("stellar-pubnet") + WSP->>RP: getRuntimeForNetwork(stellarConfig) + RP->>Stellar: createRuntime("composer", stellarConfig) + Stellar-->>RP: Stellar Runtime + RP-->>WSP: Stellar Runtime + Note over WSP: Wallet hooks: Stellar Wallets Kit +``` + +## Next Steps + +- [Components](/tools/uikit/components): Browse all available components and form fields +- [Theming & Styling](/tools/uikit/theming): Customize the visual design +- [Building an adapter](/ecosystem-adapters/building-an-adapter): Background on adapter packages and ecosystem integrations diff --git a/content/tools/uikit/storage.mdx b/content/tools/uikit/storage.mdx new file mode 100644 index 00000000..d278875a --- /dev/null +++ b/content/tools/uikit/storage.mdx @@ -0,0 +1,341 @@ +--- +title: Storage +--- + +`@openzeppelin/ui-storage` provides client-side persistence for UIKit applications. It is built on [Dexie.js](https://dexie.org/) (an IndexedDB wrapper) and ships a plugin system for extending storage with domain-specific functionality. + +## Installation + +```bash +pnpm add @openzeppelin/ui-storage +``` + +## Why Use the Storage Plugin? + +Browser applications commonly reach for `localStorage` to persist user data. While fine for a handful of string values, `localStorage` hits hard limits as your app grows: + +| Concern | `localStorage` | `@openzeppelin/ui-storage` (IndexedDB) | +| --- | --- | --- | +| **Storage quota** | ~5 MB per origin | Hundreds of MB, often limited only by available disk space | +| **Data model** | Flat key-value strings; every read/write requires `JSON.parse`/`JSON.stringify` | Structured object stores with typed records, indexes, and compound queries | +| **Performance** | Synchronous: blocks the main thread on every call | Asynchronous: all reads and writes are non-blocking | +| **Querying** | Full-scan only; no way to filter or sort without loading everything | Indexed lookups and range queries via Dexie.js | +| **Concurrent tabs** | No built-in synchronization; race conditions on simultaneous writes | Transactional; supports multi-tab coordination out of the box | +| **Schema evolution** | Manual: you must handle migrations yourself | Declarative versioned schemas with automatic upgrade migrations | + +Beyond these raw IndexedDB advantages, the storage plugin adds an **opinionated layer** designed for blockchain UIs: + +- **Typed base classes**: `EntityStorage` and `KeyValueStorage` give you CRUD, validation, quota handling, and timestamps without boilerplate. +- **Plugin system**: domain-specific plugins (like the built-in account alias plugin) drop into any Dexie database and integrate with UIKit providers automatically. +- **Reactive hooks**: `useLiveQuery` re-renders components when the underlying IndexedDB data changes, including changes from other browser tabs. +- **Quota-safe writes**: the `withQuotaHandling` wrapper catches `QuotaExceededError` and surfaces a typed error code so your UI can handle it gracefully instead of silently failing. + +### When Does It Make Sense? + +Use `@openzeppelin/ui-storage` when your application needs to persist **structured, queryable data** on the client, especially data that grows over time or must survive page reloads. Common examples include: + +- **Contract history and recent contracts**: the [Role Manager](https://github.com/OpenZeppelin/role-manager) persists recently accessed contracts per network in a `RecentContractsStorage` built on `EntityStorage`. Records are indexed by `[networkId+address]` for fast lookups and sorted by `lastAccessed` so the most recent entries always appear first. The same result in `localStorage` would require deserializing, sorting, and re-serializing an entire array. + +- **UI configuration and form state**: the [UI Builder](https://github.com/OpenZeppelin/ui-builder) stores complete contract UI configurations (including large ABIs and compiled contract definitions) in a `ContractUIStorage` entity store. With records that can reach tens of megabytes, `localStorage`'s 5 MB limit would be a non-starter. The storage plugin also powers the builder's import/export and multi-tab auto-save features. + +- **User preferences and settings**: both projects use `KeyValueStorage` for simple typed preferences (theme, active network, page size), a lightweight alternative that still benefits from async I/O and schema versioning. + +- **Address book and aliases**: the built-in account alias plugin persists address-to-name mappings in IndexedDB and wires them into UIKit's `AddressLabelProvider` and `AddressSuggestionProvider`. Every `AddressDisplay` and `AddressField` in the component tree resolves labels automatically without any per-component wiring. + +If your app only stores a single flag or token, `localStorage` is perfectly adequate. Reach for the storage plugin when you need **indexed queries, large payloads, multi-tab safety, or domain-specific persistence** that integrates with the rest of the UIKit ecosystem. + +## Core Abstractions + +The package exposes two base classes that your application can extend: + +| Class | Description | +| --- | --- | +| `EntityStorage` | Generic IndexedDB store for typed entities. Handles create, read, update, delete, and query. | +| `KeyValueStorage` | Simple key-value store backed by IndexedDB. Useful for persisting settings and preferences. | + +Both classes wrap a Dexie database instance and can be composed with plugins. + +## Account Alias Plugin + +
+
+ +![Account alias plugin](/uikit/storage-account-alias-plugin.png) + +
+
+ +The built-in **account alias plugin** persists address-to-name mappings. It powers the `AddressBookWidget` and integrates with `AddressLabelProvider` and `AddressSuggestionProvider` to resolve human-readable labels automatically across all `AddressDisplay` and `AddressField` components. + +
+
+ +### Setup + +Create a Dexie database instance with the alias schema, then use the provided hooks: + +```tsx +import { + createDexieDatabase, + ALIAS_SCHEMA, + useAliasLabelResolver, + useAliasSuggestionResolver, + useAddressBookWidgetProps, +} from '@openzeppelin/ui-storage'; +import Dexie from 'dexie'; + +const db = createDexieDatabase(new Dexie('my-app'), ALIAS_SCHEMA); +``` + +### Integration with Address Providers + +Mount the providers near the root of your app to activate automatic label resolution. The `useAliasLabelResolver` and `useAliasSuggestionResolver` hooks return props that can be spread directly into the respective providers: + +```tsx +import { + AddressLabelProvider, + AddressSuggestionProvider, +} from '@openzeppelin/ui-components'; +import { + createDexieDatabase, + ALIAS_SCHEMA, + useAliasLabelResolver, + useAliasSuggestionResolver, +} from '@openzeppelin/ui-storage'; +import Dexie from 'dexie'; + +const db = createDexieDatabase(new Dexie('my-app'), ALIAS_SCHEMA); + +function App() { + const labelResolver = useAliasLabelResolver(db); + const suggestionResolver = useAliasSuggestionResolver(db); + + return ( + + + + + + ); +} +``` + +Once mounted: + +- Every `AddressDisplay` in the subtree automatically shows the saved alias instead of the raw address. +- Every `AddressField` shows autocomplete suggestions as the user types. + +### AddressBookWidget + +Wire `AddressBookWidget` using the `useAddressBookWidgetProps` hook, which returns all the props the widget needs: + +```tsx +import { AddressBookWidget } from '@openzeppelin/ui-renderer'; +import { createDexieDatabase, ALIAS_SCHEMA, useAddressBookWidgetProps } from '@openzeppelin/ui-storage'; +import Dexie from 'dexie'; + +const db = createDexieDatabase(new Dexie('my-app'), ALIAS_SCHEMA); + +function AddressBook({ addressing }) { + const widgetProps = useAddressBookWidgetProps(db, { networkId: 'ethereum-mainnet' }); + + return ( + + ); +} +``` + +See [Components (AddressBookWidget)](/tools/uikit/components#addressbookwidget) for a screenshot. + +## Custom Entity Stores + +Extend `EntityStorage` to create typed stores for your own domain objects. The constructor takes a Dexie database instance and a table name: + +```tsx +import { EntityStorage, createDexieDatabase } from '@openzeppelin/ui-storage'; +import Dexie from 'dexie'; + +interface SavedContract { + id: string; + address: string; + name: string; + networkId: string; + addedAt: number; +} + +const MY_SCHEMA = { contracts: '++id, address, networkId' }; +const db = createDexieDatabase(new Dexie('my-app'), MY_SCHEMA); + +class ContractStore extends EntityStorage { + constructor() { + super(db, 'contracts'); + } + + async findByNetwork(networkId: string): Promise { + return this.query((item) => item.networkId === networkId); + } +} + +const contractStore = new ContractStore(); +await contractStore.add({ id: '...', address: '0x...', name: 'My Token', networkId: 'ethereum-mainnet', addedAt: Date.now() }); +``` + +## Key-Value Store + +Use `KeyValueStorage` for simple settings and flags. Like `EntityStorage`, it takes a Dexie db instance and a table name: + +```tsx +import { KeyValueStorage, createDexieDatabase } from '@openzeppelin/ui-storage'; +import Dexie from 'dexie'; + +const MY_SCHEMA = { settings: 'key' }; +const db = createDexieDatabase(new Dexie('my-app'), MY_SCHEMA); + +class AppSettings extends KeyValueStorage { + constructor() { + super(db, 'settings'); + } +} + +const settings = new AppSettings(); +await settings.set('theme', 'dark'); +const theme = await settings.get('theme'); // 'dark' +``` + +## React Hook Factories + +The storage package ships a set of **factory functions** that turn any `EntityStorage` or `KeyValueStorage` into a fully reactive React hook, complete with live queries, CRUD wrappers, and file import/export. These are the recommended way to consume storage in React components. + +### `createRepositoryHook` + +The main factory. It composes the lower-level factories below into a single hook that provides everything a component needs: live data, loading state, CRUD operations, and optional file I/O. + +```tsx +import { createRepositoryHook, createDexieDatabase, EntityStorage } from '@openzeppelin/ui-storage'; +import Dexie from 'dexie'; +import { toast } from 'sonner'; + +interface Bookmark { id: string; url: string; label: string; } + +const SCHEMA = { bookmarks: '++id, url' }; +const db = createDexieDatabase(new Dexie('my-app'), [{ version: 1, stores: SCHEMA }]); + +class BookmarkStore extends EntityStorage { + constructor() { super(db, 'bookmarks'); } + async exportJson() { return JSON.stringify(await this.getAll()); } + async importJson(json: string) { /* parse and bulk-insert */ } +} + +const bookmarkStore = new BookmarkStore(); + +const useBookmarks = createRepositoryHook({ + db, + tableName: 'bookmarks', + repo: bookmarkStore, + onError: (title, err) => toast.error(title), + fileIO: { + exportJson: () => bookmarkStore.exportJson(), + importJson: (json) => bookmarkStore.importJson(json), + filePrefix: 'bookmarks-backup', + }, +}); + +function BookmarkList() { + const { records, isLoading, save, remove, exportAsFile, importFromFile } = useBookmarks(); + + if (isLoading) return

Loading…

; + + return ( +
    + {records?.map((b) => ( +
  • + {b.label} +
  • + ))} +
+ ); +} +``` + +The hook returned by `createRepositoryHook` exposes: + +| Property | Type | Description | +| --- | --- | --- | +| `records` | `T[] \| undefined` | Live query result; `undefined` while loading | +| `isLoading` | `boolean` | `true` until the first query resolves | +| `save` | `(record) => Promise` | Insert a new record | +| `update` | `(id, partial) => Promise` | Patch an existing record | +| `remove` | `(id) => Promise` | Delete by ID | +| `clear` | `() => Promise` | Remove all records | +| `exportAsFile` | `(ids?) => Promise` | Download records as a timestamped JSON file (only when `fileIO` is configured) | +| `importFromFile` | `(file) => Promise` | Import from a JSON `File` (only when `fileIO` is configured) | + +### Lower-Level Factories + +`createRepositoryHook` is built from three smaller factories that can be used independently when you need finer-grained control: + +| Factory | Purpose | +| --- | --- | +| `createLiveQueryHook(db, tableName, query?)` | Returns a hook that re-renders whenever the underlying Dexie table changes. Powered by `useLiveQuery` from `dexie-react-hooks`. | +| `createCrudHook(repo, { onError? })` | Wraps a `CrudRepository` (anything with `save`, `update`, `delete`, `clear`) with unified error handling. | +| `createJsonFileIO({ exportJson, importJson }, { filePrefix, onError? })` | Produces `exportAsFile` / `importFromFile` functions that handle Blob creation, download triggers, file reading, and JSON validation. | + +#### `createLiveQueryHook` + +```tsx +import { createLiveQueryHook, createDexieDatabase } from '@openzeppelin/ui-storage'; + +const useContracts = createLiveQueryHook(db, 'contracts'); + +function ContractList() { + const contracts = useContracts(); // undefined while loading, then T[] + return
    {contracts?.map((c) =>
  • {c.name}
  • )}
; +} +``` + +Pass an optional `query` function for filtered or sorted results: + +```tsx +const useRecentContracts = createLiveQueryHook( + db, + 'contracts', + (table) => table.orderBy('addedAt').reverse().limit(10).toArray(), +); +``` + +#### `createCrudHook` + +```tsx +import { createCrudHook } from '@openzeppelin/ui-storage'; + +const useContractCrud = createCrudHook(contractStore, { + onError: (title, err) => console.error(title, err), +}); + +function AddButton() { + const { save } = useContractCrud(); + return ; +} +``` + +#### `createJsonFileIO` + +```tsx +import { createJsonFileIO } from '@openzeppelin/ui-storage'; + +const { exportAsFile, importFromFile } = createJsonFileIO( + { exportJson: () => store.exportJson(), importJson: (json) => store.importJson(json) }, + { filePrefix: 'my-data', onError: (title, err) => toast.error(title) }, +); + +// exportAsFile() triggers a browser download of "my-data-2026-04-09.json" +// importFromFile(file) reads a File, validates JSON, and calls importJson() +``` + +## Next Steps + +- [Components](/tools/uikit/components): UI components that consume storage plugins +- [React Integration](/tools/uikit/react-integration): Wire up providers and wallet state diff --git a/content/tools/uikit/theming.mdx b/content/tools/uikit/theming.mdx new file mode 100644 index 00000000..238aac5d --- /dev/null +++ b/content/tools/uikit/theming.mdx @@ -0,0 +1,169 @@ +--- +title: Theming & Styling +--- + +OpenZeppelin UIKit uses **Tailwind CSS 4** with a centralized design token system. This page explains how styling works, how to customize the theme, and how to set up your application's CSS pipeline. + +## How Styling Works + +UIKit components use Tailwind CSS utility classes internally. The `@openzeppelin/ui-styles` package provides: + +- A **`global.css`** file with Tailwind v4 `@theme` definitions using OKLCH color tokens +- CSS custom properties for colors, radii, spacing, and animations +- A `@custom-variant dark` for dark mode support +- Chart and sidebar color tokens + +Components do **not** ship pre-compiled CSS. Your application runs Tailwind and produces the final stylesheet, which means: + +- You have full control over the CSS output +- Unused component styles are automatically tree-shaken +- You can extend or override any design token + +## Setting Up Styles + +### Automated Setup + +The dev CLI generates and maintains the Tailwind configuration: + +```bash +pnpm add -D @openzeppelin/ui-dev-cli +pnpm exec oz-ui-dev tailwind doctor --project "$PWD" +pnpm exec oz-ui-dev tailwind fix --project "$PWD" +``` + +This creates `oz-tailwind.generated.css` with `@source` directives that tell Tailwind where to find class names in OpenZeppelin packages. + +### Manual Setup + +Add these directives to your application's entry CSS file: + +```css +@layer base, components, utilities; + +@import 'tailwindcss' source(none); + +/* Your app sources */ +@source "./"; +@source "../"; + +/* OpenZeppelin UIKit sources */ +@source "../node_modules/@openzeppelin/ui-components"; +@source "../node_modules/@openzeppelin/ui-react"; +@source "../node_modules/@openzeppelin/ui-renderer"; +@source "../node_modules/@openzeppelin/ui-styles"; +@source "../node_modules/@openzeppelin/ui-utils"; + +/* OpenZeppelin theme tokens */ +@import '@openzeppelin/ui-styles/global.css'; +``` + + +If you also use Ecosystem Adapter packages, add their `@source` directives too. Adapters may ship UI components (wallet dialogs, network selectors) that need Tailwind scanning. The `oz-ui-dev tailwind fix` command handles this automatically. + + +## Design Tokens + +The theme is defined in `@openzeppelin/ui-styles/global.css` using Tailwind v4's `@theme` directive with [OKLCH](https://oklch.com/) color values. OKLCH provides perceptually uniform colors across light and dark modes. + +### Color Tokens + +The theme defines semantic color tokens rather than raw color values: + +| Token | Purpose | +| --- | --- | +| `--background` / `--foreground` | Page background and default text | +| `--card` / `--card-foreground` | Card surfaces | +| `--primary` / `--primary-foreground` | Primary actions (buttons, links) | +| `--secondary` / `--secondary-foreground` | Secondary actions | +| `--muted` / `--muted-foreground` | Subdued elements | +| `--accent` / `--accent-foreground` | Highlighted elements | +| `--destructive` / `--destructive-foreground` | Destructive actions (delete, error) | +| `--border` | Border colors | +| `--input` | Input field borders | +| `--ring` | Focus ring color | + +### Layout Tokens + +| Token | Purpose | +| --- | --- | +| `--radius` | Base border radius (used with `rounded-*` utilities) | +| `--sidebar-*` | Sidebar-specific colors and dimensions | +| `--chart-1` through `--chart-5` | Chart/data visualization colors | + +## Dark Mode + +UIKit uses `next-themes` for dark mode support, with a `@custom-variant dark` in the theme CSS. The dark variant activates automatically based on the user's system preference or an explicit toggle. + +All color tokens have dark mode equivalents defined in the theme. Components automatically adjust when the variant is active. + +### Integrating with next-themes + +If your app uses `next-themes`, dark mode works out of the box: + +```tsx +import { ThemeProvider } from 'next-themes'; + +function App({ children }) { + return ( + + {children} + + ); +} +``` + +## Component Variants + +Button and other interactive components use [`class-variance-authority`](https://cva.style/) for variant management: + +```tsx +import { Button } from '@openzeppelin/ui-components'; + +// Variant options: default, destructive, outline, secondary, ghost, link + + + +// Size options: default, sm, lg, icon + + +``` + +## Customizing the Theme + +Since the theme is CSS custom properties, you can override any token in your app's CSS: + +```css +@import '@openzeppelin/ui-styles/global.css'; + +:root { + --primary: oklch(0.65 0.2 250); + --radius: 0.75rem; +} +``` + +This approach works because UIKit components reference the CSS variables, not hard-coded values. Your overrides take precedence and propagate to all components. + +## Utility Functions + +`@openzeppelin/ui-components` exports styling utilities used internally and available for your custom components: + +| Utility | Source | Purpose | +| --- | --- | --- | +| `cn(...classes)` | `tailwind-merge` + `clsx` | Merge Tailwind classes with conflict resolution | +| `buttonVariants` | `class-variance-authority` | Apply button variant styles to custom elements | + +```tsx +import { cn } from '@openzeppelin/ui-utils'; + +function CustomCard({ className, ...props }) { + return ( +
+ ); +} +``` + +## Next Steps + +- [Getting Started](/tools/uikit/getting-started): Full setup walkthrough including Tailwind configuration +- [Components](/tools/uikit/components): Browse all available UI components +- [Architecture](/tools/uikit/architecture): Understand the package layer system diff --git a/public/uikit/address-book-widget.png b/public/uikit/address-book-widget.png new file mode 100644 index 00000000..d05c5bc6 Binary files /dev/null and b/public/uikit/address-book-widget.png differ diff --git a/public/uikit/contract-state-widget.png b/public/uikit/contract-state-widget.png new file mode 100644 index 00000000..5a6ba485 Binary files /dev/null and b/public/uikit/contract-state-widget.png differ diff --git a/public/uikit/example-app-overview.png b/public/uikit/example-app-overview.png new file mode 100644 index 00000000..aa9b8775 Binary files /dev/null and b/public/uikit/example-app-overview.png differ diff --git a/public/uikit/form-renderer.png b/public/uikit/form-renderer.png new file mode 100644 index 00000000..849055c1 Binary files /dev/null and b/public/uikit/form-renderer.png differ diff --git a/public/uikit/storage-account-alias-plugin.png b/public/uikit/storage-account-alias-plugin.png new file mode 100644 index 00000000..00b2aac4 Binary files /dev/null and b/public/uikit/storage-account-alias-plugin.png differ diff --git a/src/components/layout/docs-layout-client.tsx b/src/components/layout/docs-layout-client.tsx index 77f3aff1..e2d38b49 100644 --- a/src/components/layout/docs-layout-client.tsx +++ b/src/components/layout/docs-layout-client.tsx @@ -28,7 +28,7 @@ export function DocsLayoutClient({ children }: DocsLayoutClientProps) { // Read sessionStorage in an effect so SSR and the initial client render match; // reading it during render would cause a hydration mismatch on shared paths - // (e.g. /relayer/*) where the active ecosystem is only known on the client. + // (e.g. /relayer/*, /tools/*) where the active ecosystem is only known on the client. const [lastEcosystem, setLastEcosystem] = useState(null); // biome-ignore lint/correctness/useExhaustiveDependencies: re-read sessionStorage when pathname changes useEffect(() => { @@ -45,28 +45,83 @@ export function DocsLayoutClient({ children }: DocsLayoutClientProps) { const isSharedPath = pathname.startsWith("/monitor") || pathname.startsWith("/relayer") || - pathname.startsWith("/ui-builder"); + pathname.startsWith("/ui-builder") || + pathname.startsWith("/ecosystem-adapters") || + pathname.startsWith("/tools"); // Include shared paths in Stellar tab only if coming from Stellar context const stellarUrls = isSharedPath && lastEcosystem === "stellar" - ? new Set(["/stellar-contracts", "/monitor", "/relayer", "/ui-builder"]) + ? new Set([ + "/stellar-contracts", + "/monitor", + "/relayer", + "/ui-builder", + "/ecosystem-adapters", + "/tools", + ]) : new Set(["/stellar-contracts"]); // Include shared paths in Polkadot tab only if coming from Polkadot context const polkadotUrls = isSharedPath && lastEcosystem === "polkadot" - ? new Set(["/substrate-runtimes", "/monitor", "/relayer"]) + ? new Set([ + "/substrate-runtimes", + "/monitor", + "/relayer", + "/ecosystem-adapters", + "/tools", + ]) : new Set(["/substrate-runtimes"]); const arbitrumStylusUrls = isSharedPath && lastEcosystem === "contracts-stylus" - ? new Set(["/contracts-stylus", "/monitor", "/relayer"]) + ? new Set([ + "/contracts-stylus", + "/monitor", + "/relayer", + "/ecosystem-adapters", + "/tools", + ]) : new Set(["/contracts-stylus"]); + // Ecosystem Adapters is cross-cutting: attribute it to the last active tab + // (Stellar, Polkadot, Arbitrum Stylus, Midnight, Zama) or default to Ethereum. + const ethereumUrls = new Set([ + "/contracts", + "/community-contracts", + "/upgrades-plugins", + "/wizard", + "/relayer", + "/monitor", + "/ui-builder", + "/upgrades", + "/defender", + ]); + if ( + !isSharedPath || + !lastEcosystem || + !["stellar", "polkadot", "contracts-stylus", "midnight", "zama"].includes( + lastEcosystem, + ) + ) { + ethereumUrls.add("/ecosystem-adapters"); + ethereumUrls.add("/tools"); + } + + const midnightUrls = + isSharedPath && lastEcosystem === "midnight" + ? new Set(["/contracts-compact", "/ecosystem-adapters", "/tools"]) + : new Set(["/contracts-compact"]); + const zamaUrls = isSharedPath && lastEcosystem === "zama" - ? new Set(["/confidential-contracts", "/relayer"]) + ? new Set([ + "/confidential-contracts", + "/relayer", + "/ecosystem-adapters", + "/tools", + ]) : new Set(["/confidential-contracts"]); return [ @@ -74,18 +129,7 @@ export function DocsLayoutClient({ children }: DocsLayoutClientProps) { title: "Ethereum & EVM", url: "/contracts", icon: , - urls: new Set([ - "/contracts", - "/community-contracts", - "/upgrades-plugins", - "/wizard", - "/relayer", - "/monitor", - "/ui-builder", - "/upgrades", - "/defender", - "/tools", - ]), + urls: ethereumUrls, }, { title: "Arbitrum Stylus", @@ -113,6 +157,7 @@ export function DocsLayoutClient({ children }: DocsLayoutClientProps) { title: "Midnight", url: "/contracts-compact", icon: , + urls: midnightUrls, }, { title: "Polkadot", diff --git a/src/hooks/use-navigation-tree.ts b/src/hooks/use-navigation-tree.ts index 4c7c18d5..6a50ff91 100644 --- a/src/hooks/use-navigation-tree.ts +++ b/src/hooks/use-navigation-tree.ts @@ -20,8 +20,8 @@ export function useNavigationTree() { // Read sessionStorage after mount so SSR and initial client render match. // Reading during render would cause hydration mismatches on shared paths - // (/relayer, /monitor, /ui-builder) where the active ecosystem is - // only known on the client. + // (/relayer, /monitor, /ui-builder, /ecosystem-adapters, /tools) where the active + // ecosystem is only known on the client. const [lastEcosystem, setLastEcosystem] = useState(null); // Track ecosystem changes in sessionStorage @@ -36,6 +36,8 @@ export function useNavigationTree() { sessionStorage.setItem("lastEcosystem", "sui"); } else if (pathname.startsWith("/contracts-stylus")) { sessionStorage.setItem("lastEcosystem", "contracts-stylus"); + } else if (pathname.startsWith("/contracts-compact")) { + sessionStorage.setItem("lastEcosystem", "midnight"); } else if (pathname.startsWith("/confidential-contracts")) { sessionStorage.setItem("lastEcosystem", "zama"); } else if ( @@ -44,12 +46,11 @@ export function useNavigationTree() { pathname.startsWith("/upgrades-plugins") || pathname.startsWith("/wizard") || pathname.startsWith("/upgrades") || - pathname.startsWith("/defender") || - pathname.startsWith("/tools") + pathname.startsWith("/defender") ) { sessionStorage.setItem("lastEcosystem", "ethereum"); } - // Note: /ui-builder, /monitor, and /relayer paths are intentionally NOT set here + // Note: /ui-builder, /monitor, /relayer, /ecosystem-adapters, and /tools are intentionally NOT set here // They inherit the lastEcosystem from whichever tab the user was in before navigating setLastEcosystem(sessionStorage.getItem("lastEcosystem")); @@ -74,22 +75,23 @@ export function useNavigationTree() { return uniswapTree; } else if (pathname.startsWith("/substrate-runtimes")) { return polkadotTree; - } else if (pathname.startsWith("/tools")) { - return ethereumEvmTree; } - // For shared paths like /monitor and /relayer, use the lastEcosystem state - // (populated from sessionStorage after mount), defaulting to ethereumEvmTree. + // For shared paths, use the lastEcosystem state (from sessionStorage after mount) if ( pathname.startsWith("/monitor") || pathname.startsWith("/relayer") || - pathname.startsWith("/ui-builder") + pathname.startsWith("/ui-builder") || + pathname.startsWith("/ecosystem-adapters") || + pathname.startsWith("/tools") ) { switch (lastEcosystem) { case "stellar": return stellarTree; case "polkadot": return polkadotTree; + case "midnight": + return midnightTree; case "ethereum": return ethereumEvmTree; case "contracts-stylus": diff --git a/src/navigation/arbitrum-stylus.json b/src/navigation/arbitrum-stylus.json index 0a8cd80d..f82c1c52 100644 --- a/src/navigation/arbitrum-stylus.json +++ b/src/navigation/arbitrum-stylus.json @@ -73,7 +73,6 @@ "name": "Beacon Proxy", "url": "/contracts-stylus/beacon-proxy" }, - { "type": "page", "name": "UUPS Proxy", diff --git a/src/navigation/index.ts b/src/navigation/index.ts index eb8ac81b..541cf948 100644 --- a/src/navigation/index.ts +++ b/src/navigation/index.ts @@ -1,6 +1,7 @@ import arbitrumStylusData from "./arbitrum-stylus.json"; import ethereumEvmData from "./ethereum-evm.json"; import impactData from "./impact.json"; +import { mergeDeveloperLibraries } from "./merge-developer-libraries"; import midnightData from "./midnight.json"; import polkadotData from "./polkadot.json"; import starknetData from "./starknet"; @@ -10,16 +11,21 @@ import type { NavigationNode, NavigationTree } from "./types"; import uniswapData from "./uniswap.json"; import zamaData from "./zama.json"; -// Type-safe imports with proper typing -const ethereumEvm = ethereumEvmData as NavigationNode[]; -const arbitrumStylus = arbitrumStylusData as NavigationNode[]; -const stellar = stellarData as NavigationNode[]; -const midnight = midnightData as NavigationNode[]; +// Type-safe imports with proper typing; shared Ecosystem Adapters + UIKit live in +// `shared/developer-libraries.json` and are merged in `mergeDeveloperLibraries`. +const ethereumEvm = mergeDeveloperLibraries( + ethereumEvmData as NavigationNode[], +); +const arbitrumStylus = mergeDeveloperLibraries( + arbitrumStylusData as NavigationNode[], +); +const stellar = mergeDeveloperLibraries(stellarData as NavigationNode[]); +const midnight = mergeDeveloperLibraries(midnightData as NavigationNode[]); const starknet = starknetData as NavigationNode[]; const sui = suiData as NavigationNode[]; -const zama = zamaData as NavigationNode[]; +const polkadot = mergeDeveloperLibraries(polkadotData as NavigationNode[]); +const zama = mergeDeveloperLibraries(zamaData as NavigationNode[]); const uniswap = uniswapData as NavigationNode[]; -const polkadot = polkadotData as NavigationNode[]; const impact = impactData as NavigationNode[]; // Create separate navigation trees for each blockchain diff --git a/src/navigation/merge-developer-libraries.ts b/src/navigation/merge-developer-libraries.ts new file mode 100644 index 00000000..76f816d7 --- /dev/null +++ b/src/navigation/merge-developer-libraries.ts @@ -0,0 +1,27 @@ +import sharedDeveloperLibraries from "./shared/developer-libraries.json"; +import type { NavigationNode } from "./types"; + +const OPEN_SOURCE_TOOLS = "Open Source Tools"; + +const developerLibrariesBlock = sharedDeveloperLibraries as NavigationNode[]; + +/** + * Inserts the shared "Developer Libraries" block (Ecosystem Adapters + UIKit) + * before the "Open Source Tools" section when present; otherwise appends + * (e.g. Midnight has no Open Source Tools in-tree). + */ +export function mergeDeveloperLibraries( + nodes: readonly NavigationNode[], +): NavigationNode[] { + const openSourceIdx = nodes.findIndex( + (n) => n.type === "separator" && n.name === OPEN_SOURCE_TOOLS, + ); + if (openSourceIdx !== -1) { + return [ + ...nodes.slice(0, openSourceIdx), + ...developerLibrariesBlock, + ...nodes.slice(openSourceIdx), + ]; + } + return [...nodes, ...developerLibrariesBlock]; +} diff --git a/src/navigation/shared/developer-libraries.json b/src/navigation/shared/developer-libraries.json new file mode 100644 index 00000000..4528e322 --- /dev/null +++ b/src/navigation/shared/developer-libraries.json @@ -0,0 +1,78 @@ +[ + { + "type": "separator", + "name": "Developer Libraries" + }, + { + "type": "folder", + "name": "Ecosystem Adapters", + "index": { + "type": "page", + "name": "Overview", + "url": "/ecosystem-adapters" + }, + "children": [ + { + "type": "page", + "name": "Architecture", + "url": "/ecosystem-adapters/architecture" + }, + { + "type": "page", + "name": "Getting Started", + "url": "/ecosystem-adapters/getting-started" + }, + { + "type": "page", + "name": "Supported Ecosystems", + "url": "/ecosystem-adapters/supported-ecosystems" + }, + { + "type": "page", + "name": "Building an Adapter", + "url": "/ecosystem-adapters/building-an-adapter" + } + ] + }, + { + "type": "folder", + "name": "UIKit", + "index": { + "type": "page", + "name": "Overview", + "url": "/tools/uikit" + }, + "children": [ + { + "type": "page", + "name": "Getting Started", + "url": "/tools/uikit/getting-started" + }, + { + "type": "page", + "name": "Architecture", + "url": "/tools/uikit/architecture" + }, + { + "type": "page", + "name": "Components", + "url": "/tools/uikit/components" + }, + { + "type": "page", + "name": "React Integration", + "url": "/tools/uikit/react-integration" + }, + { + "type": "page", + "name": "Theming & Styling", + "url": "/tools/uikit/theming" + }, + { + "type": "page", + "name": "Storage", + "url": "/tools/uikit/storage" + } + ] + } +]