@openzeppelin/contracts-ui-builder-adapter-midnight
Version:
Midnight Adapter for Contracts UI Builder
697 lines (673 loc) • 23.4 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/configuration/execution.ts
import { logger as logger2 } from "@openzeppelin/contracts-ui-builder-utils";
// src/types/artifacts.ts
function isMidnightContractArtifacts(obj) {
const record = obj;
return typeof obj === "object" && obj !== null && typeof record.contractAddress === "string" && typeof record.privateStateId === "string" && typeof record.contractSchema === "string";
}
// src/utils/artifacts.ts
function validateAndConvertMidnightArtifacts(source) {
if (typeof source === "string") {
throw new Error(
"Midnight adapter requires contract artifacts object, not just an address string."
);
}
if (!isMidnightContractArtifacts(source)) {
throw new Error(
"Invalid contract artifacts provided. Expected an object with contractAddress, privateStateId, and contractSchema properties."
);
}
return source;
}
// src/utils/schema-parser.ts
function parseMidnightContractInterface(interfaceContent) {
const circuits = extractCircuits(interfaceContent);
const queries = extractQueries(interfaceContent);
const functions = [...Object.values(circuits), ...Object.values(queries)];
const events = [];
return { functions, events };
}
function extractCircuits(content) {
const circuits = {};
const circuitsMatch = content.match(/export\s+type\s+Circuits\s*<[^>]*>\s*=\s*{([^}]*)}/s);
if (circuitsMatch) {
const circuitsContent = circuitsMatch[1];
const methodRegex = /(\w+)\s*\(\s*context\s*:[^,)]+(?:,\s*([^)]+))?\)/g;
let match;
while ((match = methodRegex.exec(circuitsContent)) !== null) {
const name = match[1];
const paramsText = match[2] || "";
circuits[name] = {
id: name,
// Simplified ID for now
name,
displayName: name.charAt(0).toUpperCase() + name.slice(1),
inputs: parseParameters(paramsText),
outputs: [],
// Assuming no direct outputs from circuits for now
modifiesState: true,
type: "function"
};
}
}
return circuits;
}
function extractQueries(content) {
const queries = {};
const ledgerMatch = content.match(/export\s+type\s+Ledger\s*=\s*{([^}]*)}/s);
if (ledgerMatch) {
const ledgerContent = ledgerMatch[1];
const propertyRegex = /readonly\s+(\w+)\s*:\s*([^;]*);/g;
let match;
while ((match = propertyRegex.exec(ledgerContent)) !== null) {
const name = match[1];
const typeStr = match[2].trim();
queries[name] = {
id: name,
name,
displayName: name.charAt(0).toUpperCase() + name.slice(1),
inputs: [],
outputs: [{ name: "value", type: typeStr }],
modifiesState: false,
type: "function",
stateMutability: "view"
};
}
}
return queries;
}
function parseParameters(paramsText) {
if (!paramsText.trim()) return [];
return paramsText.split(",").map((param) => {
const [name, type] = param.split(":").map((s) => s.trim());
return { name, type };
});
}
// src/utils/validator.ts
import { logger } from "@openzeppelin/contracts-ui-builder-utils";
// src/configuration/explorer.ts
import { logger as logger3 } from "@openzeppelin/contracts-ui-builder-utils";
// src/configuration/rpc.ts
import { logger as logger4 } from "@openzeppelin/contracts-ui-builder-utils";
function validateMidnightRpcEndpoint(_rpcConfig) {
logger4.info("validateMidnightRpcEndpoint", "Midnight RPC validation not yet implemented");
return true;
}
async function testMidnightRpcConnection(_rpcConfig) {
logger4.info("testMidnightRpcConnection", "TODO: Implement RPC connection testing");
return { success: true };
}
// src/adapter.ts
import { isMidnightNetworkConfig } from "@openzeppelin/contracts-ui-builder-types";
import { logger as logger6 } from "@openzeppelin/contracts-ui-builder-utils";
// src/wallet/components/account/AccountDisplay.tsx
import { LogOut } from "lucide-react";
import { Button } from "@openzeppelin/contracts-ui-builder-ui";
import { cn, truncateMiddle } from "@openzeppelin/contracts-ui-builder-utils";
// src/wallet/hooks/useMidnightWallet.ts
import { useContext } from "react";
// src/wallet/context/MidnightWalletContext.tsx
import { createContext } from "react";
var MidnightWalletContext = createContext(
void 0
);
// src/wallet/hooks/useMidnightWallet.ts
var useMidnightWallet = () => {
const context = useContext(MidnightWalletContext);
if (context === void 0) {
throw new Error("useMidnightWallet must be used within a MidnightWalletProvider");
}
return context;
};
// src/wallet/hooks/facade-hooks.ts
var useConnect = () => {
const { connect: connect2, isConnecting, error } = useMidnightWallet();
const connectors = [{ id: "mnLace", name: "Lace (Midnight)" }];
return {
connect: connect2,
connectors,
isConnecting,
error,
pendingConnector: void 0
// This adapter doesn't have a concept of a pending connector.
};
};
var useDisconnect = () => {
const { disconnect: disconnect2 } = useMidnightWallet();
return {
disconnect: disconnect2,
isDisconnecting: false,
// This adapter doesn't track disconnecting state.
error: null
};
};
var useAccount = () => {
const { address, isConnected, isConnecting } = useMidnightWallet();
return {
address,
isConnected,
isConnecting,
isDisconnected: !isConnected && !isConnecting,
isReconnecting: false,
// This adapter doesn't have a concept of reconnecting.
status: isConnected ? "connected" : isConnecting ? "connecting" : "disconnected"
};
};
var midnightFacadeHooks = {
useAccount,
useConnect,
useDisconnect
// Other hooks like useBalance, useSwitchChain, etc., can be added here
// in the future. For now, we only need the core connection hooks.
};
// src/wallet/utils/SafeMidnightComponent.tsx
import { useContext as useContext2 } from "react";
import { Fragment, jsx } from "react/jsx-runtime";
var SafeMidnightComponent = ({
children,
fallback = null
}) => {
const context = useContext2(MidnightWalletContext);
if (!context) {
return /* @__PURE__ */ jsx(Fragment, { children: fallback });
}
return /* @__PURE__ */ jsx(Fragment, { children });
};
// src/wallet/components/account/AccountDisplay.tsx
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
var CustomAccountDisplay = ({ className }) => {
return /* @__PURE__ */ jsx2(SafeMidnightComponent, { fallback: null, children: /* @__PURE__ */ jsx2(AccountDisplayContent, { className }) });
};
var AccountDisplayContent = ({ className }) => {
const { address, isConnected } = useAccount();
const { disconnect: disconnect2 } = useDisconnect();
if (!isConnected || !address || !disconnect2) {
return null;
}
return /* @__PURE__ */ jsxs("div", { className: cn("flex items-center gap-2", className), children: [
/* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
/* @__PURE__ */ jsx2("span", { className: "text-xs font-medium", children: truncateMiddle(address, 4, 4) }),
/* @__PURE__ */ jsx2("span", { className: "text-[9px] text-muted-foreground -mt-0.5", children: "Midnight Network" })
] }),
/* @__PURE__ */ jsx2(
Button,
{
onClick: () => disconnect2(),
variant: "ghost",
size: "icon",
className: "size-6 p-0",
title: "Disconnect wallet",
children: /* @__PURE__ */ jsx2(LogOut, { className: "size-3.5" })
}
)
] });
};
// src/wallet/components/connect/ConnectButton.tsx
import { Loader2, Wallet } from "lucide-react";
import { Button as Button2 } from "@openzeppelin/contracts-ui-builder-ui";
import { cn as cn2 } from "@openzeppelin/contracts-ui-builder-utils";
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
var ConnectButton = ({
className,
hideWhenConnected = true
}) => {
const unavailableButton = /* @__PURE__ */ jsx3("div", { className: cn2("flex items-center", className), children: /* @__PURE__ */ jsxs2(Button2, { disabled: true, variant: "outline", size: "sm", className: "h-8 px-2 text-xs", children: [
/* @__PURE__ */ jsx3(Wallet, { className: "size-3.5 mr-1" }),
"Wallet Unavailable"
] }) });
return /* @__PURE__ */ jsx3(SafeMidnightComponent, { fallback: unavailableButton, children: /* @__PURE__ */ jsx3(ConnectButtonContent, { className, hideWhenConnected }) });
};
var ConnectButtonContent = ({ className, hideWhenConnected }) => {
const { isConnected } = useAccount();
const { connect: connect2, isConnecting, connectors, error: connectError } = useConnect();
const handleConnectClick = () => {
if (connect2 && !isConnected) {
connect2();
}
};
if (isConnected && hideWhenConnected) {
return null;
}
const showButtonLoading = isConnecting;
const hasWallet = connectors.length > 0;
let buttonText = "Connect Wallet";
if (!hasWallet) {
buttonText = "No Wallet Found";
} else if (showButtonLoading) {
buttonText = "Connecting...";
} else if (connectError) {
buttonText = "Connect Wallet";
}
return /* @__PURE__ */ jsxs2("div", { className: cn2("flex flex-col items-start gap-1", className), children: [
/* @__PURE__ */ jsx3("div", { className: "flex items-center", children: /* @__PURE__ */ jsxs2(
Button2,
{
onClick: handleConnectClick,
disabled: showButtonLoading || isConnected || !hasWallet,
variant: "outline",
size: "sm",
className: "h-8 px-2 text-xs",
children: [
showButtonLoading ? /* @__PURE__ */ jsx3(Loader2, { className: "size-3.5 animate-spin mr-1" }) : /* @__PURE__ */ jsx3(Wallet, { className: "size-3.5 mr-1" }),
buttonText
]
}
) }),
connectError && !isConnecting && /* @__PURE__ */ jsx3("p", { className: "text-xs text-red-500 px-2", children: connectError.message || "Error connecting wallet" })
] });
};
// src/wallet/components/MidnightWalletProvider.tsx
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { logger as logger5 } from "@openzeppelin/contracts-ui-builder-utils";
// src/wallet/midnight-implementation.ts
var enabledApi = null;
var isEnabled = () => {
if (typeof window === "undefined" || !window.midnight?.mnLace) {
return Promise.resolve(false);
}
return window.midnight.mnLace.isEnabled();
};
var connect = async () => {
if (typeof window === "undefined" || !window.midnight?.mnLace) {
return Promise.reject(new Error("Lace wallet not found."));
}
const api = await window.midnight.mnLace.enable();
enabledApi = api;
return api;
};
var disconnect = () => {
enabledApi = null;
};
// src/wallet/components/MidnightWalletProvider.tsx
import { jsx as jsx4 } from "react/jsx-runtime";
var MidnightWalletProvider = ({ children }) => {
const [api, setApi] = useState(null);
const [error, setError] = useState(null);
const [address, setAddress] = useState(void 0);
const [isConnecting, setIsConnecting] = useState(false);
const [isInitializing, setIsInitializing] = useState(true);
const pollIntervalRef = useRef(null);
const isConnected = !!address && !!api;
const cleanupTimer = useCallback(() => {
if (pollIntervalRef.current) {
clearInterval(pollIntervalRef.current);
pollIntervalRef.current = null;
}
}, []);
useEffect(() => {
const attemptAutoConnect = async () => {
if (localStorage.getItem("midnight-hasExplicitlyDisconnected") === "true") {
setIsInitializing(false);
return;
}
try {
const alreadyEnabled = await isEnabled();
if (alreadyEnabled) {
const preflightApi = await connect();
const state = await preflightApi.state();
setApi(preflightApi);
setAddress(state.address);
}
} catch (err) {
logger5.warn("MidnightWalletProvider", "Auto-reconnect failed:", err);
} finally {
setIsInitializing(false);
}
};
attemptAutoConnect();
}, []);
const handleConnect = useCallback(async () => {
if (isConnecting || isInitializing) return;
localStorage.removeItem("midnight-hasExplicitlyDisconnected");
setIsConnecting(true);
setError(null);
try {
const preflightApi = await connect();
pollIntervalRef.current = setInterval(async () => {
try {
const state = await preflightApi.state();
cleanupTimer();
setApi(preflightApi);
setAddress(state.address);
setIsConnecting(false);
} catch {
}
}, 2e3);
setTimeout(() => {
if (pollIntervalRef.current) {
cleanupTimer();
setError(new Error("Connection timed out. Please try again."));
setIsConnecting(false);
}
}, 9e4);
} catch (initialError) {
setError(
initialError instanceof Error ? initialError : new Error("Failed to initiate connection.")
);
setIsConnecting(false);
}
}, [isConnecting, isInitializing, cleanupTimer]);
const handleDisconnect = useCallback(async () => {
disconnect();
setApi(null);
setAddress(void 0);
setError(null);
cleanupTimer();
localStorage.setItem("midnight-hasExplicitlyDisconnected", "true");
}, [cleanupTimer]);
useEffect(() => {
return cleanupTimer;
}, [cleanupTimer]);
useEffect(() => {
if (api && typeof api.onAccountChange === "function") {
const handleAccountChange = (addresses) => {
setAddress(addresses[0]);
};
api.onAccountChange(handleAccountChange);
return () => {
if (typeof api.offAccountChange === "function") {
api.offAccountChange(handleAccountChange);
}
};
}
}, [api]);
const contextValue = useMemo(
() => ({
isConnected,
isConnecting: isConnecting || isInitializing,
isConnectPending: isConnecting || isInitializing,
address,
api,
error,
connect: handleConnect,
disconnect: handleDisconnect
}),
[
isConnected,
isConnecting,
isInitializing,
address,
api,
error,
handleConnect,
handleDisconnect
]
);
return /* @__PURE__ */ jsx4(MidnightWalletContext.Provider, { value: contextValue, children });
};
// src/wallet/connection.ts
var supportsMidnightWalletConnection = () => {
return typeof window !== "undefined" && !!window.midnight?.mnLace;
};
var getMidnightAvailableConnectors = async () => {
if (!supportsMidnightWalletConnection()) {
return [];
}
return [{ id: "mnLace", name: "Lace (Midnight)" }];
};
async function disconnectMidnightWallet() {
return { disconnected: true };
}
// src/adapter.ts
var MidnightAdapter = class {
constructor(networkConfig) {
__publicField(this, "networkConfig");
__publicField(this, "initialAppServiceKitName");
__publicField(this, "artifacts", null);
if (!isMidnightNetworkConfig(networkConfig)) {
throw new Error("MidnightAdapter requires a valid Midnight network configuration.");
}
this.networkConfig = networkConfig;
this.initialAppServiceKitName = "custom";
logger6.info(
"MidnightAdapter",
`Adapter initialized for network: ${networkConfig.name} (ID: ${networkConfig.id})`
);
}
getEcosystemReactUiContextProvider() {
return MidnightWalletProvider;
}
getEcosystemReactHooks() {
return midnightFacadeHooks;
}
getEcosystemWalletComponents() {
return {
ConnectButton,
AccountDisplay: CustomAccountDisplay
};
}
supportsWalletConnection() {
return supportsMidnightWalletConnection();
}
async getAvailableConnectors() {
return getMidnightAvailableConnectors();
}
connectWallet(_connectorId) {
logger6.warn(
"MidnightAdapter",
"The `connectWallet` method is not supported. Use the `ConnectButton` component from `getEcosystemWalletComponents()` instead."
);
return Promise.resolve({ connected: false, error: "Method not supported." });
}
disconnectWallet() {
return disconnectMidnightWallet();
}
getWalletConnectionStatus() {
return {
isConnected: false,
address: void 0,
chainId: this.networkConfig.id
};
}
getContractDefinitionInputs() {
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."
}
];
}
async loadContract(source) {
const artifacts = validateAndConvertMidnightArtifacts(source);
this.artifacts = artifacts;
logger6.info("MidnightAdapter", "Contract artifacts stored.", this.artifacts);
const { functions, events } = parseMidnightContractInterface(artifacts.contractSchema);
const schema = {
name: "MyMidnightContract",
// TODO: Extract from artifacts if possible
ecosystem: "midnight",
address: artifacts.contractAddress,
functions,
events
};
return schema;
}
getWritableFunctions(contractSchema) {
return contractSchema.functions.filter((fn) => fn.modifiesState);
}
mapParameterTypeToFieldType(_parameterType) {
return "text";
}
getCompatibleFieldTypes(_parameterType) {
return ["text"];
}
generateDefaultField(parameter) {
return {
id: parameter.name,
name: parameter.name,
label: parameter.name,
type: this.mapParameterTypeToFieldType(parameter.type),
validation: {}
};
}
formatTransactionData(_contractSchema, _functionId, _submittedInputs, _fields) {
throw new Error("formatTransactionData not implemented for MidnightAdapter.");
}
async signAndBroadcast(_transactionData, _executionConfig) {
throw new Error("signAndBroadcast not implemented for MidnightAdapter.");
}
isViewFunction(functionDetails) {
return !functionDetails.modifiesState;
}
async queryViewFunction(_contractAddress, _functionId, _params, _contractSchema) {
throw new Error("queryViewFunction not implemented for MidnightAdapter.");
}
formatFunctionResult(decodedValue) {
return JSON.stringify(decodedValue, null, 2);
}
async getSupportedExecutionMethods() {
return [];
}
async validateExecutionConfig(_config) {
return true;
}
getExplorerUrl(_address) {
return null;
}
getExplorerTxUrl(_txHash) {
return null;
}
isValidAddress(_address) {
return true;
}
async getAvailableUiKits() {
return [
{
id: "custom",
name: "OpenZeppelin Custom",
configFields: []
}
];
}
async getRelayers(_serviceUrl, _accessToken) {
logger6.warn("MidnightAdapter", "getRelayers is not implemented for the Midnight adapter yet.");
return Promise.resolve([]);
}
async getRelayer(_serviceUrl, _accessToken, _relayerId) {
logger6.warn("MidnightAdapter", "getRelayer is not implemented for the Midnight adapter yet.");
return Promise.resolve({});
}
/**
* @inheritdoc
*/
async validateRpcEndpoint(rpcConfig) {
return validateMidnightRpcEndpoint(rpcConfig);
}
/**
* @inheritdoc
*/
async testRpcConnection(rpcConfig) {
return testMidnightRpcConnection(rpcConfig);
}
};
var adapter_default = MidnightAdapter;
// src/networks/testnet.ts
var midnightTestnet = {
id: "midnight-testnet",
exportConstName: "midnightTestnet",
name: "Midnight Testnet",
ecosystem: "midnight",
network: "midnight-testnet",
type: "testnet",
isTestnet: true
// Add Midnight-specific fields here when known
// explorerUrl: '...',
// apiUrl: '...',
};
// src/networks/index.ts
var midnightTestnetNetworks = [midnightTestnet];
var midnightNetworks = [...midnightTestnetNetworks];
// src/config.ts
var midnightAdapterConfig = {
/**
* Default app name to display in the wallet connection UI.
*/
appName: "OpenZeppelin Contracts UI Builder",
/**
* Dependencies required by the Midnight adapter
* These will be included in exported projects that use this adapter
*/
dependencies: {
// TODO: Review and update with real, verified dependencies and versions before production release
// Runtime dependencies
runtime: {
// Core Midnight protocol libraries
"@midnight-protocol/sdk": "^0.8.2",
"@midnight-protocol/client": "^0.7.0",
// Encryption and privacy utilities
"libsodium-wrappers": "^0.7.11",
"@openzeppelin/contracts-upgradeable": "^4.9.3",
// Additional utilities for Midnight
"js-sha256": "^0.9.0",
"bn.js": "^5.2.1",
"@midnight-ntwrk/dapp-connector-api": "^3.0.0"
},
// Development dependencies
dev: {
// Testing utilities for Midnight
"@midnight-protocol/testing": "^0.5.0",
// Type definitions
"@types/libsodium-wrappers": "^0.7.10",
"@types/bn.js": "^5.1.1"
}
}
};
export {
MidnightAdapter,
adapter_default as default,
isMidnightContractArtifacts,
midnightAdapterConfig,
midnightNetworks,
midnightTestnet,
midnightTestnetNetworks
};
//# sourceMappingURL=index.js.map