Building New Adapters
This document provides a comprehensive overview of the Contracts UI Builder’s architecture, with a focus on the patterns and requirements for creating new ecosystem adapters. It’s intended for developers tasked with extending the platform to support new blockchains.
Core Architecture: The Adapter Pattern
The entire system is built around a domain-driven adapter pattern. The goal is to keep the main application (packages/builder
) and the UI rendering library (packages/renderer
) completely chain-agnostic. They have no direct knowledge of how to interact with a specific blockchain like EVM or Solana.
This decoupling is achieved through the ContractAdapter
interface, defined in packages/types/src/adapters/base.ts
. Each supported blockchain ecosystem must have its own package (e.g., packages/adapter-evm
) that exports a class implementing this interface.
Key Principles:
- Separation of Concerns: Chain-specific logic is entirely encapsulated within its adapter package.
- Shared Type System:
packages/types
serves as the single source of truth for all shared data structures, ensuring type safety across the entire monorepo. - Network-Aware Adapters: An adapter instance is not generic; it is instantiated with a specific
NetworkConfig
object. This makes the instance aware of its target network (e.g., Ethereum Mainnet vs. Sepolia Testnet), including its RPC endpoints, explorer URLs, and chain identifiers. The adapter stores thisnetworkConfig
internally and uses it for all network-dependent operations.
The ContractAdapter
Interface: Your Implementation Contract
The ContractAdapter
interface is the most critical file for an adapter developer. Your new adapter class must implement every method defined here. The methods can be grouped by functionality:
Contract Definition & Schema Handling
These methods handle loading and interpreting a contract’s interface (like an ABI or IDL).
getContractDefinitionInputs()
: Must return aFormFieldType[]
array that defines the inputs your adapter needs to locate a contract (e.g., an address field, an optional ABI/IDL textarea).loadContract(artifacts)
: Takes the form values fromgetContractDefinitionInputs
and returns aContractSchema
. This is your main entry point for fetching and parsing the contract’s interface. You’ll handle logic here like: "If a manual ABI is provided, parse it. If not, use the address to fetch it from a block explorer."loadContractWithMetadata(artifacts)
: An enhanced version ofloadContract
that also returns metadata about the source (fetched
ormanual
), proxy information (ProxyInfo
), and definition details (ContractDefinitionMetadata
). This is used for advanced features like ABI comparison and proxy detection.validateContractDefinition(definition)
&hashContractDefinition(definition)
: (Optional) Implement these to support ABI/IDL validation and version comparison features. The EVM adapter provides a reference implementation inabi/comparison.ts
.
Form Generation & Type Mapping
These methods bridge the gap between blockchain-specific types and the generic form fields used by the UI.
mapParameterTypeToFieldType(parameterType)
: A simple mapping from a blockchain type string (e.g., ’uint256') to a default
FieldType(e.g., ’number'
).getCompatibleFieldTypes(parameterType)
: Returns an array of allFieldType
s that could be used for a given blockchain type. For example, a ’bool'might be compatible with ’checkbox'
, ’select', and ’radio'
.generateDefaultField(parameter)
: Creates a completeFormFieldType
object for a givenFunctionParameter
, including a default label, placeholder, and validation rules.
Transaction Execution
This is the core of state-changing interactions. The system uses an Execution Strategy Pattern to support multiple ways of sending a transaction (e.g., EOA, Relayer).
getSupportedExecutionMethods()
: Returns a list of supported methods (ExecutionMethodDetail
) for your ecosystem (e.g., EOA, Multisig).validateExecutionConfig(config)
: Validates a user’sExecutionConfig
for a specific method. For example, for an EOA with a specific address, it checks that the connected wallet matches.formatTransactionData(...)
: This crucial method takes the rawsubmittedInputs
from the UI form and transforms them into the data payload your blockchain’s client library expects. The EVM adapter’s implementation intransaction/formatter.ts
shows how to parse strings intoBigInt
, checksum addresses, and structure complex tuples.signAndBroadcast(...)
: The main function to execute a transaction. It receives the data fromformatTransactionData
and anExecutionConfig
. Your implementation should use a factory pattern to select the correct Execution Strategy (e.g.,EoaExecutionStrategy
,RelayerExecutionStrategy
) based onexecutionConfig.method
.waitForTransactionConfirmation?(txHash)
: (Optional) Implement this to allow the UI to wait for a transaction to be finalized.
Read-Only Queries
These methods handle fetching data from the blockchain without creating a transaction.
isViewFunction(functionDetails)
: A simple utility to check if a function is read-only.queryViewFunction(...)
: Fetches data from a read-only function. It should use thenetworkConfig
to connect to the correct RPC endpoint. The EVM adapter implementation inquery/handler.ts
provides a robust example of creating a public client and handling various scenarios (e.g., using the wallet’s provider vs. a fallback provider).formatFunctionResult(...)
: Formats the raw data returned fromqueryViewFunction
into a human-readable string for display in the UI. This should handleBigInt
, arrays, and complex objects.
Wallet Interaction & UI Facilitation
These methods allow the adapter to provide a rich, ecosystem-specific wallet experience. The EVM adapter has a very sophisticated implementation for this, designed to be extensible with UI kits like RainbowKit. For simpler ecosystems, your implementation can be much more direct.
supportsWalletConnection()
: Returntrue
if your ecosystem has wallets.getAvailableConnectors()
: Return a list of wallet connectors the user can choose from.connectWallet(connectorId)
: Logic to initiate a connection with the chosen wallet.disconnectWallet()
: Logic to disconnect.getWalletConnectionStatus()
: Return the current status (isConnected
,address
,chainId
).onWalletConnectionChange?(callback)
: (Optional) An event listener for status changes.
Advanced UI Facilitation (The "UI Kit" Pattern):
This optional but powerful pattern allows an adapter to provide React components and hooks that integrate with its ecosystem’s native wallet libraries (e.g., wagmi
for EVM).
configureUiKit(config, options)
: The entry point for the application to tell the adapter which UI kit to use (e.g., ’rainbowkit', ’custom'
) and provide configuration.getEcosystemReactUiContextProvider()
: Must return a stable React Component that provides the necessary context for your wallet library (e.g., for EVM, this is<WagmiProvider>
and<QueryClientProvider>
). SeeEvmWalletUiRoot.tsx
for a reference implementation that avoids UI flicker.getEcosystemReactHooks()
: Must return an object of facade hooks (e.g.,useAccount
,useSwitchChain
). These hooks should wrap the native library’s hooks. Crucially, your implementation must map the return values to a conventional format to ensure UI components remain chain-agnostic (e.g., map the underlying library’sisLoading
property toisPending
).getEcosystemWalletComponents()
: Must return an object of standardized UI components (ConnectButton
,AccountDisplay
, etc.) for the configured UI kit.
For a deep dive into this advanced pattern, study the EVM adapter’s wallet module (packages/adapter-evm/src/wallet/
) and the ADDING_NEW_UI_KITS.md
guide.
Standardized Adapter Package Structure
To ensure consistency, new adapter packages (packages/adapter-<your-chain>
) should follow this directory structure:
adapter-<your-chain>/
└── src/
├── adapter.ts // Main Adapter class implementing ContractAdapter
├── networks/ // Network configurations for your chain
│ ├── mainnet.ts
│ ├── testnet.ts
│ └── index.ts
├── [chain-def]/ // e.g., idl/ (Solana), abi/ (EVM) - For loading contracts
│ ├── loader.ts
│ └── transformer.ts
├── mapping/ // Type mapping and default field generation
│ ├── type-mapper.ts
│ └── field-generator.ts
├── transform/ // Data serialization/deserialization
│ ├── input-parser.ts
│ └── output-formatter.ts
├── transaction/ // Transaction execution (with strategy pattern)
│ ├── execution-strategy.ts
│ ├── eoa.ts // Example strategy
│ └── ...
├── query/ // View function querying
│ ├── handler.ts
│ └── view-checker.ts
├── wallet/ // Wallet connection & UI facilitation logic
├── configuration/ // Explorer URLs, execution method validation
├── types.ts // Internal types specific to this adapter
├── utils/ // Adapter-specific utilities
└── index.ts // Main package export
Configuration Management
A robust adapter must handle runtime configuration overrides. The project provides utility services for this: appConfigService
, userRpcConfigService
, and userExplorerConfigService
.
Your adapter logic should follow a clear priority order when resolving settings like RPC URLs or explorer API keys:
- User-Provided Config: Check the
user...ConfigService
first. This is for settings configured by the end-user in the UI and stored inlocalStorage
. - Application Config: If no user config is found, check
appConfigService
. This is for settings provided by the application deployer inapp.config.json
or environment variables. - Default Config: If neither of the above is present, fall back to the default value in the
NetworkConfig
object your adapter was instantiated with.
The functions resolveRpcUrl
and resolveExplorerConfig
in the EVM adapter provide excellent reference implementations of this layered pattern.
Step-by-Step Guide to Creating a New Adapter
- Create Package: In the
packages/
directory, createadapter-<chain-name>
. - Define
package.json
: Add dependencies on@openzeppelin/contracts-ui-builder-types
and any chain-specific SDKs. Set up standard build scripts (copy from an existing adapter). - Implement
NetworkConfig
: Insrc/networks/
, define your chain’s specific...NetworkConfig
interface (extendingBaseNetworkConfig
) and create configuration objects for its mainnets and testnets. Export them and a combined list (e.g.,export const myChainNetworks = [...]
). - Implement
ContractAdapter
: Createsrc/adapter.ts
and begin implementing theContractAdapter
interface, delegating logic to the modular subdirectories as described above. Start with the core methods and tackle the optional UI/wallet methods last. - Register in Builder: Open
packages/builder/src/core/ecosystemManager.ts
and:- Add your ecosystem to the
Ecosystem
type. - Add an entry to the
ecosystemRegistry
, providing thenetworksExportName
(e.g., ’myChainNetworks') and the
AdapterClass`. - Add a case to
loadAdapterPackageModule
to enable dynamic importing of your new package.
- Add your ecosystem to the
- Build and Test: Build your new package and the main builder app. Add unit and integration tests to ensure your adapter functions correctly within the larger system.