@openzeppelin/ui-builder-adapter-midnight
Version:
Midnight Adapter for UI Builder
315 lines (275 loc) • 10.4 kB
text/typescript
import {
testMidnightRpcConnection,
validateMidnightRpcEndpoint,
} from 'packages/adapter-midnight/src/configuration';
import type {
AvailableUiKit,
Connector,
ContractAdapter,
ContractFunction,
ContractSchema,
EcosystemReactUiProviderProps,
EcosystemSpecificReactHooks,
EcosystemWalletComponents,
ExecutionConfig,
ExecutionMethodDetail,
FieldType,
FormFieldType,
FunctionParameter,
MidnightNetworkConfig,
RelayerDetails,
RelayerDetailsRich,
UiKitConfiguration,
UserRpcProviderConfig,
} from '@openzeppelin/ui-builder-types';
import { isMidnightNetworkConfig } from '@openzeppelin/ui-builder-types';
import { logger } from '@openzeppelin/ui-builder-utils';
import type { MidnightContractArtifacts } from './types/artifacts';
import { CustomAccountDisplay } from './wallet/components/account/AccountDisplay';
import { ConnectButton } from './wallet/components/connect/ConnectButton';
import { MidnightWalletProvider } from './wallet/components/MidnightWalletProvider';
import * as connection from './wallet/connection';
import { midnightFacadeHooks } from './wallet/hooks/facade-hooks';
import { parseMidnightContractInterface, validateAndConvertMidnightArtifacts } from './utils';
/**
* Midnight-specific adapter.
*
* Implements the full ContractAdapter interface to integrate with the builder application.
* Wallet-related functionalities are implemented, while contract-specific methods
* are placeholders to be built out in later phases.
*/
export class MidnightAdapter implements ContractAdapter {
readonly networkConfig: MidnightNetworkConfig;
readonly initialAppServiceKitName: UiKitConfiguration['kitName'];
private artifacts: MidnightContractArtifacts | null = null;
constructor(networkConfig: MidnightNetworkConfig) {
if (!isMidnightNetworkConfig(networkConfig)) {
throw new Error('MidnightAdapter requires a valid Midnight network configuration.');
}
this.networkConfig = networkConfig;
this.initialAppServiceKitName = 'custom';
logger.info(
'MidnightAdapter',
`Adapter initialized for network: ${networkConfig.name} (ID: ${networkConfig.id})`
);
}
public getEcosystemReactUiContextProvider(): React.FC<EcosystemReactUiProviderProps> {
return MidnightWalletProvider;
}
public getEcosystemReactHooks(): EcosystemSpecificReactHooks {
return midnightFacadeHooks;
}
public getEcosystemWalletComponents(): EcosystemWalletComponents {
return {
ConnectButton,
AccountDisplay: CustomAccountDisplay,
};
}
public supportsWalletConnection(): boolean {
return connection.supportsMidnightWalletConnection();
}
public async getAvailableConnectors(): Promise<Connector[]> {
return connection.getMidnightAvailableConnectors();
}
public connectWallet(
_connectorId: string
): Promise<{ connected: boolean; address?: string; error?: string }> {
logger.warn(
'MidnightAdapter',
'The `connectWallet` method is not supported. Use the `ConnectButton` component from `getEcosystemWalletComponents()` instead.'
);
return Promise.resolve({ connected: false, error: 'Method not supported.' });
}
public disconnectWallet(): Promise<{ disconnected: boolean; error?: string }> {
return connection.disconnectMidnightWallet();
}
public getWalletConnectionStatus(): { isConnected: boolean; address?: string; chainId?: string } {
// This method is required by the ContractAdapter interface.
// In our React-based UI, the connection status is managed reactively by the
// MidnightWalletProvider. This function provides a non-reactive, one-time
// status check, which is not the source of truth for the UI components.
return {
isConnected: false,
address: undefined,
chainId: this.networkConfig.id,
};
}
public getContractDefinitionInputs(): FormFieldType[] {
return [
{
id: 'contractAddress',
name: 'contractAddress',
label: 'Contract Address',
type: 'blockchain-address',
validation: { required: true },
placeholder: 'ct1q8ej4px...',
helperText: 'Enter the deployed Midnight contract address (Bech32m format).',
},
{
id: 'privateStateId',
name: 'privateStateId',
label: 'Private State ID',
type: 'text',
validation: { required: true },
placeholder: 'my-unique-state-id',
helperText:
'A unique identifier for your private state instance. This ID is used to manage your personal encrypted data.',
},
{
id: 'contractSchema',
name: 'contractSchema',
label: 'Contract Interface (.d.ts)',
type: 'code-editor',
validation: { required: true },
placeholder:
'export interface MyContract {\n myMethod(param: string): Promise<void>;\n // ... other methods\n}',
helperText:
"Paste the TypeScript interface definition from your contract.d.ts file. This defines the contract's available methods.",
codeEditorProps: {
language: 'typescript',
placeholder: 'Paste your contract interface here...',
maxHeight: '400px',
},
},
{
id: 'contractModule',
name: 'contractModule',
label: 'Compiled Contract Module (.cjs)',
type: 'textarea',
validation: { required: true },
placeholder: 'module.exports = { /* compiled contract code */ };',
helperText:
"Paste the compiled contract code from your contract.cjs file. This contains the contract's implementation.",
},
{
id: 'witnessCode',
name: 'witnessCode',
label: 'Witness Functions (Optional)',
type: 'textarea',
validation: { required: false },
placeholder:
'// Define witness functions for zero-knowledge proofs\nexport const witnesses = {\n myWitness: (ctx) => {\n return [ctx.privateState.myField, []];\n }\n};',
helperText:
'Optional: Define witness functions that generate zero-knowledge proofs for your contract interactions. These functions determine what private data is used in proofs.',
},
];
}
public async loadContract(source: string | Record<string, unknown>): Promise<ContractSchema> {
// Convert and validate the input
const artifacts = validateAndConvertMidnightArtifacts(source);
this.artifacts = artifacts;
logger.info('MidnightAdapter', 'Contract artifacts stored.', this.artifacts);
const { functions, events } = parseMidnightContractInterface(artifacts.contractSchema);
const schema: ContractSchema = {
name: 'MyMidnightContract', // TODO: Extract from artifacts if possible
ecosystem: 'midnight',
address: artifacts.contractAddress,
functions,
events,
};
return schema;
}
public getWritableFunctions(contractSchema: ContractSchema): ContractFunction[] {
return contractSchema.functions.filter((fn) => fn.modifiesState);
}
public mapParameterTypeToFieldType(_parameterType: string): FieldType {
return 'text';
}
public getCompatibleFieldTypes(_parameterType: string): FieldType[] {
return ['text'];
}
public generateDefaultField(parameter: FunctionParameter): FormFieldType {
return {
id: parameter.name,
name: parameter.name,
label: parameter.name,
type: this.mapParameterTypeToFieldType(parameter.type),
validation: {},
};
}
public formatTransactionData(
_contractSchema: ContractSchema,
_functionId: string,
_submittedInputs: Record<string, unknown>,
_fields: FormFieldType[]
): unknown {
throw new Error('formatTransactionData not implemented for MidnightAdapter.');
}
public async signAndBroadcast(
_transactionData: unknown,
_executionConfig?: ExecutionConfig
): Promise<{ txHash: string }> {
throw new Error('signAndBroadcast not implemented for MidnightAdapter.');
}
public isViewFunction(functionDetails: ContractFunction): boolean {
return !functionDetails.modifiesState;
}
public async queryViewFunction(
_contractAddress: string,
_functionId: string,
_params: unknown[],
_contractSchema?: ContractSchema
): Promise<unknown> {
throw new Error('queryViewFunction not implemented for MidnightAdapter.');
}
public formatFunctionResult(decodedValue: unknown): string {
return JSON.stringify(decodedValue, null, 2);
}
public async getSupportedExecutionMethods(): Promise<ExecutionMethodDetail[]> {
return []; // Placeholder
}
public async validateExecutionConfig(_config: ExecutionConfig): Promise<true | string> {
return true; // No config to validate yet
}
public getExplorerUrl(_address: string): string | null {
return null; // No official explorer yet
}
public getExplorerTxUrl(_txHash: string): string | null {
return null; // No official explorer yet
}
public isValidAddress(_address: string): boolean {
// Placeholder - add real Bech32m validation later
return true;
}
async getAvailableUiKits(): Promise<AvailableUiKit[]> {
return [
{
id: 'custom',
name: 'OpenZeppelin Custom',
configFields: [],
},
];
}
public async getRelayers(_serviceUrl: string, _accessToken: string): Promise<RelayerDetails[]> {
logger.warn('MidnightAdapter', 'getRelayers is not implemented for the Midnight adapter yet.');
return Promise.resolve([]);
}
public async getRelayer(
_serviceUrl: string,
_accessToken: string,
_relayerId: string
): Promise<RelayerDetailsRich> {
logger.warn('MidnightAdapter', 'getRelayer is not implemented for the Midnight adapter yet.');
return Promise.resolve({} as RelayerDetailsRich);
}
/**
* @inheritdoc
*/
public async validateRpcEndpoint(rpcConfig: UserRpcProviderConfig): Promise<boolean> {
// TODO: Implement Midnight-specific RPC validation when needed
return validateMidnightRpcEndpoint(rpcConfig);
}
/**
* @inheritdoc
*/
public async testRpcConnection(rpcConfig: UserRpcProviderConfig): Promise<{
success: boolean;
latency?: number;
error?: string;
}> {
// TODO: Implement Midnight-specific RPC validation when needed
return testMidnightRpcConnection(rpcConfig);
}
}
// Also export as default
export default MidnightAdapter;